ultisuite-client/mobile
R3D347HR4Y efaaf16f60
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat: update metadata and layout for new product pages
- 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.
2026-06-19 22:11:42 +02:00
..
apps feat: update metadata and layout for new product pages 2026-06-19 22:11:42 +02:00
crates/ulti-core feat: update metadata and layout for new product pages 2026-06-19 22:11:42 +02:00
native Lots of stuff and mobile app 2026-06-17 00:13:28 +02:00
scripts Lots of stuff and mobile app 2026-06-17 00:13:28 +02:00
.gitignore Lots of stuff and mobile app 2026-06-17 00:13:28 +02:00
Cargo.lock feat: update metadata and layout for new product pages 2026-06-19 22:11:42 +02:00
Cargo.toml feat: update metadata and layout for new product pages 2026-06-19 22:11:42 +02:00
README.md Lots of stuff and mobile app 2026-06-17 00:13:28 +02:00

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) :
    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)

# 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 frontendbeforeDevCommand 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 / deviceandroid 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:
    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

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.tsPOST /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) shareOutulti-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 checkulti-core + all 5 apps compile.
  • tauri android build / tauri ios build — not run (no SDK/Xcode here); commands + toolchain documented above.