# 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/<id>/`), 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 <name> &
   ```
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/<app>/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?}|"<vcard>"]}`
  → `{created, failed:[{index,error}]}` (≤5000/req, 8 MiB) — chunked client-side.
- All authenticated calls send `Authorization: Bearer <token>`; 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.
