Skip to main content

subscription-lifecycle

Generates StoreKit 2 subscription lifecycle management — grace periods, billing retry, offer codes, win-back offers, upgrade/downgrade paths, and subscription status monitoring. Use when user needs post-purchase subscription state handling beyond the initial paywall.

Informações da origem

Repositório
rshankras/claude-code-apple-skills
Última atividade na origem
16 de julho de 2026 às 12:36
Idioma detectado do SKILL.md
inglês
Estrelas
770
Forks
74

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Explorador de arquivos
3 arquivos

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
subscription-lifecycle
description
Generates StoreKit 2 subscription lifecycle management — grace periods, billing retry, offer codes, win-back offers, upgrade/downgrade paths, and subscription status monitoring. Use when user needs post-purchase subscription state handling beyond the initial paywall.
allowed-tools
["Read","Write","Edit","Glob","Grep","Bash","AskUserQuestion"]
last_verified
2026-07-16T00:00:00.000Z
review_by
2027-06-22T00:00:00.000Z
os_version
iOS 27 / macOS 27
# Subscription Lifecycle Generator Generate production StoreKit 2 subscription lifecycle management with real-time status monitoring, grace period handling, billing retry detection, offer code redemption, win-back offers, and upgrade/downgrade path support. **Different from paywall-generator:** The paywall generator handles the purchase UI and initial transaction. This skill handles everything that happens *after* purchase — monitoring subscription state changes, handling payment failures, retaining churning users, and managing tier transitions. **Why retention math dominates acquisition (per Apple's Tech Talk):** - Commission compounds: 70% net in year one → 85% once a subscriber passes 1 year. Retained subscribers are literally worth more per renewal. - The base compounds harder: from 10,000 subscribers, 12 months later 85% monthly retention leaves 1,673; 90% leaves 3,487; 95% leaves 5,688. A 5-point retention gain more than doubles the surviving base. ## When This Skill Activates Use this skill when the user: - Asks about "subscription management" or "subscription lifecycle" - Mentions "grace period handling" or "grace period UI" - Wants "billing retry" detection or payment failure handling - Asks about "win-back offers" or "re-engagement offers" - Mentions "subscription status" monitoring or dashboard - Wants "upgrade/downgrade" path management - Asks about "offer codes" or "promotional offers" - Mentions "subscription churn" or "retention" - Wants to "track subscription state changes" ## Pre-Generation Checks ### 1. Project Context Detection - [ ] Check deployment target (StoreKit 2 requires iOS 15+) - [ ] Check for @Observable support (iOS 17+ / macOS 14+) - [ ] Check Swift version (requires Swift 5.9+) - [ ] Identify source file locations ### 2. Existing StoreKit Detection Search for existing subscription code: ``` Glob: **/*Store*.swift, **/*Subscription*.swift, **/*Entitlement*.swift Grep: "import StoreKit" or "Transaction.updates" or "Product.SubscriptionInfo" ``` If paywall-generator output found: - Integrate with existing `StoreKitManager` — don't duplicate product loading - Extend existing `SubscriptionStatus` enum if present - Wire into existing transaction listener If no existing StoreKit code found: - Generate standalone — include minimal product loading - Recommend running paywall-generator for purchase UI ### 3. Entitlement Check ``` Grep: "In-App Purchase" or "StoreKit" in *.entitlements ``` If missing, warn user to add the In-App Purchase capability in Xcode. ## Configuration Questions Ask user via AskUserQuestion: 1. **Subscription tiers?** - Single tier (one plan, e.g., "Pro") - Multiple tiers (e.g., "Basic", "Pro", "Business") with upgrade/downgrade paths 2. **Lifecycle features?** (multi-select) - Grace period detection and UI messaging - Billing retry period handling - Offer code redemption (App Store offer codes) - Win-back offers for expired subscribers - Upgrade/downgrade/crossgrade management 3. **Include subscription dashboard UI?** - Yes — SwiftUI view showing current plan, renewal date, management options - No — logic only, integrate into existing UI 4. **Server-side verification?** - Client-only (StoreKit 2 on-device verification) — recommended for most apps - Server-side (App Store Server API v2) — for apps with server backends ## Generation Process ### Step 1: Read Templates and Patterns Read `patterns.md` for lifecycle state diagrams and StoreKit 2 behavior reference. Read `templates.md` for production Swift code templates. ### Step 2: Create Core Files Generate these files: 1. `SubscriptionState.swift` — Comprehensive enum for all lifecycle states 2. `SubscriptionMonitor.swift` — @Observable class monitoring real-time status via `Transaction.updates` and `Product.SubscriptionInfo` 3. `SubscriptionEntitlement.swift` — Maps product IDs to feature access levels ### Step 3: Create Lifecycle Handlers Based on configuration: 4. `GracePeriodHandler.swift` — If grace period selected 5. `OfferManager.swift` — If offer codes or win-back selected ### Step 4: Create UI Files If dashboard UI selected: 6. `SubscriptionDashboardView.swift` — SwiftUI view for plan management ### Step 5: Determine File Location Check project structure: - If `Sources/Store/` exists → `Sources/Store/Lifecycle/` - If `Sources/` exists → `Sources/SubscriptionLifecycle/` - If `App/` exists → `App/SubscriptionLifecycle/` - Otherwise → `SubscriptionLifecycle/` ## Output Format After generation, provide: ### Files Created ``` SubscriptionLifecycle/ ├── SubscriptionState.swift # All lifecycle states enum ├── SubscriptionMonitor.swift # Real-time status monitoring ├── SubscriptionEntitlement.swift # Product ID → feature mapping ├── GracePeriodHandler.swift # Grace period detection & UI (optional) ├── OfferManager.swift # Offers, codes, win-back (optional) └── SubscriptionDashboardView.swift # Plan management UI (optional) ``` ### Integration with Existing Paywall **If paywall-generator was already used:** ```swift // In your existing StoreKitManager, add lifecycle monitoring @Observable final class StoreKitManager { // ... existing product loading and purchase code ... let lifecycleMonitor = SubscriptionMonitor() func startMonitoring() async { await lifecycleMonitor.start( groupID: "your.subscription.group", entitlements: SubscriptionEntitlement.default ) } } ``` **App Entry Point:** ```swift @main struct MyApp: App { @State private var monitor = SubscriptionMonitor() var body: some Scene { WindowGroup { ContentView() .environment(monitor) .task { await monitor.start(groupID: "your.group.id") } } } } ``` **Check Access Anywhere:** ```swift struct PremiumFeatureView: View { @Environment(SubscriptionMonitor.self) private var monitor var body: some View { if monitor.hasAccess { // Full feature PremiumContent() } else if monitor.state == .inGracePeriod { // Feature still accessible, but show payment warning VStack { PaymentWarningBanner() PremiumContent() } } else { // Show paywall PaywallView() } } } ``` **Grace Period Notification:** ```swift struct ContentView: View { @Environment(SubscriptionMonitor.self) private var monitor var body: some View { NavigationStack { MainContent() .overlay(alignment: .top) { if monitor.state == .inGracePeriod { GracePeriodBanner( daysRemaining: monitor.gracePeriodDaysRemaining, onFixPayment: { /* open manage subscriptions */ } ) } } } } } ``` **Win-Back Offer:** ```swift struct ExpiredUserView: View { @State private var offerManager = OfferManager() var body: some View { if let winBackOffer = offerManager.availableWinBackOffer { WinBackOfferCard(offer: winBackOffer) { try await offerManager.redeemWinBackOffer(winBackOffer) } } else { StandardPaywallView() } } } ``` ### Testing ```swift @Test func gracePeriodGrantsAccess() async throws { let monitor = SubscriptionMonitor() monitor.updateState(.inGracePeriod(expiresIn: 3)) #expect(monitor.hasAccess == true) #expect(monitor.gracePeriodDaysRemaining == 3) } @Test func billingRetryLimitsAccess() async throws { let monitor = SubscriptionMonitor() monitor.updateState(.inBillingRetry) // Apple's guidance: stop full service at billing retry (grace period is // where you keep serving). Prefer limited access over a hard cut. #expect(monitor.hasAccess == false) #expect(monitor.shouldShowPaymentWarning == true) } @Test func expiredRevokesAccess() async throws { let monitor = SubscriptionMonitor() monitor.updateState(.expired(reason: .autoRenewDisabled)) #expect(monitor.hasAccess == false) } @Test func upgradeChangesEntitlementLevel() async throws { let entitlements = SubscriptionEntitlement.default let basicLevel = entitlements.accessLevel(for: "com.app.basic.monthly") let proLevel = entitlements.accessLevel(for: "com.app.pro.monthly") #expect(proLevel > basicLevel) } ``` ## Common Patterns ### Status Checking ```swift // Check current subscription state let state = monitor.state switch state { case .active(let renewalDate): print("Active until \(renewalDate)") case .inGracePeriod(let expiresIn): print("Payment issue — \(expiresIn) days to fix") case .inBillingRetry: print("Apple retrying payment") case .expired(let reason): print("Expired: \(reason)") case .revoked: print("Refunded or revoked") default: break } ``` ### Per-State Entitlement & Messaging Model the full subscriber state machine — each state gets its own entitlement decision and its own message: | State | Entitlement | Message | |-------|-------------|---------| | Active | Full | None | | Active, auto-renew off | Full | Save offer *before* expiry | | Grace period | Full | Countdown + deep link to payment settings | | Billing retry | Limited access (not a hard cut) | Persistent fix-payment banner — no offers | | Expired | None | Win-back, tailored by `expirationReason` | Do NOT re-pitch or discount users in billing retry — the App Store retries billing automatically for up to 60 days and is actively recovering them. And recoveries inside the grace window preserve billing-date continuity: no revenue gap. ### Grace Period Notification ```swift // Show in-app banner during grace period if case .inGracePeriod(let days) = monitor.state { Banner( message: "Payment issue. Update payment method within \(days) days.", action: "Fix Now", onTap: { await openSubscriptionManagement() } ) } ``` ### Catch Voluntary Churn Before Expiry Per Apple's Tech Talk, >90% of customers cancel with at least 2 days of paid service remaining — the cancel-to-expiry window is your save-offer moment: ```swift // Still entitled, but auto-renew just flipped off — surface a save offer NOW if status.state == .subscribed, case .verified(let renewalInfo) = status.renewalInfo, renewalInfo.willAutoRenew == false { showSaveOffer() } // Tailor win-backs after expiry if let reason = renewalInfo.expirationReason, reason == .didNotConsentToPriceIncrease { // Price-sensitive churn — lead with a win-back offer } ``` ### Defer Apple's Billing Sheet (Message API) The App Store wants to show its own payment-update sheet on next launch after a billing failure. Use the StoreKit Message API to defer it past critical moments (mid-checkout, mid-workout): ```swift for await message in Message.messages where message.reason == .billingIssue { await deferUntilSafeMoment() try? message.display(in: windowScene) } ``` ### Server Notifications to Wire (V2) | Notification | Meaning | |--------------|---------| | `DID_FAIL_TO_RENEW` | Subscriber entered billing retry | | `DID_RENEW` + subtype `BILLING_RECOVERY` | Retry succeeded — recovery complete | | `GRACE_PERIOD_EXPIRED` | Grace ended unrecovered — retry continues, entitlement decision is yours | ### Offer Code Redemption ```swift // Present the system offer code redemption sheet try await AppStore.presentOfferCodeRedeemSheet(in: windowScene) ``` ### Tier Upgrade ```swift // Upgrade from Basic to Pro (takes effect immediately) let proProduct = try await Product.products(for: ["com.app.pro.monthly"]).first! let result = try await proProduct.purchase() // StoreKit handles prorating automatically ``` ## Gotchas ### Transaction.currentEntitlements vs Product.SubscriptionInfo.status - `Transaction.currentEntitlements` — Returns currently active transactions. Use for checking if user has access RIGHT NOW. Does not include grace period or billing retry details. - Migrate to `Transaction.currentEntitlements(for:)` — plural by design: one product can carry multiple current transactions (e.g., the user's own purchase plus a Family Sharing transaction). - `Product.SubscriptionInfo.status` — Returns detailed subscription status array including grace period state, billing retry, renewal info. Use for lifecycle management and showing appropriate UI. - **Rule:** Use `currentEntitlements` for simple access checks. Use `SubscriptionInfo.status` for lifecycle state handling. ### Grace Period vs Billing Retry Period - **Grace period** (opt-in in App Store Connect): choose 3, 16, or 28 days — pick 28 for maximum recovery. Per Apple's Tech Talk, ~40% of customers fix billing within 3 days, 75% within 16, and up to 90% within 28. - **Billing retry period**: After grace period expires (or if no grace period), Apple retries billing automatically for up to 60 days. Don't re-pitch users already in retry — the App Store is recovering them. - **Entitlement rule (`renewalState`):** keep serving `.inGracePeriod`; stop full service at `.inBillingRetryPeriod` — but prefer limited access over a hard cut, plus a persistent fix-payment banner. ### Sandbox vs Production Testing - Sandbox subscriptions renew at accelerated rates (monthly = ~5 minutes) - Sandbox does not support all offer types - `Transaction.environment` tells you if you're in sandbox, production, or Xcode - Grace periods behave differently in sandbox — shorter durations - Always test with StoreKit Testing in Xcode first, then sandbox, then TestFlight ### Offer Eligibility - **Introductory offers:** Only for users who have never subscribed to any product in the subscription group - **Promotional offers:** Require signing with your App Store Connect key; you control eligibility - **Offer codes:** One-time use codes you generate in App Store Connect; limited to 10M per app per quarter - **Win-back offers (iOS 18+):** Apple determines eligibility for lapsed subscribers; you configure in App Store Connect ### Transaction.finish() is Critical Never forget to call `transaction.finish()`. Unfinished transactions will be re-delivered on every app launch, causing duplicate processing and potential UI glitches. ## References - **templates.md** — All production Swift code templates - **patterns.md** — Lifecycle state diagrams, StoreKit 2 behavior reference, anti-patterns - Related: `generators/paywall-generator` — Purchase UI and initial transaction handling - Related: `monetization/monetization-strategy` — Pricing tiers and revenue planning
Ver no GitHub