# Ulti Suite — native shells (Tauri 2, Android + iOS) This workspace wraps the existing Next.js frontend (one repo, one static export) into a **suite of native apps** — one product per app — sharing a single Rust crate `ulti-core` for config, secure storage, push, deep links, share and contacts glue. ``` mobile/ ├─ Cargo.toml # Rust workspace (ulti-core + 5 apps) ├─ crates/ulti-core/ # shared plugin: secure store, push, contacts, share, deep-link ├─ apps/ # one Tauri app per product (generated from ultimail) │ ├─ ultimail/ → /mail ultimail:// space.ulti.mail │ ├─ ultidrive/ → /drive ultidrive:// space.ulti.drive │ ├─ ulticalmeet/ → /agenda ultiagenda:// space.ulti.cal (UltiCal + UltiMeet) │ ├─ ultiai/ → /chat ultichat:// space.ulti.ai │ └─ contacts/ → /contacts ulticontacts:// space.ulti.contacts ├─ scripts/new-app.mjs # scaffold/regenerate a sibling app from the pilot └─ native/ # platform scaffolds to paste into gen/ after init ├─ android/ # manifest snippets, DocumentsProvider, SSO provider ├─ ios/ # Share Extension, File Provider ├─ apple/ # entitlements + GoogleService-Info templates └─ firebase/ # google-services.json template ``` The frontend builds two ways from the **same** repo: | Mode | Command | `next.config` | Output | |------|---------|---------------|--------| | Web (unchanged) | `pnpm build` | `output: standalone`, rewrites/redirects, server API routes (`*.web.ts`) | `.next/` | | Mobile | `pnpm build:mobile` (`NEXT_PUBLIC_MOBILE=1`) | `output: export`, no server routes, `generateStaticParams` everywhere | `out/` | Tauri apps load `../../../../out` (the mobile export) at their start route. --- ## Build & run (developer machine) ### Prerequisites (toolchain) - **Rust via rustup** (not Homebrew on macOS — see `CLAUDE.md` § Mobile). Verify `which rustc` → `~/.cargo/bin/rustc`. - **Rust** stable + targets: - Android: `rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android` - iOS: `rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios` - **Node + pnpm** (repo root): `pnpm install` - **Tauri CLI**: depuis la racine du repo, via le wrapper par app (gère le layout imbriqué + `TAURI_CLI_CONFIG_DEPTH`) : ```bash pnpm tauri:ultimail android init pnpm tauri:ultimail android dev ``` Ne pas lancer `pnpm tauri … --config …` depuis la racine : le CLI cherche `src-tauri/tauri.conf.json` relativement au **cwd** de l'app (`mobile/apps//`), pas via `--config` seul. Alternative : `cargo install tauri-cli --version "^2"` (via rustup). - **Android**: Android Studio + SDK + NDK, `JAVA_HOME`, `ANDROID_HOME`, `NDK_HOME` - **iOS**: macOS + Xcode + CocoaPods, an Apple Developer account/team ### Frontend export (always first) ```bash # from repo root pnpm build:mobile # produces ./out (consumed by every app) ``` ### Rust compiles (no device needed — verified in CI) ```bash cd mobile cargo check # all crates: ulti-core + 5 apps ``` ### Desktop dev (exercise the native flow without a device) ```bash cd mobile/apps/ultimail/src-tauri cargo run # opens the app window pointing at /mail ``` ### Android / iOS (require SDK / Xcode — run on a configured machine) Run **per app** (cwd = the app's `src-tauri`, or pass `--config`): ```bash # one-time project generation (creates gen/android, gen/apple — gitignored) # from repo root: pnpm tauri:ultimail android init pnpm tauri:ultimail ios init # dev / build (requires a running emulator or USB device — see below) pnpm tauri:ultimail android dev pnpm tauri:ultimail android build # APK/AAB pnpm tauri:ultimail ios dev pnpm tauri:ultimail ios build # IPA ``` **Tauri dev frontend** — `beforeDevCommand` runs `mobile/scripts/dev-mobile-tauri.sh`: builds a static export (`out/`) and serves it on `:3005` for the WebView. Next.js `dev:mobile` (webpack HMR) is for browser-only; the Android WebView often never executes its dev chunks (stuck on the boot splash). First launch takes one `build:mobile` (~1–2 min); after UI changes, restart `android dev` or run `pnpm build:mobile` while serve is up and reload the app. **Browser mobile UI** (no Tauri): `pnpm dev:mobile` then open `http://localhost:3005/mail/`. **Android emulator / device** — `android dev` needs a target or it fails with `No available Android Emulator detected`: 1. **AVD** (Android Studio → Device Manager → Create Virtual Device), then start it: ```bash emulator -list-avds emulator -avd & ``` 2. **Phone USB** — enable developer mode + USB debugging, then `adb devices` must list it. 3. Retry: `pnpm tauri:ultimail android dev` > These were **not** run in the implementation environment (no Android SDK / > Xcode). Rust + the frontend export compile; the commands above are the exact > steps to produce device builds once `native/` scaffolds (below) are pasted in. ### Add / regenerate a sibling app ```bash cd mobile node scripts/new-app.mjs --all # regenerate the 4 siblings node scripts/new-app.mjs ultitasks UltiTasks space.ulti.tasks tasks /tasks/ ultitasks ``` --- ## Phase 0 — human setup checklist (config/accounts, cannot be automated) Everything below is config-driven with placeholders; fill these in: ### Authentik (OIDC) - [ ] Create **one public OIDC client** (native, PKCE, **no secret**) — or one per app. - [ ] Redirect URIs: `ultimail://oauth/callback`, `ultidrive://oauth/callback`, `ultiagenda://oauth/callback`, `ultichat://oauth/callback`, `ulticontacts://oauth/callback` (+ universal-link variants). - [ ] Note the **client_id** and issuer URL. For the hosted "UltiSpace" preset, set them in the runtime config defaults (`lib/runtime-config`); self-hosted users enter their instance URL in the server picker (OIDC autodiscovery via `/.well-known/openid-configuration`). ### Apple (iOS) - [ ] Apple Developer **Team ID** → replace `REPLACE_TEAM_ID` and set in Tauri signing. - [ ] App IDs `space.ulti.{mail,drive,cal,ai,contacts}` with capabilities: **Push Notifications**, **Keychain Sharing**, **App Groups**. - [ ] **APNs key** (.p8) → give to the BACKEND team (frontend never holds it). - [ ] App Group `group.space.ulti.suite` on every app + extension. - [ ] Keychain access group `space.ulti.suite` on every app (cross-app SSO). - [ ] Associated Domains `applinks:space.ulti.app` (universal links) — host `apple-app-site-association`. - [ ] Merge `native/apple/App.entitlements.template` into each `gen/apple/*.entitlements`. ### Android - [ ] Signing **keystore** (ONE key for all apps → enables signature-permission SSO). - [ ] Firebase project + `google-services.json` per package → copy (real file) into `gen/android/app/`. Template: `native/firebase/google-services.json.example`. Give the **FCM server key / service account** to the BACKEND team. - [ ] Host `assetlinks.json` at `https://space.ulti.app/.well-known/` for App Links. - [ ] Merge `native/android/AndroidManifest.snippets.xml` into each app manifest. ### Signing material (gitignored — never commit) `google-services.json`, `GoogleService-Info.plist`, `*.keystore`, `*.p8`, `*.p12`, `*.mobileprovision` — see `mobile/.gitignore`. --- ## Capabilities — status & wiring | Capability | Status | Notes | |-----------|--------|-------| | Static export + runtime config | ✅ done | `lib/runtime-config`, `lib/platform.ts`; env singletons removed | | Tauri workspace + 5 apps | ✅ done | `cargo check` green; icons copied from pilot | | Native OIDC PKCE + secure store | ✅ done | `lib/auth/native-auth.ts`, `ulti-core` store_* via `keyring` (desktop/iOS) | | Server picker | ✅ done | `components/mobile/server-picker.tsx` + OIDC discovery | | Push (app side) | ✅ done | `lib/native/push.ts` → `POST /devices/register`, `/unregister` on logout | | Deep links + inter-app | ✅ done | `lib/native/deep-links.ts`, `inter-app.ts`; launcher opens siblings | | Share send | ✅ done (mobile native TODO) | `shareOut` → `ulti-core share_out`; desktop opens URL, mobile needs platform glue | | Share receive | 🟡 scaffold | `native/ios/ShareExtension/*`, Android intent-filters; drained by `share_take_pending` | | Contacts import (file + device) | ✅ done | bulk `POST /contacts/books/{id}/import`; device source via `contacts_fetch` | | Cross-app SSO | 🟡 scaffold | iOS shared Keychain group (entitlement); Android `SharedSessionProvider` | | `/compte` + `/settings` everywhere | ✅ done | shared export; added `app/compte/[[...section]]` for the legacy path in mobile | | Drive mount | 🟡 scaffold | `native/android/drive/*` (SAF DocumentsProvider), `native/ios/FileProvider/*` | ### What is stubbed / placeholder and why - **Android secure store** (`ulti-core/src/secure_store.rs`): `keyring` has no Android backend, so Android uses an in-memory `HashMap` fallback. Production needs `EncryptedSharedPreferences` via a small JNI/native plugin (the `SharedSessionProvider` scaffold doubles as the cross-app store). Documented in the source. - **`share_out` on mobile** returns `share_out_native_not_wired`: the OS share sheet (Android `ACTION_SEND` / iOS `UIActivityViewController`) needs native code or `tauri-plugin-sharesheet`. Desktop fallback opens a URL. JS API + the command contract are in place. - **Share Extension / DocumentsProvider / File Provider** (`native/`): full Swift / Kotlin skeletons with the correct entry points and manifest/Info.plist, but the per-item HTTP calls to `/api/v1/drive` and the auth-token read from the shared store are marked `TODO`. They live under `native/` because the real targets live in the generated `gen/` projects (gitignored) created by `tauri {android,ios} init`. - **Push token retrieval** uses `ulti-core push_register`; the actual FCM/APNs token plumbing is provided by the notification plugin per platform and emitted on `ulti://push-token`. The backend owns all FCM/APNs credentials. - **Icons**: siblings reuse the UltiMail mark as a placeholder; replace `apps//src-tauri/icons/` per product before store submission. ### Backend contract (implemented against) - `POST /api/v1/devices/register` `{platform,app,push_token,device_id?}` → `{id}` - `POST /api/v1/devices/unregister` `{push_token}` → 204 (on logout) - `GET /api/v1/devices` → `{devices:[…]}` - `POST /api/v1/contacts/books/{bookID}/import` `{contacts:[{full_name,email?,phone?,org?,uid?,raw_vcard?}|""]}` → `{created, failed:[{index,error}]}` (≤5000/req, 8 MiB) — chunked client-side. - All authenticated calls send `Authorization: Bearer `; user is derived from the token, never sent in the body. - New-mail push payload: `data = {type:"mail.created", message_id, account_id}`. --- ## Verified in this environment - ✅ `pnpm build` (web standalone) — unchanged, passes. - ✅ `pnpm build:mobile` (static export) — passes; `/mail /drive /agenda /chat /contacts /account /compte /settings` all prerendered. - ✅ `cd mobile && cargo check` — `ulti-core` + all 5 apps compile. - ⏳ `tauri android build` / `tauri ios build` — not run (no SDK/Xcode here); commands + toolchain documented above.