skills/generators/subscription-lifecycle/SKILL.md
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.
npx skillsauth add rshankras/claude-code-apple-skills subscription-lifecycleInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
Use this skill when the user:
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:
StoreKitManager — don't duplicate product loadingSubscriptionStatus enum if presentIf no existing StoreKit code found:
Grep: "In-App Purchase" or "StoreKit" in *.entitlements
If missing, warn user to add the In-App Purchase capability in Xcode.
Ask user via AskUserQuestion:
Subscription tiers?
Lifecycle features? (multi-select)
Include subscription dashboard UI?
Server-side verification?
Read patterns.md for lifecycle state diagrams and StoreKit 2 behavior reference.
Read templates.md for production Swift code templates.
Generate these files:
SubscriptionState.swift — Comprehensive enum for all lifecycle statesSubscriptionMonitor.swift — @Observable class monitoring real-time status via Transaction.updates and Product.SubscriptionInfoSubscriptionEntitlement.swift — Maps product IDs to feature access levelsBased on configuration:
4. GracePeriodHandler.swift — If grace period selected
5. OfferManager.swift — If offer codes or win-back selected
If dashboard UI selected:
6. SubscriptionDashboardView.swift — SwiftUI view for plan management
Check project structure:
Sources/Store/ exists → Sources/Store/Lifecycle/Sources/ exists → Sources/SubscriptionLifecycle/App/ exists → App/SubscriptionLifecycle/SubscriptionLifecycle/After generation, provide:
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)
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()
}
}
}
@Test
func gracePeriodGrantsAccess() async throws {
let monitor = SubscriptionMonitor()
monitor.updateState(.inGracePeriod(expiresIn: 3))
#expect(monitor.hasAccess == true)
#expect(monitor.gracePeriodDaysRemaining == 3)
}
@Test
func billingRetryGrantsAccess() async throws {
let monitor = SubscriptionMonitor()
monitor.updateState(.inBillingRetry)
#expect(monitor.hasAccess == true)
#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)
}
// 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
}
// 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() }
)
}
// Present the system offer code redemption sheet
try await AppStore.presentOfferCodeRedeemSheet(in: windowScene)
// 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
Transaction.currentEntitlements — Returns currently active transactions. Use for checking if user has access RIGHT NOW. Does not include grace period or billing retry details.Product.SubscriptionInfo.status — Returns detailed subscription status array including grace period state, billing retry, renewal info. Use for lifecycle management and showing appropriate UI.currentEntitlements for simple access checks. Use SubscriptionInfo.status for lifecycle state handling..inGracePeriod and .inBillingRetryPeriod should typically grant continued access to reduce involuntary churn.Transaction.environment tells you if you're in sandbox, production, or XcodeNever forget to call transaction.finish(). Unfinished transactions will be re-delivered on every app launch, causing duplicate processing and potential UI glitches.
generators/paywall-generator — Purchase UI and initial transaction handlingmonetization/monetization-strategy — Pricing tiers and revenue planningdevelopment
Build, install, and launch an iOS app on a physical iPhone or iPad entirely from the command line (no Xcode GUI), using xcodebuild + devicectl. Use when the user wants to run, test, or screenshot their app on a real device without opening Xcode.
development
Comprehensive iOS development guidance including Swift best practices, SwiftUI patterns, UI/UX review against HIG, and app planning. Use for iOS code review, best practices, accessibility audits, or planning new iOS apps.
development
Build, install, launch, and screenshot an iOS app in the Simulator to verify a change visually. Use when the user wants to run the app, see a change live, screenshot the running app, or confirm a UI fix actually works (not just that it compiles).
development
Audits skills in this repo for consistency, API drift, and structural gaps. Produces a prioritized report grouped by severity (Critical/High/Medium/Low). Use when asked to "audit skills", "check the skill repo for drift", or when planning bulk skill cleanup. Read-only — does not apply fixes.