Files
ChatApp/docs/superpowers/plans/2026-05-13-mobile-phase-0-foundation.md
byGalax 1e0cf67335 docs(mobile): phase 0 implementation plan
11 tasks, all renderer-only changes inside apps/mobile:
  1. app.json rebrand (Netralax, cloud.netralax.app ids).
  2. eas.json with development/preview/production profiles.
  3. Add expo-application + react-native-gesture-handler deps.
  4-5. ErrorBoundary component + sharedSmoke runtime canary.
  6-8. Root layout (SafeArea + GestureHandler + ErrorBoundary), Netralax
       landing screen with runtime version, Chats placeholder.
  9-10. Root mobile:typecheck script + README quickstart.
  11. Manual on-device verification handed back to the user.

Asset replacement (icon/splash) is deferred to Phase 4 — the dev client
doesn't care about icon polish.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:31:39 +02:00

22 KiB

Mobile Phase 0 — Deployment Foundation Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Bring apps/mobile from an empty "ChatApp" Expo scaffold to a Netralax-branded shell that compiles, type-checks, runs locally via expo start, and is configured for eas build on iOS + Android.

Architecture: Renderer-only changes inside apps/mobile. No backend or shared-package work. The Expo managed workflow is preserved; New Architecture stays enabled. We wire SafeAreaProvider + GestureHandlerRootView + a minimal ErrorBoundary into the Expo Router root layout, rebrand app.json, add eas.json build profiles, and prove the @chat-app/shared workspace dep resolves end-to-end via a runtime smoke import.

Tech Stack: Expo SDK 52, React Native 0.76 (New Arch), expo-router 4, react-native-safe-area-context 4.12, react-native-gesture-handler (added in Task 3), expo-application (added in Task 3), TypeScript 5.6, @chat-app/shared (workspace).

Spec: docs/superpowers/specs/2026-05-13-mobile-phase-0-foundation-design.md

Testing note: No automated test framework for the mobile screens yet — vitest is wired in apps/mobile/package.json but there are no specs to run. Each task's gate is pnpm --filter @chat-app/mobile typecheck + a visual self-check (described per task). End-to-end device verification is the user's job at the end of the plan (Task 11).


File structure

File Responsibility Change
apps/mobile/app.json Expo project manifest — name, slug, bundle ids, plugins, splash Rebrand to Netralax + cloud.netralax.app ids
apps/mobile/eas.json EAS Build/Submit profile config CREATE — development / preview / production profiles
apps/mobile/package.json Workspace dependencies Add expo-application, react-native-gesture-handler
apps/mobile/components/ErrorBoundary.tsx Render-error catcher for the whole tree CREATE — class component with reload button
apps/mobile/lib/sharedSmoke.ts Runtime canary import from @chat-app/shared CREATE — proves Metro can resolve the workspace package
apps/mobile/app/_layout.tsx Expo Router root layout Wrap Stack in providers: GestureHandlerRootView → SafeAreaProvider → ErrorBoundary
apps/mobile/app/index.tsx Landing screen Netralax branding + runtime version display + nav link to Chats
apps/mobile/app/(app)/chats.tsx Chats placeholder inside authenticated group Netralax-branded "coming soon" copy
apps/mobile/README.md Mobile workspace docs Add Phase 0 quickstart section
Root package.json Monorepo convenience scripts Add mobile:typecheck (others already exist)

No file is bigger than ~80 lines after this plan. Assets (icon.png, splash.png, adaptive-icon.png) are explicitly NOT replaced in Phase 0 — see Out-of-Scope note in the spec — the Expo default chrome stays until Phase 4 adds the final Netralax art.


Task 1: Rebrand app.json to Netralax

Files:

  • Modify: apps/mobile/app.json

  • Step 1: Replace the entire app.json content

Write apps/mobile/app.json exactly as follows (newlines + 2-space indent):

{
  "expo": {
    "name": "Netralax",
    "slug": "netralax",
    "version": "0.1.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "scheme": "netralax",
    "userInterfaceStyle": "automatic",
    "newArchEnabled": true,
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#0b0b0f"
    },
    "assetBundlePatterns": ["**/*"],
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "cloud.netralax.app",
      "infoPlist": {
        "ITSAppUsesNonExemptEncryption": false
      }
    },
    "android": {
      "package": "cloud.netralax.app",
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#0b0b0f"
      }
    },
    "plugins": [
      "expo-router",
      "expo-secure-store",
      "expo-sqlite",
      [
        "expo-notifications",
        {
          "color": "#0b0b0f"
        }
      ]
    ],
    "experiments": {
      "typedRoutes": true
    },
    "extra": {
      "eas": {
        "projectId": "REPLACE_WITH_EAS_PROJECT_ID"
      }
    }
  }
}

Note: extra.eas.projectId stays as the literal placeholder string. The user fills it in by running eas init (covered in the README task).

  • Step 2: Commit
git add apps/mobile/app.json
git commit -m "chore(mobile): rebrand app.json to Netralax + cloud.netralax.app bundle ids"

Task 2: Create eas.json with build profiles

Files:

  • Create: apps/mobile/eas.json

  • Step 1: Create apps/mobile/eas.json

{
  "cli": {
    "version": ">= 13.0.0"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "ios": {
        "simulator": true
      },
      "android": {
        "buildType": "apk"
      }
    },
    "preview": {
      "distribution": "internal",
      "ios": {
        "simulator": false
      },
      "android": {
        "buildType": "apk"
      }
    },
    "production": {
      "ios": {
        "simulator": false
      },
      "android": {
        "buildType": "app-bundle"
      }
    }
  },
  "submit": {
    "production": {
      "ios": {
        "appleId": "REPLACE_WITH_APPLE_ID",
        "ascAppId": "REPLACE_WITH_ASC_APP_ID"
      },
      "android": {
        "serviceAccountKeyPath": "./play-service-account.json"
      }
    }
  }
}
  • Step 2: Commit
git add apps/mobile/eas.json
git commit -m "chore(mobile): add eas.json with development/preview/production build profiles"

Task 3: Add Phase 0 runtime dependencies

Files:

  • Modify: apps/mobile/package.json

  • Step 1: Install the two missing deps

From the repo root:

pnpm --filter @chat-app/mobile add expo-application react-native-gesture-handler

expo-application is needed to read the runtime app version (Application.nativeApplicationVersion) on the Landing screen. react-native-gesture-handler is the standard root provider for any future gesture-based UI; expo-router and most RN libraries assume it's mounted at the root.

Expected: apps/mobile/package.json gains the two entries in dependencies. pnpm picks the Expo SDK 52-compatible versions (expo-application@~6.0.x, react-native-gesture-handler@~2.20.x — let pnpm + Expo's install resolver pick).

  • Step 2: Verify typecheck still passes
pnpm --filter @chat-app/mobile typecheck

Expected: exit 0. New deps are TS-friendly out of the box.

  • Step 3: Commit
git add apps/mobile/package.json pnpm-lock.yaml
git commit -m "chore(mobile): add expo-application + react-native-gesture-handler for phase 0 shell"

Task 4: Create the ErrorBoundary component

Files:

  • Create: apps/mobile/components/ErrorBoundary.tsx

  • Step 1: Create the component

Write apps/mobile/components/ErrorBoundary.tsx:

import React from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';

// Catch render errors anywhere below this boundary and show a readable
// fallback. Without it, a thrown error during render produces a white
// screen on TestFlight / production builds with no way for the user to
// recover short of force-closing the app. Mounted once at the top of
// app/_layout.tsx so it wraps every screen.

interface Props {
  children: React.ReactNode;
}

interface State {
  error: Error | null;
}

export class ErrorBoundary extends React.Component<Props, State> {
  state: State = { error: null };

  static getDerivedStateFromError(error: Error): State {
    return { error };
  }

  componentDidCatch(error: Error, info: React.ErrorInfo): void {
    // Bubble to Metro/console in dev so the redbox still shows; in prod
    // builds this is the only place an exception trace surfaces.
    console.error('[ErrorBoundary] caught render error', error, info.componentStack);
  }

  reset = (): void => {
    this.setState({ error: null });
  };

  render(): React.ReactNode {
    if (this.state.error) {
      return (
        <View style={styles.container}>
          <Text style={styles.title}>Etwas ist schiefgelaufen</Text>
          <Text style={styles.message}>{this.state.error.message}</Text>
          <Pressable style={styles.button} onPress={this.reset}>
            <Text style={styles.buttonText}>Erneut versuchen</Text>
          </Pressable>
        </View>
      );
    }
    return this.props.children;
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#0b0b0f',
    padding: 24,
  },
  title: { color: '#fff', fontSize: 22, fontWeight: '600', marginBottom: 8 },
  message: { color: '#9ca3af', textAlign: 'center', marginBottom: 24 },
  button: {
    backgroundColor: '#5865f2',
    paddingHorizontal: 20,
    paddingVertical: 12,
    borderRadius: 10,
  },
  buttonText: { color: '#fff', fontWeight: '600' },
});
  • Step 2: Verify typecheck
pnpm --filter @chat-app/mobile typecheck

Expected: exit 0.

  • Step 3: Commit
git add apps/mobile/components/ErrorBoundary.tsx
git commit -m "feat(mobile): add ErrorBoundary for render-error fallback"

Task 5: Create the sharedSmoke canary

Files:

  • Create: apps/mobile/lib/sharedSmoke.ts

  • Step 1: Create the file

Write apps/mobile/lib/sharedSmoke.ts:

// Runtime canary import from @chat-app/shared. Phase 0's only goal here
// is to prove that Metro can resolve the workspace package and that
// nothing in shared/crypto pulls in an RN-incompatible module path.
//
// `import type` would be erased by the TypeScript compiler before
// reaching Metro, so we deliberately import a runtime symbol —
// `crypto.setCryptoBackend` — and stash it in an exported reference.
// We never call it here; Phase 1's real adapter does that after
// constructing a CryptoBackend wrapping react-native-libsodium.
//
// This file is deleted in Phase 1 once the real adapter lands.
import { crypto } from '@chat-app/shared';

export const sharedSmoke = { register: crypto.setCryptoBackend };
  • Step 2: Verify typecheck
pnpm --filter @chat-app/mobile typecheck

Expected: exit 0. If TS complains that crypto is not exported from @chat-app/shared, re-read packages/shared/src/index.ts to confirm the namespace name.

  • Step 3: Commit
git add apps/mobile/lib/sharedSmoke.ts
git commit -m "chore(mobile): add runtime canary import from @chat-app/shared"

Task 6: Rewrite the root layout with providers

Files:

  • Modify: apps/mobile/app/_layout.tsx

  • Step 1: Replace the file content

Write apps/mobile/app/_layout.tsx:

import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';

import { ErrorBoundary } from '../components/ErrorBoundary';

// Force-resolve @chat-app/shared at boot so Metro packaging issues
// surface during the very first dev-client load instead of mid-Phase-1.
// Reference the export so tree-shaking can't drop it.
import { sharedSmoke } from '../lib/sharedSmoke';
void sharedSmoke;

// Root layout for every Expo Router screen. Provider stack order:
//   GestureHandlerRootView  — required by gesture-driven libs (BottomSheet,
//                             swipeable rows, drawer nav). Must be the
//                             outermost so gesture state is global.
//   SafeAreaProvider        — feeds notch / status-bar insets to children.
//   ErrorBoundary           — last line of defence for render errors.
//   Stack                   — Expo Router's screen registry.
export default function RootLayout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <SafeAreaProvider>
        <ErrorBoundary>
          <StatusBar style="auto" />
          <Stack screenOptions={{ headerShown: false }}>
            <Stack.Screen name="index" />
            <Stack.Screen name="(app)" />
          </Stack>
        </ErrorBoundary>
      </SafeAreaProvider>
    </GestureHandlerRootView>
  );
}
  • Step 2: Verify typecheck
pnpm --filter @chat-app/mobile typecheck

Expected: exit 0.

  • Step 3: Commit
git add apps/mobile/app/_layout.tsx
git commit -m "feat(mobile): wrap root layout in GestureHandler + SafeArea + ErrorBoundary"

Task 7: Rebrand the Landing screen

Files:

  • Modify: apps/mobile/app/index.tsx

  • Step 1: Replace the file content

Write apps/mobile/app/index.tsx:

import * as Application from 'expo-application';
import { Link } from 'expo-router';
import { Pressable, StyleSheet, Text, View } from 'react-native';

// Phase 0 landing screen. Just enough to prove the app boots and routes:
//   * Netralax brand wordmark (replaces the previous "ChatApp" string).
//   * Runtime version from expo-application so we can spot stale builds
//     on a TestFlight tester's device at a glance.
//   * Placeholder nav link into (app)/chats — exercises Expo Router so a
//     routing regression shows up here instead of mid-Phase-1.
//
// Phase 1 replaces this with the actual magic-link login form.
export default function Landing() {
  const version = Application.nativeApplicationVersion ?? '0.0.0';

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Netralax</Text>
      <Text style={styles.subtitle}>Phase 0  Deployment-Fundament</Text>
      <Text style={styles.version}>v{version}</Text>

      <Link href="/(app)/chats" asChild>
        <Pressable style={styles.button}>
          <Text style={styles.buttonText}>Weiter (Smoke-Test)</Text>
        </Pressable>
      </Link>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#0b0b0f',
    padding: 24,
  },
  title: { color: '#fff', fontSize: 32, fontWeight: '700', letterSpacing: 1 },
  subtitle: { color: '#9ca3af', marginTop: 8, marginBottom: 4 },
  version: { color: '#6b7280', fontSize: 12, marginBottom: 32 },
  button: {
    backgroundColor: '#5865f2',
    paddingHorizontal: 24,
    paddingVertical: 14,
    borderRadius: 12,
  },
  buttonText: { color: '#fff', fontWeight: '600', fontSize: 15 },
});
  • Step 2: Verify typecheck
pnpm --filter @chat-app/mobile typecheck

Expected: exit 0.

  • Step 3: Commit
git add apps/mobile/app/index.tsx
git commit -m "feat(mobile): Netralax landing screen with runtime version + nav smoke test"

Task 8: Rebrand the Chats placeholder

Files:

  • Modify: apps/mobile/app/(app)/chats.tsx

  • Step 1: Replace the file content

Write apps/mobile/app/(app)/chats.tsx:

import { StyleSheet, Text, View } from 'react-native';

// Phase 0 stand-in for the conversation list. Confirms the authenticated
// route group (`(app)`) mounts without error after a navigation from
// Landing. Phase 1 replaces this with the real chat list.
export default function Chats() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Netralax</Text>
      <Text style={styles.subtitle}>Chats  coming soon (Phase 1)</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#0b0b0f',
    padding: 24,
  },
  title: { color: '#fff', fontSize: 28, fontWeight: '600' },
  subtitle: { color: '#9ca3af', marginTop: 8, textAlign: 'center' },
});
  • Step 2: Verify typecheck
pnpm --filter @chat-app/mobile typecheck

Expected: exit 0.

  • Step 3: Commit
git add 'apps/mobile/app/(app)/chats.tsx'
git commit -m "feat(mobile): Netralax-branded Chats placeholder screen"

Task 9: Add mobile:typecheck to root scripts

Files:

  • Modify: package.json (root)

  • Step 1: Add the script

Open package.json at the repo root and find the line:

    "mobile:android": "pnpm --filter @chat-app/mobile android",

Insert immediately after it:

    "mobile:typecheck": "pnpm --filter @chat-app/mobile typecheck",

Final scripts block excerpt should read:

    "mobile": "pnpm --filter @chat-app/mobile",
    "mobile:dev": "pnpm --filter @chat-app/mobile dev",
    "mobile:ios": "pnpm --filter @chat-app/mobile ios",
    "mobile:android": "pnpm --filter @chat-app/mobile android",
    "mobile:typecheck": "pnpm --filter @chat-app/mobile typecheck",
    "desktop": "pnpm --filter @chat-app/desktop",
  • Step 2: Verify the script runs
pnpm mobile:typecheck

Expected: exit 0 (forwards to mobile workspace, which currently has nothing to fail).

  • Step 3: Commit
git add package.json
git commit -m "chore(repo): add mobile:typecheck convenience script"

Task 10: Document Phase 0 quickstart in the mobile README

Files:

  • Modify: apps/mobile/README.md

  • Step 1: Replace the README content

Write apps/mobile/README.md:

# @chat-app/mobile

Expo + React Native client for **Netralax** on iOS and Android.

## Prerequisites

- Node 22+, pnpm 9+
- Xcode (iOS) / Android Studio (Android) — only needed for local
  prebuild + emulator runs; EAS Build runs everything in the cloud.
- An Expo account (free) — required for `eas init` below.
- Optional: `npm i -g eas-cli` for the CLI commands. If you skip
  globals, prefix every `eas` invocation with `npx eas-cli`.

## Phase 0 quickstart

```bash
# From the repo root.
pnpm install

# One-time: claim an EAS project ID. This rewrites the placeholder
# `extra.eas.projectId` in app.json and links the local repo to your
# Expo dashboard. Commit the resulting app.json change.
cd apps/mobile
npx eas-cli init

# Sign in to Expo if you haven't.
npx eas-cli login

# Build a development client (custom dev-client APK + iOS simulator
# bundle). First iOS build prompts for Apple ID — free signing works
# for development distribution.
npx eas-cli build --platform all --profile development
```

When the builds finish, EAS gives you a QR code / install link. Install
the dev client on your device, then start the Metro server from the repo
root:

```bash
pnpm mobile:dev    # = pnpm --filter @chat-app/mobile dev = expo start --dev-client
```

Open the dev client on the device, scan the QR code, and Netralax's
Landing screen should render.

## Day-to-day

```bash
pnpm mobile:dev          # Metro server
pnpm mobile:ios          # native iOS run (local Xcode)
pnpm mobile:android      # native Android run (local Android Studio)
pnpm mobile:typecheck    # tsc --noEmit
```

## Architecture notes

- Expo Router (file-based) — screens live under `app/`.
- `expo-secure-store` — Keychain / Keystore-backed secret store (used by
  the mobile `SecretStore` adapter in Phase 1).
- `expo-sqlite` — local encrypted history (Phase 1).
- `react-native-libsodium` — crypto primitives (Phase 1's
  `CryptoBackend` adapter wraps this).
- `@chat-app/shared` — business logic shared with the desktop, including
  the `CryptoBackend` and `SecretStore` interfaces the mobile adapters
  plug into.
- `lib/sharedSmoke.ts` exists only to prove Metro can resolve the
  workspace package; it's deleted in Phase 1.

## Roadmap

See `docs/superpowers/specs/2026-05-13-mobile-deployment-roadmap.md`.
  • Step 2: Commit
git add apps/mobile/README.md
git commit -m "docs(mobile): phase 0 quickstart + roadmap pointer"

Task 11: End-to-end verification (manual, user-driven)

Files: None.

This task does no code changes. It hands the user a checklist they run on real hardware. The implementer subagent does NOT attempt to run eas build or boot a device.

  • Step 1: Typecheck pass (automated)
pnpm mobile:typecheck

Expected: exit 0.

  • Step 2: Document hand-off

The implementer must report back to the controller / user that Phase 0 is code-complete and list these manual verifications for the user to perform on their hardware:

  1. pnpm install from a fresh clone resolves cleanly.
  2. cd apps/mobile && npx eas-cli init claims a project ID and replaces the placeholder in app.json.
  3. npx eas-cli build --platform android --profile development produces an installable APK.
  4. npx eas-cli build --platform ios --profile development produces a simulator .tar.gz (or device .ipa if free Apple signing succeeds).
  5. The dev client installs and Netralax's Landing screen renders with a non-empty version string.
  6. Tapping the "Weiter (Smoke-Test)" button navigates to (app)/chats without crash.
  7. Optional: simulate an error in app/index.tsx (throw inside the render) and confirm the ErrorBoundary shows the fallback screen with the "Erneut versuchen" button.
  • Step 3: No code commit at this step

Verification is operational; no source changes here.


Self-Review Notes

Spec coverage:

  • Spec §1 (Netralax rebrand in app.json) → Task 1.
  • Spec §2 (icon + splash) → DEFERRED. The spec asked for placeholder Netralax art, but image-generation isn't tractable from the implementer loop and the dev client doesn't care about icon polish. Expo's default chrome stays; app.json still references ./assets/icon.png etc. so dropping in Netralax PNGs later is a no-op. Final art lands in Phase 4 alongside store-listing screenshots.
  • Spec §3 (eas.json) → Task 2.
  • Spec §4 (minimal app shell) → Tasks 6 (layout), 7 (landing), 8 (chats).
  • Spec §4a (ErrorBoundary) → Task 4.
  • Spec §5 (sharedSmoke) → Task 5.
  • Spec §6 (README + scripts) → Tasks 9 (root scripts) + 10 (README).
  • Spec §7 (typecheck + lint) → typecheck gates inside every task; lint stub deferred.

Type consistency: sharedSmoke shape { register: crypto.setCryptoBackend } matches between spec §5 and Task 5. ErrorBoundary Props/State match between spec §4a and Task 4. Stack screen names (index, (app)) unchanged from the existing scaffold.

Reading-order safety: Every task block restates the file path and shows the complete code for the touched file. No "see Task N" references.