228 lines
11 KiB
Markdown
228 lines
11 KiB
Markdown
# 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.
|