|
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Refactored metadata for contacts, administration, and Ulticards pages to utilize dynamic app names and descriptions. - Introduced new product pages for Ultiai, Ultical, Ulticards, Ultidrive, Ultimail, and Ultimeet with appropriate metadata. - Enhanced layout components to ensure consistent styling and functionality across new product sections. - Updated various components to replace hardcoded labels with dynamic references to improve maintainability and consistency. |
||
|---|---|---|
| .. | ||
| apps | ||
| crates/ulti-core | ||
| native | ||
| scripts | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
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). Verifywhich 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
- Android:
- 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) :
Ne pas lancerpnpm tauri:ultimail android init pnpm tauri:ultimail android devpnpm tauri … --config …depuis la racine : le CLI cherchesrc-tauri/tauri.conf.jsonrelativement au cwd de l'app (mobile/apps/<id>/), pas via--configseul. 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)
# from repo root
pnpm build:mobile # produces ./out (consumed by every app)
Rust compiles (no device needed — verified in CI)
cd mobile
cargo check # all crates: ulti-core + 5 apps
Desktop dev (exercise the native flow without a device)
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):
# 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:
- AVD (Android Studio → Device Manager → Create Virtual Device), then start it:
emulator -list-avds emulator -avd <name> & - Phone USB — enable developer mode + USB debugging, then
adb devicesmust list it. - 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
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_IDand 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.suiteon every app + extension. - Keychain access group
space.ulti.suiteon every app (cross-app SSO). - Associated Domains
applinks:space.ulti.app(universal links) — hostapple-app-site-association. - Merge
native/apple/App.entitlements.templateinto eachgen/apple/*.entitlements.
Android
- Signing keystore (ONE key for all apps → enables signature-permission SSO).
- Firebase project +
google-services.jsonper package → copy (real file) intogen/android/app/. Template:native/firebase/google-services.json.example. Give the FCM server key / service account to the BACKEND team. - Host
assetlinks.jsonathttps://space.ulti.app/.well-known/for App Links. - Merge
native/android/AndroidManifest.snippets.xmlinto 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):keyringhas no Android backend, so Android uses an in-memoryHashMapfallback. Production needsEncryptedSharedPreferencesvia a small JNI/native plugin (theSharedSessionProviderscaffold doubles as the cross-app store). Documented in the source. share_outon mobile returnsshare_out_native_not_wired: the OS share sheet (AndroidACTION_SEND/ iOSUIActivityViewController) needs native code ortauri-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/driveand the auth-token read from the shared store are markedTODO. They live undernative/because the real targets live in the generatedgen/projects (gitignored) created bytauri {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 onulti://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 /settingsall 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.