Flow: Subscription (구매 / 복원 / 업그레이드)
구독 결제는 앱(StoreKit/Play) ↔ 백엔드 verify ↔ 트랜잭션 finish 의 비동기 다단계. 엔타이틀먼트 진실은 서버(premium_tier+premium_until). 화면: screen-paywall(시트)·screen-settings(복원), 도메인: domain-subscription, API: api/subscription.
시나리오
"페이월에서 PRO/MAX 를 고르고 결제하면 서버가 검증해 엔타이틀먼트를 부여하고, 닫으면 하던 작업으로 돌아간다. 재설치/기기변경 시 더보기 > 구매 복원으로 되살린다."
구매 (Purchase)
mermaid
sequenceDiagram
participant U as 사용자
participant App as 앱(in_app_purchase)
participant Store as App Store / Play
participant BE as 백엔드
U->>App: 페이월에서 플랜(PRO/MAX) 선택 → "구독" 탭
App->>App: 비로그인이면 requireAuth(subscribe) → 로그인
App->>App: subscribe_attempt(plan)
App->>Store: purchase(product_id)
Store-->>U: 시스템 결제 시트(Face ID/비번)
alt 취소
Store-->>App: cancelled → subscribe_result(cancelled), S1 유지
else 보류(Ask to Buy / SCA deferred)
Store-->>App: pending → "처리 중" 상태, 리스너 대기
else 결제 성공
Store-->>App: 거래 토큰(JWS / purchaseToken)
App->>BE: POST /api/v1/subscription/verify {platform, token, product_id}
BE->>Store: 서버 API 검증(Get Subscription Status)
BE-->>App: 200 {active, tier, premium_until}
alt 활성
App->>Store: 트랜잭션 finish/acknowledge
App->>App: subscribe_result(success) → 시트 닫고 진입화면 갱신(quota/capacity)
else 검증 실패(위조/미존재)
BE-->>App: 402/409 → subscribe_result(error), 토스트, 권한 X
end
end- 트랜잭션 finish/acknowledge 필수 — 서버가 권한 부여 후 StoreKit2
finish()/ Playacknowledge(). Play 는 3일 내 미acknowledge 시 자동 환불되므로 verify 성공 직후 처리. - pending(보류): 결제 시트 닫혀도 미완.
in_app_purchase트랜잭션 리스너가 추후 완료 이벤트 수신 → verify. 그 사이 UI 는 무료 상태 유지.
복원 (Restore)
mermaid
sequenceDiagram
participant U as 사용자
participant App as 앱
participant Store as App Store / Play
participant BE as 백엔드
U->>App: 더보기 > "구매 복원" 탭
App->>App: 비로그인이면 requireAuth(subscribe) → 로그인
App->>App: restore_attempt
App->>Store: restorePurchases / queryPurchases
Store-->>App: 과거 거래(활성 구독 있으면 토큰)
alt 활성 구독 존재
App->>BE: POST /api/v1/subscription/verify {platform, token, product_id}
BE-->>App: 200 {active:true, tier, premium_until}
App->>App: restore_result(restored) → 토스트 "구독이 복원되었어요" + UI 갱신
else 없음
App->>App: restore_result(none) → 토스트 "복원할 구독이 없어요"
end- 복원 verify 요청도 구매와 동일 필드(
platform/token/product_id, 누락 시 400) —product_id는 교차검증용, 티어는 스토어 검증 응답으로 결정(api/subscription). - 복원은 사용 빈도 낮음 — verify-on-launch 가 평소 자동 동기화(앱 실행 시 1회 재검증)하므로, 명시적 복원은 그 보정/Apple 심사 요건용. 위치를 페이월 아닌 설정에 둔 이유. (screen-settings)
업/다운그레이드 (PRO ↔ MAX)
- 같은 구독 그룹(App Store) / base plan(Play)이라 스토어가 proration 처리. 페이월 S2 에서 PRO 구독자가 "MAX 로 업그레이드" 탭 → 스토어 변경 시트 → verify 가
premium_tier갱신. - 다운그레이드(MAX→PRO)·만료·환불 →
premium_tier하락 → domain-subscription 다운그레이드 규칙(관심종목 새 상한 초과분 7일 후 자동 정리, 주간 한도는 다음 주 reset 부터).
분기 / Edge case
| 상황 | 처리 |
|---|---|
| 결제 취소 | subscribe_result(cancelled), S1 유지 |
| 보류(Ask to Buy/SCA) | "처리 중" → 트랜잭션 리스너가 완료 시 verify |
| verify 실패(위조) | 토스트 + 권한 X, 트랜잭션은 미finish(재시도 여지) |
| 이미 활성 구독 보유 후 구매 시도 | 스토어가 기존 거래 반환 → verify → S2 |
| 네트워크 끊김(결제 후 verify 전) | 트랜잭션 리스너 + verify-on-launch 가 다음 기회에 보정 |
| 비로그인 | 구매/복원 모두 auth gate 먼저(엔타이틀먼트=userID) |
Analytics
domain-analytics: subscribe_attempt/subscribe_result(plan,outcome) — 페이월, restore_attempt/restore_result(outcome) — 설정.
관련 docs
- domain-subscription — 엔타이틀먼트/검증/다운그레이드 룰
- api/subscription —
verify+ 웹훅 - screen-paywall / screen-settings
- flow-auth-gate — 구매/복원 전 로그인 게이트