Skip to content

ブラウザコイン購入 (Windows) ​

スマホ新法対応で web (ブラウザ) からコインを買えるようにする件のうち、最優先ターゲットである Windows (Microsoft Store 版) の技術仕様。アプリの認証をブラウザへ引き継いだ上で、Stripe でコインを購入できるようにする。

事業側の決定事項 (Stripe 採用 / Windows 先行 / webhook は既存 backend / 価格差の 2 軸は未確定) は Notion のまとめを前提とし、本ドキュメントはその技術実装を扱う。

全体アーキテクチャ ​

登場するコンポーネントは 4 つ。決定事項「コイン購入を管理する web サーバーは既存 backend とは独立したサービス」「Stripe webhook は既存 backend 側」に沿って責務を分ける。

コンポーネント新規 / 既存責務
dart/apps/mobile (Windows)既存に導線追加transfer code を取得してブラウザを開くだけ (片道)。残高反映は既存 subscription
javascript/apps/auth既存 (変更ほぼなし)認証引き継ぎ (/api/transfer/*) と SSO。client_id store の登録のみ追加
javascript/apps/store新規商品一覧ページ + Stripe Checkout Session 作成 API。Hono + Vite React SPA
Go backend既存に追加/webhook/stripe でコイン付与・返金。consumable_products の WEB 対応

D-1: 認証引き継ぎは auth の transfer フローを使う ​

認証基盤 には「モバイル → ブラウザ引き継ぎ」専用の transfer フローが実装・ドキュメント化済みだが、呼び出し元がリポジトリのどこにも存在しない (javascript/apps/auth/src/app/api/transfer/code/route.ts / src/lib/transfer.ts)。本件がその最初の利用者になる。

  1. アプリが FirebaseAuth.currentUser.getIdToken() で ID トークンを取得
  2. POST {authBaseUrl}/api/transfer/code に {id_token, redirect_uri: store の URL} → {code} (TTL 5 分・1 回限り)
  3. launchUrl("https://auth.uzu-app.com/transfer?code=...") で外部ブラウザを開く
  4. auth が code を redeem → ブラウザに SSO セッション Cookie を作成 → store?firebaseAuthCustomToken=... へ 302
  5. store が custom token を拾って signInWithCustomToken (studio / admin と同じ受け口)

この方式を選ぶ理由:

  • 片道で完結する。 購入フローはアプリへのコールバックを必要としない (残高は subscription で戻る)。Windows で custom scheme を使うと MSIX の protocol_activation が mobile.exe を別プロセスで起動してしまう既知問題 (dart/apps/mobile/lib/adapters/auth_app_web_auth.dart のコメントに詳述) を、そもそも踏まない
  • 2 回目以降は transfer 不要。 手順 4 でブラウザに SSO セッションが張られるため、以後 store を直接開いても auth SSO で自動ログインされる
  • URL に長命クレデンシャルを載せない。 custom token を直接 URL に載せて開く案と比べ、ブラウザ履歴・プロセス引数に残るのは 5 分・1 回限りの code のみ
  • 代替の /api/link/grant (TTL 60 秒、SSO 連携用) は目的外流用になるため不採用

直リンクアクセス (アプリを経由しない導線) も v1 から対応する ​

store のエントリーポイントは (a) Windows アプリからの transfer 経由 と (b) 直リンク (SNS・公式 X 等の告知からブラウザで直接アクセス) の 2 経路を最初からサポートする。

  • 直リンクで未ログインの場合は auth の通常 SSO (/login?redirect_uri=<store URL>&client_id=store) へリダイレクトする。auth のログイン UI (Google / X / Apple / Email / Phone) がそのまま使えるため、store 側の実装は「未ログインなら auth へ飛ばす」分岐だけ (studio 等と同じ既存 web SSO フロー)
  • iOS / Android の制約は「アプリ内にリンクを置かない」ことだけで、ユーザーが自分で web に来て発生したトランザクションはプラットフォームへの報告責任がない (決定事項どおり)。告知経由の流入はスマホブラウザが主になりうるため、store はレスポンシブ必須
  • アプリを起動していないユーザーの残高は、次回アプリ起動時に反映される (残高はサーバー元帳が正であり、クライアント側に状態を持たないため追加実装は不要)

D-2: 導線ゲートは Platform.isWindows のみ (GPG on PC には出さない) ​

Google Play Games on PC 版は Windows 上で動くが実体は Google Play 配信の Android ビルド (Platform.isAndroid == true)。Play Billing がそのまま動作し、Play 配信物としてアンチステアリング規約の対象になるため、web 導線は出さず現行のアプリ内決済を維持する。

「Microsoft Store 版と GPG on PC 版で挙動が変わる」問題は、GPG on PC を対象外と仕様で固定することで解消する。QA 項目は「GPG on PC で導線が表示されないこと」。

D-3: store は Hono + Vite React SPA で新設する ​

javascript/apps/store を新設する。Next.js は使わない。ストアに SSR が必要な要素はなく (ユーザーはアプリから認証済みで着地するので SEO 不要、商品一覧もクライアントフェッチで十分)、決定事項の「SSR が不要なら重量級を使わず軽量な API サーバーとして実装してよい」に沿う。

構成は「画面は Vite + React の SPA、Hono はその静的配信 + API」の 1 コンテナ:

javascript/apps/store/
├── server/     # Hono (Node) — GKE で 1 コンテナ
│   ├── POST /api/checkout        # Firebase ID token 検証 → Checkout Session 作成 → { url } を返し SPA が遷移
│   ├── GET  /api/session-status  # success ページ用
│   ├── GET  /api/products        # consumable_products(WEB) × Stripe Price を突合
│   └── GET  /*                   # Vite ビルド成果物の静的配信
└── client/     # Vite + React SPA (商品一覧 / 決済結果)
  • サーバー側の依存は firebase-admin (ID token 検証) と stripe SDK のみ
  • hono/jsx だけで画面まで書く純 Hono 構成は採らない。 React を捨てると packages/ui・i18next・Firebase Web SDK まわりの既存パターンを全部手書きすることになり、「軽量」の狙いと逆に総コストが増える
  • 認証の受け口: @uzupj/auth は Next.js 依存のため使えない。同じ理由で薄い自前 auth を持つ前例 javascript/uzutokyo/frontend/src/lib/auth.tsx (60 行程度) をコピーする。URL クエリの firebaseAuthCustomToken を拾って signInWithCustomToken → 未ログインなら auth へリダイレクト、だけの受け口
  • packages/ui は使わない。OvalButton / SquareButton を含むボタン系が共通の型ヘルパー (internal/WithSpreadableComponentProps) 経由で next/link を import しており、パッケージ自体も next を peerDependencies に持つため、Vite SPA に持ち込めない (uzutokyo/frontend も同じ理由で不使用)。デザイントークンだけ @uzupj/tailwind-config の preset で共有し、最小限の部品を client/src/components/ui.tsx に持つ
  • i18n: i18next はフレームワーク非依存なので Vite でも既存の翻訳運用 (ja / en / ko / zh-CN / zh-TW) に乗る
  • レスポンシブ必須: 直リンク (D-1) の流入はスマホブラウザが主になりうる。デスクトップ (Windows) とモバイルブラウザの両方を v1 の対象にする
  • UI は現行アプリのコイン購入画面を参考に再実装する: デザインは Figma (プレイ外 - outside) を正とする
  • プレミアム加入状態の表示: store サーバーが backend GraphQL の me を Authorization: Bearer <Firebase ID token> (ユーザーの token を転送) で引き、SPA には /api/me で返す。User.hasActiveSubscription はプロバイダ・プラットフォーム非依存 (expiration_confirmed_at IS NULL のみ参照) なので、モバイルで加入した会員が Windows から開いても正しく判定できる。商品の subscriptionBonusRate と組み合わせ、「プレミアム会員バッジ + ボーナス率 (+X%)」を表示する — WebDepositing() が付与時に参照するのと同じ subscription_bonus_rate なので表示と付与がズレない。次回更新日等の契約詳細は出さない (GetCurrentSubscription が IAP platform 前提のため。ブラウザウズプレミアム の Phase で解消)
項目内容
ドメインstore.uzu-app.com (dev/stg は dev. / stg. prefix)。auth の redirect ホワイトリストは uzu-app.com の suffix match なので変更不要
認証uzutokyo 方式の薄い auth ラッパー。auth の clients.ts に client_id: store を登録 (表示名・規約マッピング)
デプロイGKE Ingress + ManagedCertificate (uzutokyo backend と同じ kustomize 構成)。Docker イメージは node + Hono のみで Next standalone より軽い。STRIPE_SECRET_KEY は Secret Manager → ExternalSecret。手順は store の Deploy
特商法表記web 上に既存ページあり (確認済み)。フッターからリンク

Vercel を使わない理由

決済シークレットの管理を既存クラスタの Secret 運用に揃えられること、Skew Protection や FDT といった Vercel 固有の考慮を決済ページに持ち込まないため。

D-4: Stripe Checkout (ホスト型・mode=payment)、対応通貨は JPY / KRW / TWD / HKD ​

  • ホスト型 Checkout を使う。PCI スコープ最小、3DS・多言語・Apple Pay / Google Pay / Link が自動で付く。埋め込み (Payment Element) は UI 自由度と引き換えに実装・保守が重く、v1 で必要な理由がない
  • 支払い方法は payment_method_types: ['card', 'link'] でコード側に固定する (Apple Pay / Google Pay は card に含まれる)。Dashboard 任せにすると、コンビニ払いなどの後払い型を ON にしただけで checkout.session.completed 時点で未払いの Session が生まれ、webhook (即時決済前提) がスキップして付与漏れになる。PayPay は即時決済だが JPY 専用で、含めると Session の通貨が JPY に縛られ多通貨と両立しないため v1 では外す
  • 対応通貨は JPY / KRW / TWD / HKD (日本 + 課金実績のある KR / TW / HK)。Stripe Price の currency_options に 4 通貨を設定し、Checkout が顧客の所在地から通貨を自動選択する
  • 各通貨の金額は為替の自動換算ではなく、当社が定めた国ごとの相当額を設定する。 通貨レート明文化の方針 (1 コイン = 1.4286 円基準 + 国ごとの相当額を料金表に明記) と整合させ、料金表 (Notion) と currency_options の値を一致させる。web は自社で価格を持てるため、過去に起きたストア間の為替レート差による価格ズレは構造的に起きない
  • すべての価格は内税 (tax_behavior: 'inclusive') で設定し、表示価格 = 請求額にする。Stripe Tax は利用しない — 税額の自動計算・徴収は行わず、内税価格の設定のみとし、税務処理 (消費税・海外販売分の申告) は IAP 売上と同様に経理側の既存処理で行う
  • JPY / KRW は Stripe の zero-decimal 通貨、TWD / HKD は minor units が 2 桁。Stripe Price / currency_options の金額は minor units で指定するため設定時に注意する
  • 価格の正は Stripe Price (lookup_key = product_id)、コイン数の正は consumable_products。クライアント申告値を信じない — サーバが商品マスタから付与数を解決する既存原則 (UZU-5648) を踏襲する。価格差の未確定 2 軸 (web 増量幅など) は Stripe Price の差し替え + applied_at 運用で、コード変更なしに動かせる
  • Session には client_reference_id = user_id と metadata: {user_id, product_id} を焼き込む
  • プロバイダ分離: mdms_jp backend の payment-provider (interface / factory で Stripe と fincode を切替可能) を参考に、Go 側も internal/adapters/payment/ 配下の adapter に Stripe SDK を閉じ込め、usecase からはインターフェース越しに使う。決定事項の「後から差し替えられる形」に対応

D-5: 新テーブルは作らず iap_deposits を additive 拡張する ​

残高は Wallet.accountOf() の元帳集計 (iap_deposits + admin_funds − orders の SUM) で算出されており、取引履歴・返金 (cancelled_at)・admin・BigQuery ミラーもすべて iap_deposits を読む。新テーブル案はこの全面に手が入るのに対し、拡張案は migration 1 本 + 除外フィルタ 1 箇所で済む。

sql
-- additive (NULL 許容)。既存行・稼働中 Pod に影響しない
ALTER TABLE iap_deposits
  ADD COLUMN payment_provider text;  -- 'APP_STORE' / 'PLAY_STORE' / 'STRIPE'
  • 冪等キー: iap_purchase_id = Stripe PaymentIntent ID (pi_...)。既存の UNIQUE 制約 + ON CONFLICT DO NOTHING (backend/internal/usecase/wallet.go) がそのまま webhook 再送・二重配送を吸収する。Apple / Google の ID 形式と衝突しない
  • payment_provider: 決済経路の明示列。web 購入の行には 'STRIPE' を入れる。現状のテーブルには「どのストアの購入か」を示す列がなく、batch_check_iap_refund (D-6) が全行を Apple に照会している状態のため、この列で照会対象の限定と経路別の集計を行う。外部 ID の形式 (pi_ プレフィックス等) からの推測に依存しないための明示列
  • この migration と既存行の backfill を全作業の最初に実施する。 既存行は iap_purchase_id の形式 (Play の orderId は GPA. 形式、Apple は数字列) から判別して 'APP_STORE' / 'PLAY_STORE' を埋める一回きりのデータ migration を行い、NULL 行を残さない。以降の IAP 付与経路 (IapDepositing / RTDN) でも書き込むようにして、新規行にも常に値が入る状態にする
  • usecase: Wallet.iapDepositingWithBonus() と同型の WebDepositing() を追加。増量分を期間限定コインで配る案 (軸 3 案 B) は、サブスクボーナスが既にやっている「同一 tx で iap_deposits + limited_coin_deposits を INSERT」と完全に同じ形なので、価格差の意思決定がどう転んでも吸収できる。Redis pubsub の残高 publish も既存経路に乗る
  • 商品マスタ: consumable_products.store enum と GraphQL enum Store に WEB を追加 (additive)。applied_at による価格改定履歴・admin 運用を再利用。Ent スキーマ変更 3 点セット (go generate ./internal/ent / collect_path.yml / tbls.yml) を忘れない

D-6: webhook は Go backend に POST /webhook/stripe (署名検証を初日から) ​

/webhook/appstore / /webhook/playstore と並べて backend/internal/servers/graph/main.go に登録する。

イベント処理
checkout.session.completedpayment_status == paid のとき、metadata の user / product から consumable_products(WEB) でコイン数を解決し WebDepositing()。冪等キーは PaymentIntent ID
charge.refundedcancelled_at を打つ論理削除 (App Store refund の cancelIapDeposit と同型。残高不足時スキップ + Slack 通知も流用)
charge.dispute.created付与は触らず Slack 通知のみ (運用判断)
  • 署名検証は stripe-go の webhook.ConstructEvent。Play RTDN が検証なしのまま残っている轍を踏まず、初日から入れる
  • 処理失敗時は 5xx を返して Stripe の自動リトライに任せる (Voided 通知で実績のあるパターン)。イベントの順序保証はないため、イベント内容ではなく PaymentIntent を Stripe API から取り直して現在状態に同期する (mdms_jp の syncPayment 方式)
  • 成功ページの UX: success_url に戻った store は Session status を表示し「残高への反映まで数秒かかる」旨を出す。webhook 遅延対策として、認証付きで「Session を取得して webhook と同じ冪等経路で付与する」同期 API を後続で足せる (v1 では必須にしない)
  • batch_check_iap_refund (Apple への返金照会バッチ) は照会対象を payment_provider = 'APP_STORE' に限定する。web 行の除外に加え、これまで Play 行を Apple に照会して空振りしていた無駄も解消される

D-7: アプリ側は「Web で購入」ボタン 1 つ + Remote Config フラグ ​

  • dart/apps/mobile/lib/views/pages/account/coin/account_coin_screen.dart の Platform.isWindows 分岐 (現在は「この端末ではコインが購入できません」の固定文言) を、「ブラウザで購入する」ボタンへ。タップで D-1 の transfer フローを実行して外部ブラウザを開く
  • v1 ではアプリ内に価格・商品一覧を出さない。 ボタン 1 つに留めれば円価格の i18n・為替・価格改定への追随をアプリリリースから切り離せる。商品はすべて web 側で見せる
  • フィーチャーフラグ: lib/helpers/feature_flags.dart のパターンで web_coin_purchase (Firebase Remote Config、env 別)。段階公開と即時ロールバックのため
  • 残高反映: 既存の currentUserCoinBalance GraphQL subscription (WebSocket push) がそのまま反映する。WebDepositing() が Wallet 経由で publish するので追加実装は不要。Windows でこの subscription が購読されているかは要確認 (後述) — 仮に外れていた場合はウィンドウフォーカス時の再フェッチをフォールバックにする
  • transfer code の取得失敗 (オフライン等) はボタン上でエラー表示。code 発行後にブラウザが開けなかった場合も code は 5 分で自然失効するため後始末不要

ウズプレミアムの扱い ​

web でのウズプレミアム加入は v1 (本ドキュメント) のスコープ外。 詳細な現状分析と設計方針は ブラウザウズプレミアム を参照。

  • 既加入者への特典は v1 のままで動く。 バックエンドの「プレミアム加入中か」の判定はすべて user_subscriptions.expiration_confirmed_at IS NULL を見るだけでプロバイダ非依存。WebDepositing() を iapDepositingWithBonus() と同型で作れば、モバイルで加入したプレミアム会員が web でコインを買ってもボーナス (+3〜15%) が乗る。WEB 商品の subscription_bonus_rate をいくつにするか (web 増量との重ね掛け) は事業判断が必要
  • web での加入 (Stripe サブスク決済) は今後のフェーズで対応する。 サブスク実装はプロバイダ抽象化がなく IAP 直結 (user_subscriptions.platform が IOS/ANDROID 決め打ち、契約キーは iap_transaction_id、加入 mutation・webhook・毎時バッチもストア API 前提) のため、コイン単発購入とは別の改修範囲になる。還元方式 (P-1 価格下げ / P-2 付与コイン増) が価格差の軸 1 / 軸 2 とセットで未確定であることとも整合するため、コイン先行とする

要確認事項 ​

  • [ ] transfer フローの実証: /api/transfer/* は未使用のため、dev で E2E (code 発行 → redeem → SSO Cookie → store ログイン) を最初に検証する
  • [x] redirect ホワイトリスト: javascript/apps/auth/src/lib/redirect.ts の matchesAllowedHost は suffix match (hostname.endsWith('.uzu-app.com')) なので store.uzu-app.com は変更なしで通る。dev は dev. prefix 必須のため dev.store.uzu-app.com。localhost は任意ポートが全環境で許可されている
  • [ ] Windows での残高 subscription: current_user_provider.dart は Windows で IAP listen 等をスキップしている。_listenUserCoinBalance() がスキップ対象に入っていないことを確認
  • [x] packages/ui の依存確認: ボタン系を含め Next.js 依存のため使えない (D-3 参照)。store はローカルの部品で実装した
  • [ ] Stripe アカウント: mdms_jp の既存アカウントと分けるか。精算・レポート・webhook secret の分離観点では新規 (または同一アカウント内の別 webhook endpoint) を推奨
  • [ ] 資金決済法: web 販売分 (特に増量の期間限定コイン) の前払式支払手段としての整理・未使用残高への影響はコンプラ側の既存論点。決済金額・通貨の一次データは Stripe 側で参照できる前提で連携
  • [ ] QA: Microsoft Store 版で購入 E2E、GPG on PC 版で導線が出ないこと、直リンク (未ログインブラウザ・スマホブラウザ含む) からのログイン → 購入 E2E の 3 系統を確認

store API と backend の契約 ​

store サーバー (javascript/apps/store/server) は状態を持たず、backend と Stripe の間をつなぐだけ。

項目正store サーバーでの扱い
コイン数・ボーナス率backend currentConsumableProducts(store: WEB)そのまま一覧に載せる
価格Stripe Price (lookup_key = product_id、currency_options)product_id で突合。どちらか片方にしか無い商品は一覧から落とし warn を出す
購入ユーザーbackend me (Firebase ID token を転送して解決)users.id を client_reference_id と metadata.user_id に焼き込む
残高・プレミアム加入状態backend me (同上)/api/me で SPA に返す
付与backend /webhook/stripestore は付与に関与しない
  • POST /api/checkout は body に userId を明示させ、token から解決した users.id と一致しなければ 403 にする。タブを開いたままアカウントを切り替えた場合に、画面に出ているのと別のアカウントへコインが入るのを防ぐ
  • 価格 ID はクライアントから受け取らず、サーバーが product_id から引き直す
  • GET /api/session-status は Session の metadata.user_id が呼び出しユーザーと一致する場合だけ結果を返す (session_id は URL に載るため)
  • success / cancel の戻り先 origin は STORE_PUBLIC_URL で固定し、リクエストの Origin / Host ヘッダは使わない
  • SPA は backend を直接呼ばず、store サーバーの /api/* だけを使う。backend との通信方式 (今は GraphQL) を変えても SPA に影響せず、SPA の build に backend の URL を埋め込む必要もない。決済完了画面では webhook の付与を待って /api/me を数回取り直す
  • store サーバーから backend へのクエリは GraphQL のコード生成 (javascript/apps/store/codegen.ts、pnpm run gen) で型を付ける。backend のスキーマと食い違うクエリは生成時に失敗する

ローカル開発 ​

auth はローカルで立てず共有の https://dev.auth.uzu-app.com を使う (localhost への redirect は許可済み)。

Stripe は dev と同じサンドボックスを使う。鍵は手元に置かず、各 task が uzu-dev の Secret Manager (store-stripe-secret-key / backend-stripe-secret-key) から引く。前提は gcloud auth login だけ。

store だけ起動する (既定) ​

bash
# store (server + client)。zellij が無ければ store:server / store:client を別々に起動
task store:dev
  • ブラウザで http://localhost:40014 を開く (ポートは .ports.local の STORE_PORT)
  • backend は dev (https://dev.graph.backend.app.uzu.one/graphql) を向く。商品・ユーザー・残高は dev のもので、 購入すると dev backend の /webhook/stripe が dev の自分のアカウントにコインを付与する
  • 別の backend に向けるときは BACKEND_GRAPHQL_URL を export してから起動する

ローカル backend で付与まで通す ​

backend の webhook 処理を手元で動かすときは、backend と Stripe CLI も起動する。

bash
# 1. backend (Stripe の鍵と Stripe CLI の署名 secret は起動時に自動で読み込まれる)
task backend:graph

# 2. Stripe のイベントをローカル backend へ転送
task store:stripe-listen

# 3. store (server + client)。backend はローカルを向く
task store:dev-backend
  • 商品は ローカル DB の consumable_products (store = WEB) と Stripe サンドボックスの Price (lookup_key = product_id) の両方に要る。 Price は dev と共有なので、ローカル DB には dev と同じ product_id (depositing_web_v1_dummy_dev) の行を入れる
  • dev SSO でログインした Firebase UID の users 行がローカル DB に無いと me が null になり「アカウントが見つかりません」になる。 ローカル DB の dev の自分と同じ users.id の行 の firebase_uid を自分の UID に書き換えて使う
  • サンドボックスを dev と共有しているので、ローカルの購入イベントは dev の backend にも届き、dev の同じ users.id にもコインが付く。 dev に無い users.id / product_id で購入すると dev 側が 5xx を返し、ERROR ログと Stripe の再送が続く
  • task store:stripe-listen を動かしている間は、task store:dev (dev 向き) での購入や dev 環境での購入のイベントも ローカル backend に届く。ローカル DB に無いユーザー・商品だとローカル側が 5xx を返す
  • backend の起動時に ⚠️ STRIPE_SECRET_KEY ... / ⚠️ STRIPE_WEBHOOK_SECRET ... が出ていたら鍵を読めていない。そのままだと webhook が 400 になり付与されない。 Stripe CLI が未導入だと警告なしで署名 secret が空になるので、導入後に backend を起動し直す
  • 別のサンドボックスで試すときは STRIPE_SECRET_KEY を export してから各 task を起動する (Secret Manager より優先される)
  • 決済は Stripe のテストカード (4242 4242 4242 4242) で通す

請求通貨を固定して試す (dev / stg / ローカル) ​

Checkout の通貨は顧客の所在地で決まるので、そのままでは日本から JPY 以外を試せない。dev / stg / ローカルでは ?currency= で一覧の表記と請求通貨を固定できる (?locale= と併用可)。

https://stg.store.uzu-app.com/?currency=hkd
https://stg.store.uzu-app.com/?locale=ko&currency=krw
  • 指定した通貨が Price の currency_options に無い商品は、指定なしと同じ扱い (表記は言語から、請求は所在地から) になる
  • locale と違って保存しない。パラメータを外して開き直せば所在地による自動選択に戻る
  • 有効なのは APP_ENV が prd 以外のときだけ (server/env.ts の allowCurrencyOverride)。 prd では currency を無視する。利用者が国ごとの価格の安い方を選べてしまうため
  • Session 作成時に currency を渡して所在地の判定を飛ばすので、自動選択そのものの確認にはならない。そちらは VPN で確かめる

実装フェーズ ​

各フェーズは独立してデプロイ可能。破壊的変更はなく、すべて additive。

フェーズ内容
Phase 1: backend 基盤最初に payment_provider 追加 migration + 既存行の APP_STORE / PLAY_STORE backfill (D-5)。その後 Store.WEB 追加、WebDepositing()、/webhook/stripe、payment adapter。Stripe test mode + CLI で単体検証
Phase 2: store アプリapps/store 新設 (auth 統合 → 商品一覧 → Checkout)。dev で transfer フロー実証もここで実施。直リンクアクセス (未ログイン → SSO ログイン) を含め、ブラウザ単体で購入 E2E が通る状態にする
Phase 3: アプリ導線Windows の「Web で購入」ボタン + transfer 呼び出し + Remote Config フラグ。Microsoft Store 版 / GPG on PC 版の両方で検証
Phase 4: リリースstg → prd。フラグを段階的に開き、webhook 失敗率・付与遅延・Slack 通知を監視。価格差 (軸 1 / 軸 2) の決定は Stripe Price の設定だけで反映できる
Phase 5: ウズプレミアム web 加入Stripe Billing でウズプレミアムに web から加入できるようにする。サブスク基盤のプロバイダ汎用化を含む。設計は ブラウザウズプレミアム 参照。還元方式 (P-1 / P-2) の決定後に着手