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.
Best use case
subscription-lifecycle is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
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.
Teams using subscription-lifecycle should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/subscription-lifecycle/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How subscription-lifecycle Compares
| Feature / Agent | subscription-lifecycle | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
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.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# 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.
## 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 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)
}
```
## 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
}
```
### 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() }
)
}
```
### 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.
- `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** (if enabled in App Store Connect): User retains access for 6 or 16 days after payment failure. Apple shows its own payment failure messaging.
- **Billing retry period**: After grace period expires (or if no grace period), Apple retries billing for up to 60 days. User access depends on your app's policy.
- **Important:** Both `.inGracePeriod` and `.inBillingRetryPeriod` should typically grant continued access to reduce involuntary churn.
### 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 planningRelated Skills
subscription-offers
Generates StoreKit 2 code for all subscription offer types — introductory, promotional, offer codes, and win-back. Includes eligibility checks, offer presentation, and the preferredSubscriptionOffer modifier. Use when adding subscription offers, free trials, or promotional pricing.
watchOS
watchOS development guidance including SwiftUI for Watch, Watch Connectivity, complications, and watch-specific UI patterns. Use for watchOS code review, best practices, or Watch app development.
visionos-widgets
visionOS widget patterns including mounting styles, glass/paper textures, proximity-aware layouts, and spatial widget families. Use when creating or adapting widgets for visionOS.
test-data-factory
Generate test fixture factories for your models. Builder pattern and static factories for zero-boilerplate test data. Use when tests need sample data setup.
test-contract
Generate protocol/interface test suites that any implementation must pass. Define the contract once, test every implementation. Use when designing protocols or swapping implementations.
tdd-refactor-guard
Pre-refactor safety checklist. Verifies test coverage exists before AI modifies existing code. Use before asking AI to refactor anything.
tdd-feature
Red-green-refactor scaffold for building new features with TDD. Write failing tests first, then implement to pass. Use when building new features test-first.
tdd-bug-fix
Fix bugs using red-green-refactor — reproduce the bug as a failing test first, then fix it. Use when fixing bugs to ensure they never regress.
snapshot-test-setup
Set up SwiftUI visual regression testing with swift-snapshot-testing. Generates snapshot test boilerplate and CI configuration. Use for UI regression prevention.
integration-test-scaffold
Generate cross-module test harness with mock servers, in-memory stores, and test configuration. Use when testing networking + persistence + business logic together.
characterization-test-generator
Generates tests that capture current behavior of existing code before refactoring. Use when you need a safety net before AI-assisted refactoring or modifying legacy code.
testing
TDD and testing skills for iOS/macOS apps. Covers characterization tests, TDD workflows, test contracts, snapshot tests, and test infrastructure. Use for test-driven development, adding tests to existing code, or building test infrastructure.