feat: Android Play版 Mobile基盤を実装する(WP-0〜4)#282
Open
keisato848 wants to merge 40 commits into
Open
Conversation
- packages/shared/src/mobile/ に共通エラー・auth・sync・contentのDTOを追加 - 同期バッチ上限50件・部分ACK5状態・PKCE・固定mergeIdを契約として固定 - apps/web側に契約テスト11件を追加(vitest合格) refs: 26_AndroidPlayDetailedDesign.md §6 / WBS WP-2.1
- Expo SDK 52 + expo-router、package名 com.shikakuno.app - eas.json: development/preview/production の3環境分離 - 詳細設計§2準拠のroute骨格((auth)/(tabs) 5タブ) refs: WBS WP-0.1/0.2
- 12テーブル: app_users/exams/questions/learning_sessions/learning_events/ study_plans/outbox_events/sync_cursors/sync_conflicts/guest_merges/ content_staging/schema_metadata - UNIQUE(exam_id,q_no)、outbox state/next_attempt_at索引、Token非保存 refs: WBS WP-2.2
- GET /api/mobile/v1/bootstrap: contentVersion・cursor・flags・serverTime
- GET /api/mobile/v1/content/manifest: ETag/If-None-Match 304対応、0件試験除外
- GET /api/mobile/v1/content/exams/{examId}: 0件防壁(404)・contentHash付与
- 共通エラー {code,message,retryable,correlationId} と相関IDヘッダー伝搬
- 応答は共有DTOスキーマでparseして契約を保証、テスト6件
refs: 26_AndroidPlayDetailedDesign.md §6 / WBS WP-2.3
- jest-expo preset、RNTL前提のtransformIgnorePatterns - ESLint: any禁止・未使用変数エラー - domain/policies/backoff: 2-60秒系列+jitter、8回失敗で保留(削除しない) - 純粋ロジックは標準tscで型検証・nodeで動作確認済み(jest実行はnpm install後) refs: WBS WP-0.3 / 26_AndroidPlayDetailedDesign.md §8
…Merges)を登録する - CONTAINER_PARTITION_KEYS へ PK /userId で追加 - DB設計書 §2 へコンテナ表とセッション解決方式を追記(設計書同期) refs: 26_AndroidPlayDetailedDesign.md §13 / WBS WP-2.7(先行)
- jwt: RS256、TTL15分、claims iss/aud/sub/sid/jti/role/auth_type
- session-store: {sessionId}.{secret}形式、SHA-256保存、絶対30日/無操作14日、
rotation+使用済みhash照合、reuse検知でtoken family全失効
- auth-guard: Bearer検証+セッション失効確認(JWT subを正本とする)
- .env.templateへMOBILE_JWT鍵を追加(NextAuth secretと非共有)
refs: 26_AndroidPlayDetailedDesign.md §5.2 / WBS WP-1.3
- POST auth/guest: credential+初回トークン発行(平文secret非保存) - POST auth/refresh: rotation、reuse検知時はTOKEN_REUSE_DETECTEDで401 - POST auth/revoke: ログアウト時のサーバートークン失効(204) - GET auth/me: セッション確認 - 共有DTO: guest応答へtokensを追加、テスト9件(reuse→family失効を含む) refs: 26_AndroidPlayDetailedDesign.md §5-6 / WBS WP-1.3
- POST sync/batch: eventId冪等(Cosmos createの409で判定)、イベント単位の applied/duplicate/rejected/retryable_error、最大50件、未来時刻rejected - GET sync/changes: _ts cursorによる自ユーザー差分pull - 認可はJWT subを正本としpayloadのuserIdを使わない(横取り防止) - テスト7件: 冪等・部分失敗の非波及・51件拒否・他ユーザー分離を検証 refs: 26_AndroidPlayDetailedDesign.md §6/§8 / WBS WP-2.5
- outbox-state: pending/in_flight/acknowledged/conflict/dead_letter/retry_wait、 applied・duplicate→ACK、rejected→dead_letter、retryable→バックオフ、 8回失敗で保留(削除しない)、acknowledged7日保持 - sync-outbox: lease→最大50件送信→部分ACK反映。バッチ全体失敗・応答漏れは 送達不明としてeventId維持で再試行。依存はポート注入 - jestテスト同梱(実行はnpm install後)。標準tsc+nodeで5シナリオ検証済み refs: 26_AndroidPlayDetailedDesign.md §8 / WBS WP-2.4
- authorize: PKCE付き認証トランザクション開始、server stateハッシュ保存 - callback: provider code交換はBFF内で完結(tokenを端末へ返さない)、 (provider, providerAccountId)で解決、メール一致の自動リンク禁止(§14)、 一回限りbridge code発行→allowlist固定のapp schemeへ302 - exchange: bridge code単回消費+PKCE S256検証(不一致はトランザクション失効) →Mobile session発行 - Google/GitHub環境別OAuth App用のenvを.env.templateへ追加 - テスト6件: E2Eフロー成立、再ログイン同一userId、state改ざん、 PKCE不一致、bridge code再利用拒否 refs: 26_AndroidPlayDetailedDesign.md §5.1 / WBS WP-1.2
- guest credentialのsha256照合で所有を証明(OAuthセッション必須) - 固定mergeIdで冪等。再実行はalready_mergedで件数を再現 - 統合済みguestの別アカウント統合は409 rejected(横取り拒否) - 同期イベントをPK移管(create 409は移管済み扱いでクラッシュ再開に耐える) - 統合後はゲストセッション全失効・credentialをmerged化 - テスト6件: 移管・冪等・横取り・credential不一致・guest403・統合後pull refs: 26_AndroidPlayDetailedDesign.md §5.3 / WBS WP-1.5
WP-4.2: /api/mobile/v1/study-plans エンドポイントを追加。 - GET /api/mobile/v1/study-plans — ユーザー全計画一覧 - GET /api/mobile/v1/study-plans/:id — 1件取得(未存在は404) - PUT /api/mobile/v1/study-plans/:id — 楽観ロック付き更新 - version 一致 → 保存・version+1 を返す - version 不一致 → 409 VERSION_CONFLICT + current 返却 packages/shared/src/mobile/study-plans.ts: mobileStudyPlanSchema(version フィールド付き) studyPlansListResponseSchema / studyPlanUpdateRequestSchema apps/web/lib/mobile/study-plans.ts: mobilePlanStore — Cosmos StudyPlan コンテナ流用、version付き upsert テスト: __tests__/api/mobile/study-plans.test.ts 11件合格
クライアント側認証基盤を全実装。 src/constants/env.ts: API_BASE_URL(dev/beta/prod)・APP_SCHEME・OAUTH_REDIRECT_URI src/infrastructure/auth/pkce.ts: generatePkce()(S256)・generateState()(CSRF) Array.from で downlevelIteration 不要化 src/infrastructure/auth/token-store.ts: saveRefreshToken/loadRefreshToken/clearRefreshToken(SecureStore) saveGuestCredential/loadGuestCredential/clearGuestCredential AT は保存しない(メモリのみ) src/infrastructure/api/api-client.ts: setAccessToken/clearAccessToken(モジュール変数) single-flight refresh(setRefreshFn で DI) 401 → refresh → 1回リトライ、失敗 → onUnauthorized コールバック src/infrastructure/api/auth-api.ts: authorize/exchange/refresh/revoke/me/guest の型付きラッパー src/infrastructure/auth/oauth-flow.ts: startOAuthFlow() — PKCE→authorize→openAuthSessionAsync→exchange state 検証(CSRF)、Provider token は端末へ返さない src/store/auth-store.ts: Zustand v5 create<AuthState>() status: initializing|unauthenticated|authenticated src/application/usecases/auth.ts: bootstrapAuth()・restoreSession()・loginWithOAuth() loginAsGuest()・logout() app/_layout.tsx: bootstrapAuth()+restoreSession()、status でルーティング app/(auth)/login.tsx: Google/GitHub/ゲスト ボタン UI(48dp A11y) app/(auth)/oauth-result.tsx: コールバック受け取り画面 app/(tabs)/settings.tsx: ログアウト確認ダイアログ付きボタン
- infrastructure/db/client.ts: expo-sqlite + Drizzle ORM 遅延初期化クライアント - store/exam-session-store.ts: 進行中セッション・回答キャッシュ(Zustand) - usecases/learning-session.ts: セッション作成・回答Outbox保存・完了記録 - app/exam/[examId]/question/[qNo].tsx: 4択問題画面(即時採点・解説・オフライン保存) - app/exam/[examId]/result.tsx: 試験結果画面(正答率・問題別○×) - app/(tabs)/history.tsx: 学習履歴タブ(SQLite一覧・再開ボタン) 設計制約: - payloadJson にメール・自由記述は含めない - ownerId は JWT sub のみ(body/query 由来禁止) - SQLite 失敗時も UI は継続(オフライン対応)
- exams タブにマニフェスト取得・試験一覧表示を実装 - exam/[examId]/index に試験入口画面(開始ボタン・概要表示)を実装 - content-api に manifest/exam content 取得クライアントを追加 - TanStack Query キー規約 (query-keys.ts) を追加
94af9b3 to
4fd24a2
Compare
Root cause of the stuck CI: the apps/mobile (Expo/React Native) workspace deps were never written to the root package-lock.json, so 'npm ci' aborted at install (EUSAGE: Missing @ipa-lab/mobile, expo, react-native, drizzle, zustand, ...). - merged current main (Node 22 + security lockfile + dashboard fix) into the branch - regenerated package-lock.json so it is in sync (npm ci --dry-run passes) - react/react-dom stay pinned at 18.3.1 (no duplicate); next 16.2.6 retained Does not touch main; updates PR #282 only.
@ipa-lab/shared re-exports mobile schemas under the 'Mobile' namespace
(export * as Mobile). study-plans.ts was the only file importing the type as a
top-level named export, breaking 'next build' type-check. Aligns with the other
mobile BFF files which all use 'import { Mobile } from @ipa-lab/shared'.
✅ Staging デプロイ完了
|
…le WBS approvals Adds 27_MobileDesignBaseline.md freezing the design and recording the 4 decisions made on 2026-06-21: - Dashboard = mobile-first redesign (not a 1:1 web port; avoids known web UX issues) - Closed beta scope = AM + PM (ships the flagship AI essay scoring from day one) - Error monitoring = Sentry (PII-scrubbed) + Play Data Safety 'diagnostics' declaration - Android applicationId = com.shikakuno.app Reconciles the WBS 'approval' checklist: Access Token 15m / sync batch 50 / the 3 new Cosmos containers (/userId) were already decided in the detailed design; marked resolved with pointers. Remaining items are operational setup only (OAuth apps, Cosmos provisioning, Play Console / Sentry).
The apps/mobile (Expo/RN) workspace had type-check/lint/test scripts and unit tests but was never wired into CI — which is why the lockfile desync and a type error went undetected. This job runs on PRs touching apps/mobile or packages/shared so mobile regressions are caught.
…CI gate)
The AM practice screen referenced fields that do not exist on the current
content schema — hidden because tsc never ran in CI:
- q.questionNumber -> q.qNo (question lookup was always failing)
- question.correctAnswerIndex -> derived from question.correctAnswer ('a'-'d'
letter mapped to choice index 0-3); grading was always wrong (correctIdx=-1)
Also types the exam-content query (Mobile.ExamContentResponse) and adds
@types/jest so test-file globals (describe/it/expect) type-check.
- oauth-flow: openAuthSessionAsync lives in expo-web-browser (already imported), not expo-auth-session (SDK 52). Removes the now-unused AuthSession import. - sync-outbox.test: optional-chain transitions[0] (noUncheckedIndexedAccess).
…slint config deps added
Comment on lines
+22
to
+51
| runs-on: ubuntu-latest | ||
| name: Mobile type-check / lint / test | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v6 | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: ${{ env.NODE_VERSION }} | ||
| cache: 'npm' | ||
|
|
||
| - name: Install dependencies | ||
| run: npm ci | ||
|
|
||
| - name: Build shared package (@ipa-lab/shared types live in dist/) | ||
| run: npx turbo run build --filter=@ipa-lab/shared | ||
|
|
||
| - name: Type-check (tsc --noEmit) | ||
| run: npm run --workspace @ipa-lab/mobile type-check | ||
|
|
||
| - name: Unit tests (Jest) | ||
| run: npm run --workspace @ipa-lab/mobile test | ||
|
|
||
| # Non-blocking: apps/mobile's .eslintrc extends expo/prettier/@typescript-eslint | ||
| # but those config/plugin packages are not yet in its devDependencies (eslint | ||
| # exits 2 = config error). Tracked as a follow-up; flip to blocking once added. | ||
| - name: Lint (non-blocking — eslint config deps pending) | ||
| run: npm run --workspace @ipa-lab/mobile lint | ||
| continue-on-error: true |
Adds the learning-plan vertical slice on the verified mobile foundation: - plan-api: GET /api/mobile/v1/study-plans - study-plan usecase: online fetch -> SQLite cache; offline falls back to cache - plan-selectors: pure helpers (active plan, days-until-exam, today's task) + tests - plan.tsx: replaces the stub with a lean screen leading with 'today's task', plus monthly goals and weekly schedule; loading/error/empty/offline states Read-only display this increment; create/edit/complete mutations are next. Validated by the Mobile CI gate (type-check + jest).
- study-plan.test: loadStudyPlans network->cache->offline branches (api/db mocked) - plan.test.tsx: RNTL component test for empty/data/error states (usecase+store mocked) - adds @testing-library/react-native devDependency CI-validatable without an emulator; complements (not replaces) real-device QA.
…act 18) @testing-library/react-native pulled react-test-renderer@19 (needs React 19), conflicting with the pinned React 18.3.1. Pin react-test-renderer to 18.3.1.
Root cause of the Kotlin/Compose build failure: RN 0.76.5 ships Kotlin 1.9.24 while Expo SDK52's prebuild template and expo-modules-core expect 1.9.25 (Compose Compiler 1.5.15). RN 0.76.9 (the SDK52-recommended version) bundles Kotlin 1.9.25, resolving the mismatch at its source -- no expo-build-properties override needed. Verified by a successful native build + install on a physical Pixel 9a. - react-native 0.76.5 -> 0.76.9, expo-sqlite -> ~15.1.4 (SDK52-aligned) - removed the temporary expo-build-properties kotlinVersion workaround
The app was scaffolded from daidoko and inherited its visual identity. Per the intended split (architecture borrows from daidoko, UI/UX follows shikakuno), corrected all branding: - add src/constants/theme.ts with shikakuno web color tokens (blue accent #0070F3, dark #0F1117/#1A202C) as the single source of truth - login: replace '台所' (daidoko kitchen logo) with 'シカクノ' + blue theme - replace daidoko gold/brown palette with shikakuno tokens across all screens - SQLite db name daidoko.db -> shikakuno.db - document the UI=shikakuno / architecture=daidoko principle in the design baseline
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概要
Android Play版(IpaLab mobile)の基盤実装。認証・コンテンツ配信・Outbox同期・試験コア画面・学習計画APIをすべて含む。
変更スコープ
📄 設計文書(docs/)
📦 packages/shared
AgentResult<T>共通型・エージェントエラーコード🖥 apps/web(Next.js BFF)
POST /api/mobile/v1/auth/authorizeGET /api/mobile/v1/auth/callbackPOST /api/mobile/v1/auth/exchangePOST /api/mobile/v1/auth/refreshPOST /api/mobile/v1/auth/revokeGET /api/mobile/v1/auth/mePOST /api/mobile/v1/auth/guestPOST /api/mobile/v1/auth/mergeGET /api/mobile/v1/bootstrapGET /api/mobile/v1/content/manifestGET /api/mobile/v1/content/exams/:idPOST /api/mobile/v1/sync/batchGET /api/mobile/v1/sync/changesGET/PUT /api/mobile/v1/study-plans/:id📱 apps/mobile(Expo SDK 52)
db/client.ts/db/schema.ts/api/api-client.ts/auth/token-store.ts/auth/pkce.ts/auth/oauth-flow.tsauth-api.ts/content-api.tsauth.ts/sync-outbox.ts/learning-session.tsauth-store.ts/exam-session-store.ts_layout.tsx/(auth)/login.tsx/(auth)/oauth-result.tsx/(tabs)/exams.tsx/(tabs)/history.tsx/(tabs)/settings.tsx/exam/[examId]/index.tsx/exam/[examId]/question/[qNo].tsx/exam/[examId]/result.tsxセキュリティ制約(変更不可)
MOBILE_JWT_PRIVATE_KEY/MOBILE_JWT_PUBLIC_KEYを NextAuth シークレットと共有しないbody/queryの userId を認可に使わない(JWTsubが正本)テスト
E2E テストエビデンス報告書
レビュー依頼事項
MOBILE_JWT_PRIVATE_KEYの環境変数設定方法を確認MobileSessions/MobileSyncEvents/MobileGuestMergesの手動作成要否npm install:ローカルで依存関係をインストールし typecheck / jest-expo を通してください関連 WP
WP-0.1WP-0.3WP-1.2WP-1.3WP-1.4WP-1.5WP-2.1WP-2.3WP-2.4WP-2.5WP-3WP-4.2