Skip to content

feat: Android Play版 Mobile基盤を実装する(WP-0〜4)#282

Open
keisato848 wants to merge 40 commits into
mainfrom
docs/android-play-design-consistency
Open

feat: Android Play版 Mobile基盤を実装する(WP-0〜4)#282
keisato848 wants to merge 40 commits into
mainfrom
docs/android-play-design-consistency

Conversation

@keisato848

Copy link
Copy Markdown
Owner

概要

Android Play版(IpaLab mobile)の基盤実装。認証・コンテンツ配信・Outbox同期・試験コア画面・学習計画APIをすべて含む。

変更スコープ

📄 設計文書(docs/)

  • 計画4文書追加(要件定義・基本設計・詳細設計・WBS)
  • 基本設計 Compact版を正式版に昇格 / 旧版を Archived 化
  • 要件定義 v1.1:U-01/U-02確定(BFFプロキシ方式)、未解決事項クローズ
  • Access Token TTL 15分統一・同期成功率 KPI 99.5% へ修正

📦 packages/shared

  • Mobile API 共有 Zod スキーマ(auth / sync / content / study-plans)
  • AgentResult<T> 共通型・エージェントエラーコード

🖥 apps/web(Next.js BFF)

エンドポイント 内容
POST /api/mobile/v1/auth/authorize OAuth PKCE bridge
GET /api/mobile/v1/auth/callback Provider callback → bridge code
POST /api/mobile/v1/auth/exchange bridge code → JWT AT/RT
POST /api/mobile/v1/auth/refresh RT rotation(reuse 検知付き)
POST /api/mobile/v1/auth/revoke セッション失効
GET /api/mobile/v1/auth/me セッション情報
POST /api/mobile/v1/auth/guest ゲストセッション発行
POST /api/mobile/v1/auth/merge ゲスト→本登録統合
GET /api/mobile/v1/bootstrap 初回起動データ
GET /api/mobile/v1/content/manifest コンテンツマニフェスト
GET /api/mobile/v1/content/exams/:id 試験問題データ
POST /api/mobile/v1/sync/batch Outbox 部分 ACK 同期
GET /api/mobile/v1/sync/changes 差分 pull
GET/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.ts
API auth-api.ts / content-api.ts
ユースケース auth.ts / sync-outbox.ts / learning-session.ts
ストア auth-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

セキュリティ制約(変更不可)

  • Provider token はモバイルへ 絶対に送らない(BFF のみ保持)
  • JWT は RS256 のみ(HS256 禁止)
  • MOBILE_JWT_PRIVATE_KEY / MOBILE_JWT_PUBLIC_KEY を NextAuth シークレットと共有しない
  • body/query の userId を認可に使わない(JWT sub が正本)
  • メール一致のみによる自動アカウントリンク禁止

テスト

apps/web: 21 スイート / 376 テスト — すべて ✅ PASS
スイート 件数
mobile/auth 9
mobile/oauth-bridge 6
mobile/sync 7
mobile/guest-merge 6
mobile/study-plans 11
middleware 29
api/routes 15
api/score 16
api/learning-records 22
api/exam-progress 15
api/ai-assistant/bug-report 6
hooks/useMonthlyProgress 21
hooks/useUserProgress 35
hooks/useGuestSync 9
components/MonthlyGoalEditor 15
lib/* 157

apps/mobile のユニットテスト(jest-expo)はローカルで npm install 後に実行してください。

E2E テストエビデンス報告書

実行日 報告書
本 PR は BFF + Mobile 基盤のため E2E は次フェーズ(OAuth App 登録後)に実施

レビュー依頼事項

  1. JWT 鍵管理MOBILE_JWT_PRIVATE_KEY の環境変数設定方法を確認
  2. OAuth App 登録(WP-1.1 Kei さん担当):Google/GitHub dev/staging/prod の redirect URI 設定
  3. Cosmos コンテナMobileSessions / MobileSyncEvents / MobileGuestMerges の手動作成要否
  4. apps/mobile npm install:ローカルで依存関係をインストールし typecheck / jest-expo を通してください

関連 WP

WP-0.1 WP-0.3 WP-1.2 WP-1.3 WP-1.4 WP-1.5 WP-2.1 WP-2.3 WP-2.4 WP-2.5 WP-3 WP-4.2

keisato848 added 27 commits May 28, 2026 22:56
- 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) を追加
@keisato848 keisato848 force-pushed the docs/android-play-design-consistency branch from 94af9b3 to 4fd24a2 Compare June 18, 2026 12:09
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'.
@github-actions

github-actions Bot commented Jun 21, 2026

Copy link
Copy Markdown

✅ Staging デプロイ完了

項目
🌐 Staging URL Stagingで確認する
🌿 ブランチ docs/android-play-design-consistency
📝 コミット c40dd9c

⚠️ 複数PRが同時進行している場合は最後にデプロイされたPRが反映されています。
確認後は本PRのレビューを進めてください。

…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.
Comment thread .github/workflows/mobile-ci.yml Fixed
…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).
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants