Skip to content

ブラウザウズプレミアム (Web サブスク) ​

ウズプレミアム (IAP product uzu_premium、月額 980 円) を web (Stripe) から加入できるようにするための技術ドキュメント。ブラウザコイン購入 (Windows) の後続フェーズ (Phase 5) にあたる。

前提

還元方式 (P-1: web 経由の月額を下げる / P-2: 付与コインを増やす) は、コイン価格差の軸 1 / 軸 2 とセットで事業側が未確定。本ドキュメントは還元方式がどちらに転んでも成立する技術設計を先に固めておくもの。

現状の実装 (IAP 直結) ​

サブスク実装にはプロバイダ抽象化が存在せず、すべて IAP (App Store / Play Store) 直結。ただし「加入中か」の判定だけはプロバイダ非依存になっており、これが web 対応の足場になる。

スキーマ ​

テーブルIAP 依存箇所
user_subscriptionsplatform が "IOS" / "ANDROID" 決め打ち。契約キーは iap_transaction_id 1 カラム (iOS = originalTransactionId / Android = purchaseToken を詰め込み)。有効期限は持たず expiration_confirmed_at (NULL = 有効) のみ。iap_transaction_id に WHERE expiration_confirmed_at IS NULL の部分ユニークインデックス
subscription_plansストアとの紐付けが iap_product_id (Unique) 一本。Stripe Price ID を持つ場所がない
iap_subscription_transaction_infos課金期ごとの付与記録 (冪等性担保)。名前は iap 固有だが構造は汎用 ((user_subscription_id, transaction_id) Unique)

プロバイダ非依存な部分 (無改修で動く) ​

以下はすべて usersubscription.UserID(userID) AND ExpirationConfirmedAtIsNil() の Exist() だけで判定しており、user_subscriptions に有効な行さえ作れば Stripe 契約でもそのまま動く:

  • User.hasActiveSubscription (GraphQL、user.resolvers.go)
  • コイン購入時のプレミアムボーナス (wallet.go の iapDepositingWithBonus)
  • ログインボーナス (subscription_login_bonus.go)

月次コイン付与 GrantSubscriptionCoins (usecase/subscription_coins.go) も、冪等キー transactionID が単なる string (課金期単位: iOS = transactionId / Android = latestSuccessfulOrderId) なので、Stripe の invoice ID を渡せばロジックごと流用できる。

IAP 直結な部分 (改修が必要) ​

  • 加入 mutation startIAPSubscription (start_subscription.gql / resolver): 入力が iapTransactionId / purchaseToken 固定。ストアを正として契約キーを解決する (UZU-5636 対策)
  • 契約状態の表示 Wallet.GetCurrentSubscription / inferPurchasePlatform (wallet.go): platform で iOS / Android のストア API に分岐。端末 platform ≠ 購入 platform なら IsDifferentPlatform だけ返す
  • 更新・解約・失効: appstore_webhook.go / playstore_webhook.go + 毎時バッチ batch_check_subscription_status (ストア API で状態照会)
  • 通知正規化層 iap_notification.go: 両ストアの通知を UnifiedNotificationType (InitialBuy / Renewed / Expired / Revoked …) に正規化し、失効確定 markSubscriptionExpired を共有。ここが唯一の再利用しやすい抽象点

設計方針 ​

P-1: Stripe Billing (Checkout mode=subscription) を使う ​

  • store アプリ (ブラウザコイン購入 の apps/store) に /premium ページを追加し、Checkout Session (mode=subscription) で加入する。決済 UI・3DS・カード更新は Stripe に任せる
  • 解約・支払い方法変更は Stripe Customer Portal に委ねる (自前の解約 UI を作らない)。store に「契約を管理」リンクを置くだけ
  • 月額・還元方式 (P-1 / P-2) は Stripe Price と subscription_plans.monthly_coin_amount の設定値であり、コード変更なしで動かせる形にする

P-2: user_subscriptions をプロバイダ汎用化する (additive) ​

sql
ALTER TABLE user_subscriptions
  ADD COLUMN provider text;  -- 'STRIPE'。NULL = 既存 IAP (platform が IOS/ANDROID)
  • 契約キーは iap_transaction_id に Stripe Subscription ID (sub_...) を入れて流用する (Apple / Google の形式と衝突しない。コインの iap_purchase_id に PaymentIntent ID を入れるのと同じ判断)
  • 部分ユニークインデックスは既存のままで成立する (契約キー空間が重ならないため)。カラム名の rename (contract_key 等) は価値に対して影響範囲が大きいのでやらない
  • subscription_plans は Stripe Price の lookup_key = iap_product_id (= uzu_premium) で対応づけ、新カラムを足さない。web 専用プラン (P-2 で付与コインを変える場合) が必要になったら、そのとき uzu_premium_web の別行を追加する

P-3: Stripe webhook を UnifiedNotificationType にマッピングする ​

コインの /webhook/stripe (Go backend) にサブスク系イベントのハンドラを追加し、既存の正規化層に流し込む:

Stripe イベントUnifiedNotificationType 相当処理
checkout.session.completed (mode=subscription)InitialBuyuser_subscriptions に行を作成 (provider = 'STRIPE')、初回付与は invoice.paid に委ねる
invoice.paidInitialBuy / RenewedGrantSubscriptionCoins(userSubscription, plan, invoiceID)。冪等キー = invoice ID
customer.subscription.deletedExpiredmarkSubscriptionExpired (既存共有ヘルパー)
charge.refunded (サブスク分)Revoked失効 + 必要なら付与済みコインの回収 (運用判断)
  • 支払い失敗のリトライ (dunning) は Stripe の Smart Retries に任せ、backend は invoice.paid / subscription.deleted の結果だけを見る
  • 毎時バッチ batch_check_subscription_status には provider = 'STRIPE' の分岐を追加し、Stripe API で Subscription status を照会する取りこぼし救済を IAP と同等に持つ

P-4: 契約表示とガード ​

  • GetCurrentSubscription に provider 分岐を追加。Stripe 契約は Stripe API から current_period_end 等を取得して SubscriptionInfo に詰める
  • 二重加入ガード: 加入フロー冒頭で hasActiveSubscription を確認し、IAP 契約が有効なユーザーには web 加入をブロックして「アプリ側の契約を解約してから」と案内する。逆方向 (Stripe 契約中に IAP 加入) はアプリ側 startIAPSubscription で同様にガードする
  • 端末をまたぐ表示は既存の IsDifferentPlatform パターンを踏襲 (Stripe 契約はどのプラットフォームから見ても「web で契約中 → 管理は web で」と表示)

P-5: アプリ側 (Windows) ​

  • isOSUzuPremiumUnavailable (purchase.dart) で Windows のプレミアム UI が全て隠れている。これを解除し、加入導線は ブラウザコイン購入 D-1 と同じ transfer フローで store の /premium を開く
  • サブスク管理 (_openManageSubscriptions) の Windows 分岐は Customer Portal への導線にする
  • Remote Config フラグ (web_premium_purchase 等) で段階公開

改修範囲一覧 ​

箇所変更
schema/user_subscription.goprovider カラム追加 (additive)
schema/subscription_plan.go変更なし (Stripe Price の lookup_key で対応づけ)
usecase/subscription_coins.go変更なし (invoice ID を渡すだけ)
usecase/wallet.go (GetCurrentSubscription / inferPurchasePlatform)provider 分岐追加。最も IAP 直結な箇所
adapters/rest/iap_notification.goStripe イベントのマッピング追加、markSubscriptionExpired 共有
/webhook/stripe (コイン対応で新設)サブスク系イベントのハンドラ追加
jobs/batch_check_subscription_statusprovider 分岐追加
GraphQLstartIAPSubscription は触らない。web 加入は store → Stripe → webhook 経路のため新 mutation 不要。契約表示系 (GetCurrentSubscription 経由) のみ影響
apps/store/premium ページ + Checkout (mode=subscription) + Customer Portal リンク
dart/apps/mobileisOSUzuPremiumUnavailable の解除 (Windows)、管理導線、Remote Config フラグ

要確認事項 ​

  • [ ] 還元方式 (P-1 / P-2): 事業側の決定待ち。P-2 (付与コイン増) の場合は web 専用の subscription_plans 行 (uzu_premium_web) を追加する
  • [ ] 二重加入の運用: IAP → web への乗り換え導線 (解約タイミング、日割りなし、期限切れまで待つ案内) の UX をどうするか
  • [ ] 既存 IAP 契約者の扱い: 移行を促すか、自然流入に任せるか (Apple の 7 日間ルールがあるためアプリ内での告知は Windows のみ)
  • [ ] 返金時のコイン回収: サブスク返金時に当月付与済みの期間限定コインを回収するか (IAP 側の現行運用に合わせる)
  • [ ] 多通貨・内税: コイン側 (ブラウザコイン購入 D-4) に合わせ JPY / KRW / TWD / HKD を内税 (tax_behavior: 'inclusive') で提供する。各通貨の月額は当社が定めた相当額を設定。過去にストア間の為替レート差で価格がズレた事故 (2026-03) があるため、web 価格と IAP 価格の並記管理は料金表側で行う
  • [ ] 資金決済法 / コンプラ: 月次付与の期間限定コインが「対価を得て発行された前払式支払手段」と整理される場合の未使用残高・供託への影響 (コイン側と共通の論点)

実装フェーズ ​

コイン側 (ブラウザコイン購入) の Phase 1〜4 が完了し、/webhook/stripe と store アプリが存在することが前提。

フェーズ内容
Premium Phase 1: スキーマ汎用化user_subscriptions.provider 追加、GetCurrentSubscription の provider 分岐。IAP の挙動を変えないことをテストで担保
Premium Phase 2: Stripe サブスク基盤webhook ハンドラ (InitialBuy / Renewed / Expired マッピング)、batch_check_subscription_status 分岐、二重加入ガード
Premium Phase 3: store /premium加入ページ + Checkout (mode=subscription) + Customer Portal 導線。ブラウザ単体で加入 → 月次付与 → 解約の E2E
Premium Phase 4: アプリ導線Windows の プレミアム UI 解除 + transfer 導線 + Remote Config フラグ。還元方式 (P-1 / P-2) 決定後にリリース