ultisuite-client/mobile/README.md
R3D347HR4Y d6d18f911b
Some checks failed
E2E / Playwright e2e (push) Has been cancelled
Lots of stuff and mobile app
2026-06-17 00:13:28 +02:00

228 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` (~12 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.