Skip to content

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() / Play acknowledge(). 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