swiftship/internal/skills/data/features/subscription-manager/SKILL.md
Subscription and credit management architecture with RevenueCat. Use when implementing subscription state, credit balance, or feature gating.
npx skillsauth add abdullah4ai/apple-developer-toolkit subscription-managerInstall 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.
Single-layer: SubscriptionManager is an @Observable @MainActor singleton that wraps Purchases.shared directly. It holds RevenueCat Package objects — NOT custom plan models.
Do NOT create ANY of these:
enum SubscriptionPlan / enum SubscriptionTier / struct Plan with price propertiesvar price: String returning "$X.XX"static let sampleData: [SomePlanType] with hardcoded plans[SubscriptionPlan] or similar custom plan arraysimport Foundation
import RevenueCat
@Observable
@MainActor
final class SubscriptionManager {
static let shared = SubscriptionManager()
var isPremium = false
var packages: [Package] = [] // RevenueCat Package objects ONLY
var selectedPackage: Package?
var isLoading = false
var errorMessage: String?
var purchaseSuccess = false // Signals successful purchase for UX feedback
private init() {}
func configure() {
#if DEBUG
Purchases.logLevel = .debug
#endif
Purchases.configure(
with: Configuration.Builder(withAPIKey: AppConfig.revenueCatAPIKey)
.with(storeKitVersion: .storeKit2)
.build()
)
Task { await refreshStatus() }
Task { await listenForChanges() }
}
func loadOfferings() async {
isLoading = true
defer { isLoading = false }
do {
let offerings = try await Purchases.shared.offerings()
packages = offerings.current?.availablePackages ?? []
if selectedPackage == nil { selectedPackage = packages.first }
} catch {
errorMessage = "Could not load plans."
}
}
func purchase(_ package: Package) async {
isLoading = true
defer { isLoading = false }
do {
let result = try await Purchases.shared.purchase(package: package)
if !result.userCancelled {
isPremium = result.customerInfo.entitlements[AppConfig.entitlementID]?.isActive == true
if isPremium { purchaseSuccess = true }
}
} catch {
errorMessage = error.localizedDescription
}
}
func resetPurchaseSuccess() {
purchaseSuccess = false
}
func restore() async {
isLoading = true
defer { isLoading = false }
do {
let info = try await Purchases.shared.restorePurchases()
isPremium = info.entitlements[AppConfig.entitlementID]?.isActive == true
if !isPremium { errorMessage = "No active subscriptions found." }
} catch {
errorMessage = error.localizedDescription
}
}
private func refreshStatus() async {
do {
let info = try await Purchases.shared.customerInfo()
isPremium = info.entitlements[AppConfig.entitlementID]?.isActive == true
} catch {}
}
private func listenForChanges() async {
for try await info in Purchases.shared.customerInfoStream {
isPremium = info.entitlements[AppConfig.entitlementID]?.isActive == true
}
}
}
Key points:
packages is [Package] from RevenueCat — NOT a custom plan arraypurchase() takes a Package — NOT a custom enumpackage.storeProduct.localizedPriceString in the view layerAppConfig.entitlementID — never hardcode "premium"if SubscriptionManager.shared.isPremium {
// Premium feature
} else {
// Show paywall
}
AppConfig.entitlementIDAppConfig.ProductID constantsSubscriptionManager is the ONLY type that calls Purchases.shared. No other file should import and call Purchases.shared methods.
BANNED:
PaywallViewModel or any ViewModel that wraps/duplicates SubscriptionManagerPurchases.shared.purchase(), Purchases.shared.offerings(), or Purchases.shared.restorePurchases() directly@State private var manager = SubscriptionManager.shared — not its own ViewModelViewModels that gate features behind a subscription MUST observe SubscriptionManager.shared.isPremium reactively. Because SubscriptionManager is @Observable, any SwiftUI view or @Observable class that reads isPremium will automatically re-evaluate when subscription status changes (purchase, renewal, expiration, restore).
@Observable
@MainActor
final class SomeFeatureViewModel {
private let subscriptionManager = SubscriptionManager.shared
var canAccessFeature: Bool {
subscriptionManager.isPremium
}
var showUpgradePrompt: Bool {
!subscriptionManager.isPremium
}
}
Key points:
isPremium into a local property — always read through SubscriptionManager.shared@Observable tracking propagates automatically: when isPremium changes, any computed property reading it triggers view updatescustomerInfoStream in SubscriptionManager handles all external changes (renewals, expirations, family sharing, App Store refunds) — ViewModels do NOT need their own listeners@State private var manager = SubscriptionManager.shared also react automaticallyBANNED:
NotificationCenter or Combine publishers for subscription state — use @Observable trackingvar isPremium copies in ViewModels — always read from SubscriptionManager.sharedTask loops polling Purchases.shared.customerInfo() — the stream handles thisSee Model Pattern for why custom tier enums are banned.
tools
Apple platform skill for docs, WWDC lookup, App Store Connect work, and SwiftUI app generation. Use repo-local `node cli.js` for Apple docs and WWDC search, `appledev store` for App Store Connect workflows, and `appledev build` for app scaffolding or fix loops on macOS. USE WHEN: Apple APIs, WWDC sessions, TestFlight/App Store tasks, or building/fixing Apple-platform apps. DON'T USE WHEN: non-Apple platforms, generic backend work, or general web research. EDGE CASES: docs-only queries use `node cli.js` in this repo, not `appledev`; release workflows use `appledev store`; app scaffolding uses `appledev build`; rules-only requests can read `references/ios-rules/` or `references/swiftui-guides/` progressively without invoking binaries.
tools
All-in-one Apple developer skill with three integrated tools shipped as a single unified binary. (1) Documentation search across Apple frameworks, symbols, and 1,267 WWDC sessions from 2014-2025. No credentials needed. (2) App Store Connect CLI with 120+ commands covering builds (find/wait/upload), TestFlight, pre-submission validate, submissions, signing, subscriptions (family-sharable), IAP, analytics, Xcode Cloud, metadata workflows, release pipeline dashboard, insights, win-back offers, promoted purchases, product pages, nominations, accessibility declarations, pre-orders, pricing filters, localizations update, diff, webhooks with local receiver, workflow automation, and more. Requires App Store Connect API key. (3) Multi-platform app builder (iOS/watchOS/tvOS/iPad/macOS/visionOS) that generates complete Swift/SwiftUI apps from natural language with auto-fix, simulator launch, interactive chat mode, and open-in-Xcode. Requires an LLM API key and Xcode. Includes 38 iOS development rules and 12 SwiftUI best practice guides for Liquid Glass, navigation, state management, and modern APIs. All three tools ship as one binary (appledev). USE WHEN: Apple API docs, App Store Connect management, WWDC lookup, or building iOS/watchOS/tvOS/macOS/visionOS apps from scratch. DON'T USE WHEN: non-Apple platforms or general coding.
testing
watchOS complications: WidgetKit complication families, accessory sizes, timeline providers for watch face. Use when implementing watchOS-specific patterns related to widgets.
development
watchOS haptic feedback: WKInterfaceDevice preset haptic types for wrist-based feedback. Use when implementing watchOS-specific patterns related to haptics.