No description
  • TypeScript 93.5%
  • JavaScript 4.5%
  • Shell 1.7%
  • Nix 0.3%
Find a file
2026-08-23 15:39:44 +07:00
.agent-memory FLA-84: Configure Apple and Google sign-in 2026-08-23 15:35:29 +07:00
.codex/skills chore: openspec init 2026-04-28 11:27:16 +02:00
.maestro FLA-73: Simplify review grading actions 2026-08-19 16:10:26 +07:00
.pi FLA-15: Add project-local Instant MCP 2026-07-17 21:16:22 +07:00
__tests__ FLA-84: Configure Apple and Google sign-in 2026-08-23 15:35:29 +07:00
assets chore: add a test icon for iOS 2026-04-02 19:02:22 +02:00
docs FLA-80: Enforce feature package boundaries 2026-08-17 15:04:57 +07:00
scripts FLA-84: Configure Apple and Google sign-in 2026-08-23 15:35:29 +07:00
src FLA-84: Configure Apple and Google sign-in 2026-08-23 15:35:29 +07:00
.dir-locals.el nix flake init 2026-02-18 15:25:05 +01:00
.envrc FLA-000: tidy up workspace 2026-08-17 15:40:11 +07:00
.gitignore refactor(cards): isolate update card planning 2026-04-28 15:50:10 +02:00
.prettierignore FLA-80: Enforce feature package boundaries 2026-08-17 15:04:57 +07:00
.prettierrc.json Install prettier 2026-02-19 10:44:16 +01:00
AGENTS.md FLA-84: Require review before commits 2026-08-23 15:39:44 +07:00
app.config.js FLA-84: Configure Apple and Google sign-in 2026-08-23 15:35:29 +07:00
app.json FLA-84: Configure Apple and Google sign-in 2026-08-23 15:35:29 +07:00
babel.config.js Install unistyles 2026-02-19 11:58:34 +01:00
bun.lock FLA-84: Configure Apple and Google sign-in 2026-08-23 15:35:29 +07:00
eas.json FLA-84: Configure Apple and Google sign-in 2026-08-23 15:35:29 +07:00
eslint.config.js FLA-80: Close package boundary enforcement gaps 2026-08-17 15:20:06 +07:00
flake.lock nix flake init 2026-02-18 15:25:05 +01:00
flake.nix FLA-28: Add initial Maestro create-and-review test 2026-08-05 16:36:38 +07:00
index.ts fix(entry): load metro runtime first 2026-04-30 19:22:46 +02:00
metro.config.js Setup Sentry 2026-07-10 15:18:54 +02:00
package.json FLA-84: Configure Apple and Google sign-in 2026-08-23 15:35:29 +07:00
README.md FLA-84: Configure Apple and Google sign-in 2026-08-23 15:35:29 +07:00
skills-lock.json chore: install instantdb skill 2026-04-08 15:03:44 +02:00
tsconfig.json Fix TypeScript path config 2026-07-06 13:54:43 +02:00

Flashcards

Maestro on the iPhone simulator

Run the iPhone-only Maestro suite against the dedicated test build and InstantDB app:

bun run maestro:test:iphone

The package command loads the dev/test InstantDB identifiers, the test Google iOS identifier, MAESTRO_TEST_EMAIL, and the sensitive MAESTRO_TEST_CODE from the EAS development environment. Keep the test-user credentials in EAS and out of the repository. The runner uses the iPhone 17 Pro with iOS 26.0 by default. It boots or creates that exact simulator, generates a clean native project for the test app variant, builds and installs a bundled Release app, verifies the bundle and embedded variant/backend/provider fingerprints, runs the functional suite in English, runs the localization smoke test in German, and restores the simulator to English with the standard Large Dynamic Type size.

The runner builds de.totap.flashcards.test with APP_VARIANT=test. That one variant selects TEST_INSTANT_APP_ID and TEST_GOOGLE_IOS_CLIENT_ID; there is no independent public backend or variant override. Before the destructive reset, the runner verifies the test app ID against a checked-in SHA-256 allowlist, verifies the built and installed configuration markers, and verifies that Settings shows MAESTRO_TEST_EMAIL. It refuses to run against another InstantDB app or authenticated user.

The test build uses the permanent test code without requesting a new email code, and installing a rebuilt test bundle preserves its authenticated simulator session. This avoids Instant's per-email code-request rate limit while still exercising real token sign-in when the simulator is not already authenticated. Normal app builds retain the standard magic-code request flow.

Reuse the installed build while iterating on flows:

bun run maestro:test

Override MAESTRO_IPHONE_NAME, MAESTRO_IPHONE_DEVICE_TYPE, or MAESTRO_IOS_RUNTIME when required. The functional suite covers creating and reviewing a card, preserving tags while adding another card, editing during review, confirming unsaved-draft dismissal, and deleting during review. A separate German smoke flow verifies localized card creation.

Authentication build variants

APP_VARIANT is the only build selector:

Variant Bundle ID EAS environment Services
dev de.totap.flashcards.dev preview or development DEV_INSTANT_APP_ID and DEV_GOOGLE_IOS_CLIENT_ID
test de.totap.flashcards.test development TEST_INSTANT_APP_ID and TEST_GOOGLE_IOS_CLIENT_ID
production de.totap.flashcards production Unavailable until FLA-27 provisions isolated production services

A missing APP_VARIANT defaults to dev for local work. Unknown variants, missing values, swapped values, and production builds fail during Expo config evaluation. Public runtime values come from validated Expo extra; application code does not read an independent backend environment variable.

Both flashcards-dev and flashcards-test use the Instant auth-client names apple-ios and google-ios. The Apple Services ID is de.totap.flashcards.auth; this native Expo flow has no web domains, return URLs, private key, or Google client secret.

Architecture

The application uses top-level packages with an enforced dependency direction:

src/
├── app/             # Expo Router entry points and cross-package composition
├── auth/            # Sign-in and profile workflows
├── cards/           # Reusable card domain, persistence, queries, audio, and backup
├── editor/          # New/edit card workflows
├── library/         # Card library workflow
├── review-prep/     # Review selection and customization
├── review-session/  # Active review workflow
├── settings/        # Settings and backup orchestration
└── shared/          # Domain-neutral infrastructure and reusable UI

Files under src/app define the stable navigation tree required by Expo Router. Route groups such as (tabs) and (modals) organize navigation without adding URL segments, _layout.tsx configures navigators and providers, [id].tsx represents a dynamic parameter, and +api.ts defines a server endpoint. App entry points also own cross-package handoffs such as editor seeds, review-session seeds, and injected auth operations. Screen packages do not import one another or app modules.

Each package keeps only the directories it needs. Routes adapt navigation-independent inputs to screens, screens compose complete workflows, components contain package-owned UI, hooks orchestrate package state, model contains pure types and rules, and lib contains package-specific utilities. Cards additionally owns data, queries, audio, and backup. Shared remains domain-neutral.

The dependency direction is:

app and root i18n composition
  → auth/editor/library/review-prep/review-session/settings
  → cards
  → shared

Cross-package imports use the public cards and shared paths enforced in eslint.config.js. Native screen code cannot import cards persistence implementations, admin adapters, or server modules. Focused server paths are available only to API routes and cards server code. Use bunx expo lint to verify both ESM and CommonJS imports, including parent-relative escapes.

Tests for package-owned behavior are colocated under each package's __tests__ directory. Only multi-package composition tests belong under root __tests__/app; script tests remain under root __tests__/scripts.

For example, editing an existing card flows through:

src/app/(modals)/edit-card/[id].tsx
  → src/editor/routes/edit-card-route.tsx
  → src/editor/screens/edit-card-screen.tsx
  → editor hooks
  → the cards CardStore contract
  → the cards InstantDB implementation

API routes and preview builds

Do not use Expo's automatic build-time server deployment (EXPO_UNSTABLE_DEPLOY_SERVER). The app uses a stable EAS Hosting alias instead:

https://tobio-flashcards--preview.expo.app

Development keeps using the local Expo server/API routes because EXPO_PUBLIC_API_ORIGIN should be unset locally. Preview builds get EXPO_PUBLIC_API_ORIGIN from eas.json/EAS env, and app.config.js uses it as the Expo Router origin.

Deploy API routes for preview

Deploy API routes manually whenever server code or server environment variables change:

bunx expo export -p web --api-only
bunx eas-cli@latest deploy --environment preview --alias preview

A quick TTS route sanity check should return 401 Unauthorized for an invalid token, not Missing INSTANT_APP_ID:

curl -i -X POST \
  https://tobio-flashcards--preview.expo.app/api/tts/draft \
  -H 'Authorization: Bearer invalid-token' \
  -H 'Content-Type: application/json' \
  --data '{"html":"<p>Hello</p>","locale":"en-US"}'

Because the native app points at the stable preview alias, API route changes can be redeployed without rebuilding the app. Rebuild only when native/client code or build configuration changes.

Local iPhone preview build

Connect, unlock, and trust the iPhone. Then pull the EAS preview environment into .env.local:

bunx eas-cli@latest env:pull preview

Regenerate the native iOS project with the preview environment:

bunx expo prebuild --clean --platform ios

Finally, build the Release configuration with local Xcode signing and install it directly on the connected iPhone:

EXPO_SKIP_SERVER_EXPORT=1 bunx expo run:ios --device --configuration Release

EXPO_SKIP_SERVER_EXPORT=1 makes app.config.js use static web output so the native Release build does not try to export the API routes. The command prompts for the target device when necessary.