← LeaderboardThe Skill Leaderboard

subscription-lifecycle

Skill file skills/generators/subscription-lifecycle/SKILL.md in rshankras/claude-code-apple-skills. Read the publisher’s upstream source.

Description could not be read from the source metadata.

Published by rshankras ↗rshankras/claude-code-apple-skills

Try your first task

subscription-lifecycle

Start a fresh conversation.

Use subscription-lifecycle to implement StoreKit 2 subscription lifecycle management in [project path and subscription configuration: product tiers, desired lifecycle features, dashboard preference, and client-only or server-side verification], integrating with existing StoreKit code when present and producing the appropriate lifecycle state, monitoring, entitlement, optional handler, and UI files.

    User-approved starter prompt based on a pinned source version; check current skill requirements before running. Task evidence ↗

    SKILL.md

    name: subscription-lifecycledescription: 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-16review_by: 2027-06-22os_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:

    // 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:

    @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:

    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:

    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:

    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

    @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

    // 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:

    StateEntitlementMessage
    ActiveFullNone
    Active, auto-renew offFullSave offer before expiry
    Grace periodFullCountdown + deep link to payment settings
    Billing retryLimited access (not a hard cut)Persistent fix-payment banner — no offers
    ExpiredNoneWin-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

    // 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:

    // 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):

    for await message in Message.messages where message.reason == .billingIssue {
        await deferUntilSafeMoment()
        try? message.display(in: windowScene)
    }
    

    Server Notifications to Wire (V2)

    NotificationMeaning
    DID_FAIL_TO_RENEWSubscriber entered billing retry
    DID_RENEW + subtype BILLING_RECOVERYRetry succeeded — recovery complete
    GRACE_PERIOD_EXPIREDGrace ended unrecovered — retry continues, entitlement decision is yours

    Offer Code Redemption

    // Present the system offer code redemption sheet
    try await AppStore.presentOfferCodeRedeemSheet(in: windowScene)
    

    Tier Upgrade

    // 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