All skills

Code generator skills that produce production-ready Swift code for common app components. Use when user wants to add logging, analytics, onboarding, review prompts, networking, authentication, paywalls, settings, persistence, error monitoring, CI/CD pipelines, localization, push notifications, deep linking, testing, accessibility, widgets, feature flags, app icons, image caching, pagination, HTTP caching, share cards, social export, subscription lifecycle, referral systems, watermarks, streak tracking, milestone celebrations, what's new screens, lapsed user re-engagement, usage insights, variable rewards, consent flows, account deletion, permission priming, force updates, state restoration, debug menus, offline queues, feedback forms, announcement banners, quick win sessions, Spotlight indexing, App Clips, screenshot automation, background processing, app extensions, data export, or SwiftUI preview sample data and variant matrices.

Use this Skill: https://skilld.dev/gh/rshankras/claude-code-apple-skills/generators

This session only. Nothing lands on disk.

subscription-lifecycletemplates.md

≈8.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Subscription Lifecycle Code Templates

Production-ready Swift templates for StoreKit 2 subscription lifecycle management. All code targets iOS 15+ (iOS 17+ / macOS 14+ for @Observable) and uses modern Swift concurrency.

SubscriptionState.swift

import Foundation
import StoreKit

/// Comprehensive subscription lifecycle state.
///
/// Covers all possible states a subscription can be in,
/// including grace period, billing retry, and revocation.
enum SubscriptionState: Sendable, Equatable {
    /// No subscription found — user has never subscribed.
    case notSubscribed

    /// Subscription is active and in good standing.
    /// - Parameter renewalDate: Next renewal or expiration date.
    case active(renewalDate: Date)

    /// Payment failed but user retains access during grace period.
    /// - Parameter daysRemaining: Approximate days left in grace period.
    case inGracePeriod(daysRemaining: Int)

    /// Grace period expired; Apple is retrying billing.
    /// User access is at your discretion (recommended: grant access).
    case inBillingRetry

    /// Subscription expired.
    /// - Parameter reason: Why the subscription expired.
    case expired(reason: ExpirationReason)

    /// Subscription was revoked (refund or family sharing removal).
    case revoked

    /// User upgraded to a higher tier in the same subscription group.
    /// - Parameter newProductID: The product ID of the new tier.
    case upgraded(newProductID: String)

    /// Status is being determined (initial load).
    case unknown

    /// Whether the user should have access to premium features.
    ///
    /// Grants access during grace period and billing retry
    /// to minimize involuntary churn.
    var hasAccess: Bool {
        switch self {
        case .active, .inGracePeriod, .inBillingRetry:
            return true
        case .notSubscribed, .expired, .revoked, .upgraded, .unknown:
            return false
        }
    }

    /// Whether the app should show a payment warning banner.
    var shouldShowPaymentWarning: Bool {
        switch self {
        case .inGracePeriod, .inBillingRetry:
            return true
        default:
            return false
        }
    }
}

/// Reasons a subscription expired.
enum ExpirationReason: Sendable, Equatable {
    /// User disabled auto-renew.
    case autoRenewDisabled
    /// Billing failed and retry period ended.
    case billingError
    /// User declined a price increase.
    case didNotConsentToPriceIncrease
    /// Product is no longer available.
    case productUnavailable
    /// Unknown or unhandled reason.
    case unknown
}

SubscriptionMonitor.swift

import Foundation
import StoreKit
import Observation

/// Monitors subscription status in real time using StoreKit 2.
///
/// Listens for `Transaction.updates` and polls `Product.SubscriptionInfo`
/// to keep subscription state current throughout the app lifecycle.
///
/// Usage:
/// ```swift
/// @State private var monitor = SubscriptionMonitor()
///
/// ContentView()
///     .environment(monitor)
///     .task { await monitor.start(groupID: "your.group.id") }
/// ```
@Observable
final class SubscriptionMonitor {
    /// Current subscription state.
    private(set) var state: SubscriptionState = .unknown

    /// Current subscription product ID, if any.
    private(set) var currentProductID: String?

    /// Next renewal or expiration date, if active.
    private(set) var renewalDate: Date?

    /// Whether auto-renew is enabled for the current subscription.
    private(set) var isAutoRenewEnabled: Bool = false

    private var transactionListener: Task<Void, Never>?
    private var groupID: String = ""
    private var entitlements: SubscriptionEntitlement?

    deinit {
        transactionListener?.cancel()
    }

    // MARK: - Public API

    /// Start monitoring subscription status.
    ///
    /// Call this once at app launch. The monitor will continuously
    /// listen for transaction updates and refresh status.
    ///
    /// - Parameters:
    ///   - groupID: Your subscription group identifier from App Store Connect.
    ///   - entitlements: Optional entitlement mapping for multi-tier subscriptions.
    func start(groupID: String, entitlements: SubscriptionEntitlement? = nil) async {
        self.groupID = groupID
        self.entitlements = entitlements

        // Listen for real-time transaction updates
        transactionListener = Task.detached { [weak self] in
            for await result in Transaction.updates {
                guard let self else { return }
                if case .verified(let transaction) = result {
                    await transaction.finish()
                    await self.refreshStatus()
                }
            }
        }

        // Initial status check
        await refreshStatus()
    }

    /// Force a status refresh.
    ///
    /// Call after a purchase, restore, or when returning to foreground.
    func refreshStatus() async {
        do {
            let statuses = try await Product.SubscriptionInfo.status(for: groupID)
            await MainActor.run {
                updateFromStatuses(statuses)
            }
        } catch {
            // If status fetch fails, fall back to entitlements check
            await checkEntitlements()
        }
    }

    /// Convenience: whether the user currently has access.
    var hasAccess: Bool {
        state.hasAccess
    }

    /// Days remaining in grace period, or nil if not in grace period.
    var gracePeriodDaysRemaining: Int? {
        if case .inGracePeriod(let days) = state {
            return days
        }
        return nil
    }

    /// Whether the user should see a payment warning.
    var shouldShowPaymentWarning: Bool {
        state.shouldShowPaymentWarning
    }

    /// Open the system subscription management page.
    func openSubscriptionManagement() async {
        guard let windowScene = await MainActor.run(body: {
            UIApplication.shared.connectedScenes
                .compactMap { $0 as? UIWindowScene }
                .first
        }) else { return }

        do {
            try await AppStore.showManageSubscriptions(in: windowScene)
        } catch {
            // Fallback: open Settings
            if let url = URL(string: "https://apps.apple.com/account/subscriptions") {
                await MainActor.run {
                    UIApplication.shared.open(url)
                }
            }
        }
    }

    // MARK: - Internal (exposed for testing)

    /// Update state directly. Exposed for unit testing.
    func updateState(_ newState: SubscriptionState) {
        state = newState
    }

    // MARK: - Private

    private func updateFromStatuses(_ statuses: [Product.SubscriptionInfo.Status]) {
        // Find the most relevant status (highest priority)
        for status in statuses {
            guard case .verified(let renewalInfo) = status.renewalInfo,
                  case .verified(let transaction) = status.transaction else {
                continue
            }

            currentProductID = transaction.productID
            isAutoRenewEnabled = renewalInfo.willAutoRenew

            switch status.state {
            case .subscribed:
                renewalDate = transaction.expirationDate
                state = .active(renewalDate: transaction.expirationDate ?? Date.distantFuture)
                return

            case .inGracePeriod:
                let daysRemaining = daysUntil(transaction.expirationDate)
                state = .inGracePeriod(daysRemaining: max(daysRemaining, 0))
                return

            case .inBillingRetryPeriod:
                state = .inBillingRetry
                return

            case .expired:
                let reason = mapExpirationReason(renewalInfo)
                state = .expired(reason: reason)
                return

            case .revoked:
                state = .revoked
                return

            default:
                continue
            }
        }

        // No active status found
        state = .notSubscribed
        currentProductID = nil
        renewalDate = nil
    }

    private func checkEntitlements() async {
        var foundEntitlement = false
        for await result in Transaction.currentEntitlements {
            if case .verified(let transaction) = result {
                if transaction.productType == .autoRenewable {
                    foundEntitlement = true
                    await MainActor.run {
                        currentProductID = transaction.productID
                        renewalDate = transaction.expirationDate
                        state = .active(renewalDate: transaction.expirationDate ?? Date.distantFuture)
                    }
                    break
                }
            }
        }

        if !foundEntitlement {
            await MainActor.run {
                state = .notSubscribed
            }
        }
    }

    private func mapExpirationReason(_ renewalInfo: Product.SubscriptionInfo.RenewalInfo) -> ExpirationReason {
        guard let reason = renewalInfo.expirationReason else {
            return .unknown
        }

        switch reason {
        case .autoRenewDisabled:
            return .autoRenewDisabled
        case .billingError:
            return .billingError
        case .didNotConsentToPriceIncrease:
            return .didNotConsentToPriceIncrease
        case .productUnavailable:
            return .productUnavailable
        default:
            return .unknown
        }
    }

    private func daysUntil(_ date: Date?) -> Int {
        guard let date else { return 0 }
        let interval = date.timeIntervalSince(Date())
        return max(Int(ceil(interval / 86400)), 0)
    }
}

GracePeriodHandler.swift

import Foundation
import StoreKit
import SwiftUI

/// Handles grace period detection and provides UI components
/// for communicating payment issues to the user.
///
/// Grace periods (6 or 16 days, configured in App Store Connect)
/// give users continued access while Apple resolves payment issues.
/// This handler detects the state and provides appropriate messaging.
///
/// Usage:
/// ```swift
/// GracePeriodBanner(
///     daysRemaining: monitor.gracePeriodDaysRemaining ?? 0,
///     onFixPayment: { await monitor.openSubscriptionManagement() }
/// )
/// ```
struct GracePeriodBanner: View {
    let daysRemaining: Int
    let onFixPayment: () async -> Void

    @State private var isProcessing = false

    var body: some View {
        HStack(spacing: 12) {
            Image(systemName: "exclamationmark.triangle.fill")
                .foregroundStyle(.yellow)
                .font(.title3)

            VStack(alignment: .leading, spacing: 2) {
                Text("Payment Issue")
                    .font(.subheadline.bold())

                Text(warningMessage)
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }

            Spacer()

            Button {
                isProcessing = true
                Task {
                    await onFixPayment()
                    isProcessing = false
                }
            } label: {
                if isProcessing {
                    ProgressView()
                        .controlSize(.small)
                } else {
                    Text("Fix Now")
                        .font(.subheadline.bold())
                }
            }
            .buttonStyle(.borderedProminent)
            .controlSize(.small)
            .disabled(isProcessing)
        }
        .padding()
        .background {
            RoundedRectangle(cornerRadius: 12)
                .fill(Color.yellow.opacity(0.1))
                .overlay(
                    RoundedRectangle(cornerRadius: 12)
                        .strokeBorder(Color.yellow.opacity(0.3), lineWidth: 1)
                )
        }
        .padding(.horizontal)
    }

    private var warningMessage: String {
        if daysRemaining <= 1 {
            return "Update your payment method today to keep your subscription."
        } else {
            return "Update your payment method within \(daysRemaining) days to keep your subscription."
        }
    }
}

/// Banner for billing retry period (after grace period expires).
struct BillingRetryBanner: View {
    let onFixPayment: () async -> Void

    @State private var isProcessing = false

    var body: some View {
        HStack(spacing: 12) {
            Image(systemName: "creditcard.trianglebadge.exclamationmark")
                .foregroundStyle(.orange)
                .font(.title3)

            VStack(alignment: .leading, spacing: 2) {
                Text("Billing Issue")
                    .font(.subheadline.bold())

                Text("There's a problem with your payment method. Update it to continue your subscription.")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }

            Spacer()

            Button {
                isProcessing = true
                Task {
                    await onFixPayment()
                    isProcessing = false
                }
            } label: {
                if isProcessing {
                    ProgressView()
                        .controlSize(.small)
                } else {
                    Text("Update")
                        .font(.subheadline.bold())
                }
            }
            .buttonStyle(.bordered)
            .controlSize(.small)
            .disabled(isProcessing)
        }
        .padding()
        .background {
            RoundedRectangle(cornerRadius: 12)
                .fill(Color.orange.opacity(0.1))
                .overlay(
                    RoundedRectangle(cornerRadius: 12)
                        .strokeBorder(Color.orange.opacity(0.3), lineWidth: 1)
                )
        }
        .padding(.horizontal)
    }
}

/// Composite view that shows the appropriate banner based on state.
struct SubscriptionWarningOverlay: View {
    let state: SubscriptionState
    let onFixPayment: () async -> Void

    var body: some View {
        switch state {
        case .inGracePeriod(let daysRemaining):
            GracePeriodBanner(
                daysRemaining: daysRemaining,
                onFixPayment: onFixPayment
            )
            .transition(.move(edge: .top).combined(with: .opacity))

        case .inBillingRetry:
            BillingRetryBanner(onFixPayment: onFixPayment)
                .transition(.move(edge: .top).combined(with: .opacity))

        default:
            EmptyView()
        }
    }
}

OfferManager.swift

import Foundation
import StoreKit
import Observation

/// Manages subscription offers: promotional offers, offer codes,
/// and win-back offers for lapsed subscribers.
///
/// Usage:
/// ```swift
/// @State private var offerManager = OfferManager()
///
/// // Check for win-back eligibility
/// if let offer = offerManager.availableWinBackOffer {
///     WinBackOfferCard(offer: offer) {
///         try await offerManager.redeemWinBackOffer(offer)
///     }
/// }
/// ```
@Observable
final class OfferManager {
    /// Available win-back offer for lapsed subscribers, if eligible.
    private(set) var availableWinBackOffer: WinBackOffer?

    /// Available promotional offers for the subscription group.
    private(set) var promotionalOffers: [PromotionalOffer] = []

    /// Whether an offer redemption is in progress.
    private(set) var isRedeeming = false

    /// Last error from an offer operation.
    private(set) var lastError: Error?

    private var groupID: String = ""
    private var productIDs: [String] = []

    // MARK: - Public API

    /// Load available offers for the subscription group.
    ///
    /// - Parameters:
    ///   - groupID: Your subscription group identifier.
    ///   - productIDs: Product IDs to check offers for.
    func loadOffers(groupID: String, productIDs: [String]) async {
        self.groupID = groupID
        self.productIDs = productIDs

        await loadWinBackOffers()
        await loadPromotionalOffers()
    }

    /// Redeem a win-back offer.
    ///
    /// - Parameter offer: The win-back offer to redeem.
    /// - Throws: StoreKit errors if purchase fails.
    func redeemWinBackOffer(_ offer: WinBackOffer) async throws {
        isRedeeming = true
        lastError = nil
        defer { isRedeeming = false }

        let products = try await Product.products(for: [offer.productID])
        guard let product = products.first else {
            throw OfferError.productNotFound(offer.productID)
        }

        let result = try await product.purchase()

        switch result {
        case .success(let verification):
            let transaction = try checkVerified(verification)
            await transaction.finish()
        case .userCancelled:
            break
        case .pending:
            break
        @unknown default:
            break
        }
    }

    /// Redeem a promotional offer.
    ///
    /// Promotional offers require server-side signing. Pass the signed
    /// offer from your server to complete the purchase.
    ///
    /// - Parameters:
    ///   - offer: The promotional offer to redeem.
    ///   - product: The product to purchase with this offer.
    ///   - signedOffer: Server-signed offer parameters.
    func redeemPromotionalOffer(
        _ offer: PromotionalOffer,
        for product: Product,
        signedOffer: Product.PurchaseOption
    ) async throws {
        isRedeeming = true
        lastError = nil
        defer { isRedeeming = false }

        let result = try await product.purchase(options: [signedOffer])

        switch result {
        case .success(let verification):
            let transaction = try checkVerified(verification)
            await transaction.finish()
        case .userCancelled:
            break
        case .pending:
            break
        @unknown default:
            break
        }
    }

    /// Present the system offer code redemption sheet.
    ///
    /// Users can enter offer codes generated in App Store Connect.
    @MainActor
    func presentOfferCodeRedemption() async throws {
        guard let windowScene = UIApplication.shared.connectedScenes
            .compactMap({ $0 as? UIWindowScene })
            .first else {
            throw OfferError.noWindowScene
        }

        try await AppStore.presentOfferCodeRedeemSheet(in: windowScene)
    }

    /// Check if a user is eligible for an introductory offer.
    ///
    /// - Parameter product: The subscription product to check.
    /// - Returns: `true` if the user has never subscribed to the group.
    func isEligibleForIntroductoryOffer(_ product: Product) async -> Bool {
        await product.subscription?.isEligibleForIntroOffer ?? false
    }

    // MARK: - Private

    private func loadWinBackOffers() async {
        // Win-back offers are available in iOS 18+
        guard #available(iOS 18, *) else { return }

        do {
            let products = try await Product.products(for: productIDs)
            for product in products {
                guard let subscription = product.subscription else { continue }

                // Check subscription offers for win-back type
                for offer in subscription.promotionalOffers {
                    // Win-back offers are a subset of promotional offers
                    // Apple determines eligibility automatically
                    let winBack = WinBackOffer(
                        id: offer.id ?? "unknown",
                        productID: product.id,
                        displayPrice: offer.displayPrice,
                        period: offer.period,
                        periodCount: offer.periodCount,
                        paymentMode: offer.paymentMode
                    )
                    availableWinBackOffer = winBack
                    return  // Use first available
                }
            }
        } catch {
            lastError = error
        }
    }

    private func loadPromotionalOffers() async {
        do {
            let products = try await Product.products(for: productIDs)
            var offers: [PromotionalOffer] = []

            for product in products {
                guard let subscription = product.subscription else { continue }

                for offer in subscription.promotionalOffers {
                    offers.append(PromotionalOffer(
                        id: offer.id ?? "unknown",
                        productID: product.id,
                        displayPrice: offer.displayPrice,
                        period: offer.period,
                        periodCount: offer.periodCount,
                        paymentMode: offer.paymentMode
                    ))
                }
            }

            promotionalOffers = offers
        } catch {
            lastError = error
        }
    }

    private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
        switch result {
        case .verified(let safe):
            return safe
        case .unverified(_, let error):
            throw OfferError.verificationFailed(error)
        }
    }
}

// MARK: - Models

/// Represents a win-back offer for a lapsed subscriber.
struct WinBackOffer: Identifiable, Sendable {
    let id: String
    let productID: String
    let displayPrice: String
    let period: Product.SubscriptionPeriod
    let periodCount: Int
    let paymentMode: Product.SubscriptionOffer.PaymentMode

    var displayDescription: String {
        switch paymentMode {
        case .freeTrial:
            return "Free for \(periodCount) \(period.unit.localizedDescription)"
        case .payUpFront:
            return "\(displayPrice) for \(periodCount) \(period.unit.localizedDescription)"
        case .payAsYouGo:
            return "\(displayPrice)/\(period.unit.localizedDescription) for \(periodCount) periods"
        default:
            return displayPrice
        }
    }
}

/// Represents a promotional offer.
struct PromotionalOffer: Identifiable, Sendable {
    let id: String
    let productID: String
    let displayPrice: String
    let period: Product.SubscriptionPeriod
    let periodCount: Int
    let paymentMode: Product.SubscriptionOffer.PaymentMode
}

/// Errors specific to offer operations.
enum OfferError: Error, LocalizedError {
    case productNotFound(String)
    case verificationFailed(Error)
    case noWindowScene

    var errorDescription: String? {
        switch self {
        case .productNotFound(let id):
            return "Product not found: \(id)"
        case .verificationFailed(let error):
            return "Verification failed: \(error.localizedDescription)"
        case .noWindowScene:
            return "No active window scene available"
        }
    }
}

// MARK: - Period Unit Extension

extension Product.SubscriptionPeriod.Unit {
    var localizedDescription: String {
        switch self {
        case .day: return "day"
        case .week: return "week"
        case .month: return "month"
        case .year: return "year"
        @unknown default: return "period"
        }
    }
}

SubscriptionDashboardView.swift

import SwiftUI
import StoreKit

/// Dashboard view showing current subscription status, plan details,
/// renewal information, and management options.
///
/// Usage:
/// ```swift
/// NavigationLink("Subscription") {
///     SubscriptionDashboardView()
/// }
/// ```
struct SubscriptionDashboardView: View {
    @Environment(SubscriptionMonitor.self) private var monitor

    @State private var products: [Product] = []
    @State private var isLoadingProducts = true
    @State private var showUpgradeSheet = false

    var body: some View {
        List {
            currentPlanSection
            statusSection
            managementSection
        }
        .navigationTitle("Subscription")
        .task {
            await loadProducts()
        }
        .sheet(isPresented: $showUpgradeSheet) {
            UpgradePlanSheet(products: products)
        }
    }

    // MARK: - Current Plan

    @ViewBuilder
    private var currentPlanSection: some View {
        Section {
            if let productID = monitor.currentProductID,
               let product = products.first(where: { $0.id == productID }) {
                CurrentPlanRow(product: product, monitor: monitor)
            } else if monitor.state == .notSubscribed {
                NotSubscribedRow()
            } else if case .unknown = monitor.state {
                LoadingRow()
            }
        } header: {
            Text("Current Plan")
        }
    }

    // MARK: - Status

    @ViewBuilder
    private var statusSection: some View {
        Section {
            StatusRow(state: monitor.state)

            if let renewalDate = monitor.renewalDate {
                LabeledContent("Renewal Date") {
                    Text(renewalDate, style: .date)
                }
            }

            LabeledContent("Auto-Renew") {
                Text(monitor.isAutoRenewEnabled ? "On" : "Off")
                    .foregroundStyle(monitor.isAutoRenewEnabled ? .primary : .red)
            }
        } header: {
            Text("Status")
        }
    }

    // MARK: - Management

    @ViewBuilder
    private var managementSection: some View {
        Section {
            if monitor.hasAccess && products.count > 1 {
                Button("Change Plan") {
                    showUpgradeSheet = true
                }
            }

            Button("Manage Subscription") {
                Task { await monitor.openSubscriptionManagement() }
            }

            Button("Restore Purchases") {
                Task {
                    try? await AppStore.sync()
                    await monitor.refreshStatus()
                }
            }
        } header: {
            Text("Manage")
        }
    }

    // MARK: - Data Loading

    private func loadProducts() async {
        // Replace with your actual product IDs
        let productIDs = SubscriptionEntitlement.default.allProductIDs
        do {
            products = try await Product.products(for: productIDs)
            isLoadingProducts = false
        } catch {
            isLoadingProducts = false
        }
    }
}

// MARK: - Row Views

private struct CurrentPlanRow: View {
    let product: Product
    let monitor: SubscriptionMonitor

    var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 4) {
                Text(product.displayName)
                    .font(.headline)
                Text(product.displayPrice + periodSuffix)
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }

            Spacer()

            if monitor.hasAccess {
                Image(systemName: "checkmark.circle.fill")
                    .foregroundStyle(.green)
                    .font(.title2)
            }
        }
        .padding(.vertical, 4)
    }

    private var periodSuffix: String {
        guard let period = product.subscription?.subscriptionPeriod else { return "" }
        switch period.unit {
        case .month: return "/month"
        case .year: return "/year"
        case .week: return "/week"
        case .day: return "/day"
        @unknown default: return ""
        }
    }
}

private struct NotSubscribedRow: View {
    var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 4) {
                Text("No Active Subscription")
                    .font(.headline)
                Text("Subscribe to unlock premium features")
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }

            Spacer()

            Image(systemName: "xmark.circle")
                .foregroundStyle(.secondary)
                .font(.title2)
        }
        .padding(.vertical, 4)
    }
}

private struct LoadingRow: View {
    var body: some View {
        HStack {
            Text("Loading subscription status...")
                .foregroundStyle(.secondary)
            Spacer()
            ProgressView()
        }
    }
}

private struct StatusRow: View {
    let state: SubscriptionState

    var body: some View {
        LabeledContent("Status") {
            HStack(spacing: 6) {
                Circle()
                    .fill(statusColor)
                    .frame(width: 8, height: 8)
                Text(statusLabel)
            }
        }
    }

    private var statusLabel: String {
        switch state {
        case .active:
            return "Active"
        case .inGracePeriod(let days):
            return "Grace Period (\(days) days left)"
        case .inBillingRetry:
            return "Billing Issue"
        case .expired:
            return "Expired"
        case .revoked:
            return "Revoked"
        case .upgraded:
            return "Upgraded"
        case .notSubscribed:
            return "Not Subscribed"
        case .unknown:
            return "Checking..."
        }
    }

    private var statusColor: Color {
        switch state {
        case .active:
            return .green
        case .inGracePeriod, .inBillingRetry:
            return .yellow
        case .expired, .revoked:
            return .red
        case .upgraded:
            return .blue
        case .notSubscribed, .unknown:
            return .secondary
        }
    }
}

// MARK: - Upgrade Plan Sheet

private struct UpgradePlanSheet: View {
    let products: [Product]
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        NavigationStack {
            List(sortedProducts, id: \.id) { product in
                Button {
                    Task {
                        let result = try? await product.purchase()
                        if case .success = result {
                            dismiss()
                        }
                    }
                } label: {
                    HStack {
                        VStack(alignment: .leading) {
                            Text(product.displayName)
                                .font(.headline)
                            Text(product.description)
                                .font(.caption)
                                .foregroundStyle(.secondary)
                        }
                        Spacer()
                        Text(product.displayPrice)
                            .font(.headline)
                    }
                }
            }
            .navigationTitle("Change Plan")
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Cancel") { dismiss() }
                }
            }
        }
    }

    private var sortedProducts: [Product] {
        products.sorted { ($0.price) < ($1.price) }
    }
}

SubscriptionEntitlement.swift

import Foundation

/// Maps subscription product IDs to feature access levels.
///
/// Use this to determine what features a user has access to
/// based on their current subscription product.
///
/// Usage:
/// ```swift
/// let entitlements = SubscriptionEntitlement.default
/// let level = entitlements.accessLevel(for: "com.app.pro.monthly")
/// if level >= .pro {
///     // Grant pro features
/// }
/// ```
struct SubscriptionEntitlement: Sendable {

    /// Access tier levels, ordered from lowest to highest.
    enum AccessLevel: Int, Comparable, Sendable {
        case free = 0
        case basic = 1
        case pro = 2
        case business = 3

        static func < (lhs: AccessLevel, rhs: AccessLevel) -> Bool {
            lhs.rawValue < rhs.rawValue
        }
    }

    /// Mapping of product ID to access level.
    private let productLevels: [String: AccessLevel]

    /// All registered product IDs.
    var allProductIDs: [String] {
        Array(productLevels.keys)
    }

    init(productLevels: [String: AccessLevel]) {
        self.productLevels = productLevels
    }

    /// Get the access level for a product ID.
    ///
    /// - Parameter productID: The StoreKit product identifier.
    /// - Returns: The access level, or `.free` if unknown.
    func accessLevel(for productID: String) -> AccessLevel {
        productLevels[productID] ?? .free
    }

    /// Check if a product ID grants at least the required level.
    ///
    /// - Parameters:
    ///   - productID: The StoreKit product identifier.
    ///   - requiredLevel: Minimum access level needed.
    /// - Returns: `true` if the product grants sufficient access.
    func hasAccess(for productID: String, requiredLevel: AccessLevel) -> Bool {
        accessLevel(for: productID) >= requiredLevel
    }

    /// Determine upgrade/downgrade/crossgrade relationship.
    ///
    /// - Parameters:
    ///   - fromProductID: Current product ID.
    ///   - toProductID: Target product ID.
    /// - Returns: The tier change type.
    func tierChange(from fromProductID: String, to toProductID: String) -> TierChangeType {
        let fromLevel = accessLevel(for: fromProductID)
        let toLevel = accessLevel(for: toProductID)

        if toLevel > fromLevel {
            return .upgrade
        } else if toLevel < fromLevel {
            return .downgrade
        } else {
            return .crossgrade
        }
    }

    /// Types of tier changes.
    enum TierChangeType: Sendable {
        /// Moving to a higher tier (takes effect immediately).
        case upgrade
        /// Moving to a lower tier (takes effect at next renewal).
        case downgrade
        /// Moving to an equivalent tier (e.g., monthly to yearly at same level).
        case crossgrade
    }

    // MARK: - Default Configuration

    /// Default entitlement configuration.
    ///
    /// **Customize these product IDs** to match your App Store Connect configuration.
    static let `default` = SubscriptionEntitlement(productLevels: [
        // Basic tier
        "com.yourapp.basic.monthly": .basic,
        "com.yourapp.basic.yearly": .basic,

        // Pro tier
        "com.yourapp.pro.monthly": .pro,
        "com.yourapp.pro.yearly": .pro,

        // Business tier
        "com.yourapp.business.monthly": .business,
        "com.yourapp.business.yearly": .business,
    ])
}

Source: SKILL.md on GitHub

1 alert16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The 'generators' skill provides a robust set of templates and workflows for generating Swift code for iOS and macOS applications. Security analysis identified a surface for indirect prompt injection because the skill reads local project files to adapt its generation logic. It also utilizes dynamic execution by generating and running local Swift scripts to create application icons and uses shell commands to process images and audit project code. All external references are to well-known, established services.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: MEDIUM · 1 issue

  • Runlayer7mo

    59/179 files flagged

  • ZeroLeaks5mo

    Scan incomplete

Signed by skilld at 30e898f. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 2 months ago
What it can do
Reads files Edits files Runs commands
last_verified
2026-07-16
review_by
2027-06-22
os_version
iOS 27 / macOS 27
All 7 allowed tools
ReadWriteEditGlobGrepBashAskUserQuestion

README badge

README badge for rshankras/claude-code-apple-skills/generators