11 tasks taking the mobile app from Phase 0 shell to a working end-to-end magic-link login + conversation list + decrypted detail + text-send. All renderer-only, plumbed through @chat-app/shared's CryptoBackend / SecretStore / Supabase-session-storage interfaces: 1. Foundation deps (AsyncStorage), env reader, theme constants. 2. Crypto + secret-store + session-storage adapters. 3. Supabase client. 4. AuthProvider + cleanup of Phase-0 sharedSmoke canary. 5. Session-gated (app) layout. 6. Magic-link login screen. 7. Auth-callback deep-link screen. 8. Avatar + ConversationRow + timeFormat primitives. 9. Conversation list with pull-to-refresh + logout. 10. Conversation detail with decrypt + send. 11. Workspace-wide typecheck pass. Push notifications, realtime, attachments, voice messages, edits, reactions, calls are explicitly deferred to later phases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
10 KiB
Mobile Phase 1 — Auth + Chat MVP
Date: 2026-05-13
Scope: apps/mobile
Roadmap context: 2026-05-13-mobile-deployment-roadmap.md
Problem
Phase 0 delivered a Netralax-branded shell. It boots, renders a placeholder, and is configured for eas build, but the app has no actual functionality. To call mobile "deployment-ready with working features" the user must be able to log in, see their chats, and send a text message — the smallest end-to-end vertical slice that's also useful.
Goal
After Phase 1, a Netralax user on iOS or Android can:
- Open the app, enter their account email, request a magic link.
- Tap the link in their email (deep link returns to
netralax://auth/callback), complete the session, and land on the chat list. - See their existing conversations (DMs + groups) with last-message previews, decrypted on-device.
- Open a conversation, see the message history (decrypted), and send a new text message that the desktop client receives correctly.
- Log out from a settings entry, returning to the login screen.
Non-goals
- Signup with invite code (login-only MVP — accounts are provisioned via desktop or admin scripts).
- Attachments, voice messages, reactions, edits (Phase 2).
- Calls (Phase 3).
- Push notifications (deferred — chat works without push, the user pulls fresh data on screen focus / pull-to-refresh).
- Realtime subscription on conversations (deferred). Phase 1 polls / refetches on screen focus.
- Read receipts, typing indicator, presence (Phase 2).
- Friend list, profile edit, settings beyond logout.
Design
1. Theme constants
apps/mobile/theme/colors.ts exports a flat colors object with the hex codes Phase 0 inlined across files (#0b0b0f, #9ca3af, #fff, #6b7280, #5865f2) plus a couple of new ones we need for Phase 1 (input border, danger). Every Phase 1 screen imports from here; no new hex literals appear in components.
2. Crypto adapter (apps/mobile/lib/cryptoBackend.ts)
A near-mirror of apps/desktop/src/lib/cryptoBackend.ts but wrapping react-native-libsodium instead of libsodium-wrappers-sumo. The two libraries expose the same primitive API (the RN port re-exports the libsodium-wrappers types) — only the import + the absence of _sodium.ready differs. Synchronous (no WASM warm-up needed on a native lib). Registered at boot via crypto.setCryptoBackend(createLibsodiumBackend()) from _layout.tsx.
3. Secret store adapter (apps/mobile/lib/secretStore.ts)
Implements SecretStore (from @chat-app/shared/auth/secure-storage) backed by expo-secure-store. Per the contract, values are Uint8Array; we base64-encode at the boundary because SecureStore only accepts strings. buffer polyfill is part of RN's base set, no extra install.
4. Supabase session storage (apps/mobile/lib/sessionStorage.ts)
Supabase's JS client needs an async-storage object for session tokens. Use @react-native-async-storage/async-storage (new dep) — supabase-js v2 accepts its getItem / setItem / removeItem API directly.
5. Env config (apps/mobile/lib/env.ts)
Expo exposes vars prefixed EXPO_PUBLIC_* to the bundle via process.env. Read three:
EXPO_PUBLIC_SUPABASE_URLEXPO_PUBLIC_SUPABASE_ANON_KEYEXPO_PUBLIC_AUTH_REDIRECT_URL— defaults tonetralax://auth/callback.
apps/mobile/.env.example is added with placeholder values + a README pointer.
6. Supabase client (apps/mobile/lib/supabase.ts)
Mirrors desktop's apps/desktop/src/lib/supabase.ts. Calls createClient from @chat-app/shared/supabase with the mobile sessionStorage adapter and detectSessionInUrl: false (the callback screen parses the URL manually).
7. Auth context (apps/mobile/lib/authContext.tsx)
React Context provider that wraps the authenticated tree. Exposes:
session: Session | nulluser: User | nulldevice: DeviceRecord | null— the registered device for this installloading: boolean— true while hydrating from AsyncStorage on bootsignOut(): Promise<void>
On mount:
await supabase.auth.getSession()— pulls from AsyncStorage.- If a session exists, derive the device by
listOwnDevices(supabase)and matching on a locally-stored device id (insecretStoreunder keydevice.id). - If session exists but no device record yet (fresh install, account exists on other devices), call
registerDevicewith a fresh key pair, store the new device id + private key insecretStore.
The provider subscribes to supabase.auth.onAuthStateChange so signing out from anywhere updates the tree.
8. Route gating
app/index.tsx: ifsessionis set, redirect to/(app)/chats. Otherwise show the login form.app/(app)/_layout.tsx: if nosession, redirect to/. (Existing file: change from "always render Stack" to "guard on session".)
9. Login screen (app/index.tsx)
Replaces the Phase-0 placeholder. Renders:
- Netralax wordmark.
- Email
TextInput. - "Magic Link senden" button →
auth.loginWithMagicLink(supabase, email, env.authRedirectUrl). - After tap: show "Check deine Mails" state until the deep link fires.
- Error banner on failure.
10. Auth callback (app/auth/callback.tsx)
A new screen reachable via the netralax://auth/callback deep link. Reads URL params (Supabase magic-link callback puts access_token + refresh_token in the URL hash or query), calls supabase.auth.setSession({...}), routes to /(app)/chats. On failure, routes back to / with an error message.
Expo Router auto-handles the deep link → screen mapping when the scheme in app.json matches and the path matches the route file.
11. Conversation list (app/(app)/chats.tsx)
Replaces the Phase-0 placeholder. Uses chat.listConversations(supabase) from @chat-app/shared/chat. Renders a FlatList of ConversationRow (new component):
- DM: peer's display name + Avatar (initial-letter circle).
- Group: group name + "N Mitglieder".
- Last message preview — decrypted via
decryptMessagesif encrypted, else "…". - Timestamp (relative — "Vor 5 Min").
Pull-to-refresh refetches.
Tap on row → navigates to /(app)/conversations/[id].
A small icon-button in the header opens a logout modal calling signOut().
12. Conversation detail (app/(app)/conversations/[id].tsx)
Loads:
fetchConversationMessages(supabase, { conversationId, limit: 50 }).listConversationDeviceKeys(supabase, conversationId)for decryption.decryptMessages({ messages, deviceKeys, myDeviceId, myPrivateKey })to get plaintext.
Renders an inverted FlatList (newest at the bottom, like Discord). Each row shows sender name + body + timestamp. Attachments rendered as "📎 [Anhang]" placeholders — Phase 2 adds real rendering.
Bottom: a TextInput + send button. On send: sendEncryptedMessage. Optimistically appends to local state, then refetches.
13. Cleanup
- Delete
apps/mobile/lib/sharedSmoke.tsand thevoid sharedSmokeimport in_layout.tsx.
File structure (after Phase 1)
apps/mobile/
├── app/
│ ├── _layout.tsx ← MODIFIED: register crypto backend, mount AuthProvider
│ ├── index.tsx ← MODIFIED: Login screen (magic link form)
│ ├── auth/
│ │ └── callback.tsx ← NEW: deep-link landing
│ └── (app)/
│ ├── _layout.tsx ← MODIFIED: session-gated stack
│ ├── chats.tsx ← MODIFIED: real conversation list + logout
│ └── conversations/
│ └── [id].tsx ← NEW: conversation view + send
├── components/
│ ├── ErrorBoundary.tsx ← unchanged
│ ├── Avatar.tsx ← NEW: initial-letter avatar circle
│ └── ConversationRow.tsx ← NEW: list row
├── lib/
│ ├── authContext.tsx ← NEW
│ ├── cryptoBackend.ts ← NEW
│ ├── env.ts ← NEW
│ ├── secretStore.ts ← NEW
│ ├── sessionStorage.ts ← NEW
│ ├── supabase.ts ← NEW
│ ├── timeFormat.ts ← NEW
│ └── sharedSmoke.ts ← DELETED
├── theme/
│ └── colors.ts ← NEW
├── .env.example ← NEW
└── package.json ← MODIFIED: + @react-native-async-storage/async-storage
Risks
react-native-libsodiumNew Architecture compatibility. v1.7 claims Fabric/TurboModule support. IfsetCryptoBackend(createLibsodiumBackend())crashes on app boot under New Arch, fall back to"newArchEnabled": falseand re-evaluate.@chat-app/sharedESM resolution under Metro. Shared emits ESM with.jsimport specifiers. Metro's default resolver handles this; if it doesn't, setmetro.config.js'sresolver.unstable_enablePackageExports: true.- Magic-link deep-link return. Expo Router auto-maps
netralax://auth/callbacktoapp/auth/callback.tsxsince the scheme inapp.jsonmatches (netralax) andexpo-linkingis in the prebuild. - Device key persistence. The private key blob lives in
expo-secure-store(Keychain on iOS, Keystore on Android). Uninstalling the app wipes the key — same as desktop. Backup/restore is post-Phase-4.
Verification (manual on real hardware after typecheck passes)
pnpm --filter @chat-app/mobile typecheck→ exit 0..env.localpopulated with the sameSUPABASE_URL/SUPABASE_ANON_KEYthe desktop uses → app boots, shows Login.- Enter an existing-user email → "Magic Link senden" succeeds → "Check deine Mails" state appears.
- Open the email link on the device → callback URL fires Expo Router → app navigates to
/(app)/chats. - Chat list loads; existing DMs and groups from the desktop are visible with decrypted previews.
- Tap a conversation → detail screen opens, history loads, decrypted message bodies render newest-at-bottom.
- Type "test from mobile" → send → desktop client of the same account sees the new message via Supabase realtime.
- Pull-to-refresh on chat list re-fetches.
- Tap settings → "Abmelden" → returns to Login screen, session cleared from AsyncStorage.
Out of scope
- Push notifications (
expo-notifications+notify-pushedge function). - Realtime subscription on the mobile side (relies on focus-refetch).
- Conversation creation (new DM / new group) from mobile.
- Attachments, voice messages, reactions, edits, replies, forwards (Phase 2).
- Read receipts, typing, delivery state (Phase 2).
- Voice/video calls (Phase 3).
- Backup/restore device keys (post-Phase-4).
- Profile editing, friends, admin tools (post-Phase-4).