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.

Quellinformationen

Repository
rshankras/claude-code-apple-skills
Letzte Quellaktivität
16. Juli 2026 um 12:36
Erkannte Sprache von SKILL.md
Englisch
Sterne
770
Forks
74

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

Datei-Explorer
3 Dateien

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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
Auf GitHub ansehen