Subscription

This guide covers subscription purchasing, offers, and ongoing subscription management in your app.

iOS Commitment Billing Plans#

StoreKit 26.4 supports auto-renewable subscriptions that can be paid monthly while committing the customer to a longer period, such as a 12-month commitment, or paid up front for the full subscription period. OpenIAP exposes these iOS billing plans without changing the Android subscription-offer flow.

  • Fetch products first and read pricingTermsIOS to show the available monthly and up-front plans.
  • Pass billingPlanType: 'monthly' when the user chooses monthly billing with commitment.
  • Read PurchaseIOS.billingPlanTypeIOS and PurchaseIOS.commitmentInfoIOS after purchase to record the selected commitment.
  • Use RenewalInfoIOS.renewalBillingPlanType and RenewalInfoIOS.commitmentInfo for upcoming commitment renewal state when StoreKit returns it.
OpenIAP fieldUse it for
ProductSubscriptionIOS.pricingTermsIOSDisplaying StoreKit billing choices before purchase.
RequestSubscriptionIosProps.billingPlanTypeSelecting monthly or up-front.
PurchaseIOS.billingPlanTypeIOSPersisting the plan selected on the completed transaction.
PurchaseIOS.commitmentInfoIOSTracking current billing period, total periods, expiration, and commitment price.
RenewalInfoIOS.commitmentInfoInspecting the next commitment renewal state.
swift
import OpenIap

try await OpenIapModule.shared.requestPurchase(
    RequestPurchaseProps(
        request: .subscription(
            RequestSubscriptionPropsByPlatforms(
                apple: RequestSubscriptionIosProps(
                    sku: "premium_annual",
                    billingPlanType: .monthly
                )
            )
        ),
        type: .subs
    )
)

Subscription State#

After purchase, use Active Subscriptions to check which product or plan is currently active across Apple, Google Play, Meta Horizon, Amazon Fire OS, and Vega OS. That page explains how OpenIAP normalizes store-specific subscription group behavior into productId, currentPlanId, and purchase-token based entitlement checks.

Subscription Offers#

Subscription offers represent different pricing plans for the same subscription product:

  • Base Plan: The standard pricing for a subscription
  • Introductory Offers: Special pricing for new subscribers (free trial, discounted period)
  • Promotional Offers: Limited-time discounts configured in the app stores

Platform Differences

PlatformBehavior
iOSBase plan is used by default. Introductory offers are automatically applied when eligible. Promotional offers require server-side signature via withOffer.
AndroidSubscription offers are required when purchasing. You must pass subscriptionOffers with offer tokens from fetchProducts().

Platform Implementation

iOS Overview#

iOS handles subscription offers differently - the base plan is used by default, and promotional offers are optional.

  • Introductory Offers: Automatically applied when user is eligible (no code needed)
  • Promotional Offers: Requires server-side signature generation

Fetching Subscription Info#

swift
import OpenIap

let iapStore = OpenIapStore()

// Fetch subscription products
try await iapStore.fetchProducts(skus: ["premium_monthly"], type: .subs)

let subscription = iapStore.iosSubscriptionProducts.first { $0.id == "premium_monthly" }

// Check for introductory offer
if let introOffer = subscription?.subscriptionOffers?.first(where: {
    $0.type == .introductory
}) {
    print("Intro offer: \(introOffer.displayPrice)")
    print("Payment mode: \(introOffer.paymentMode)")
    print("Period: \(String(describing: introOffer.period))")
}

// Check for promotional offers
if let offers = subscription?.subscriptionOffers {
    for offer in offers where offer.type == .promotional {
        print("Promo: \(offer.id) - \(offer.localizedPriceIOS ?? offer.displayPrice)")
    }
}

Introductory Offers#

iOS automatically applies introductory prices (free trials, intro pricing) configured in App Store Connect. No additional code is needed - users will see the introductory offer when eligible.

swift
func displayIntroOffer(_ subscription: ProductSubscriptionIOS) -> String? {
    guard let offer = subscription.subscriptionOffers?.first(where: {
        $0.type == .introductory
    }) else {
        return nil
    }

    return offer.displayPrice
}

// Check eligibility
let isEligible = try await iapStore.isEligibleForIntroOfferIOS(sku: "premium_monthly")
if isEligible, let offerText = displayIntroOffer(subscription) {
    print(offerText)
}

Promotional Offers#

Promotional offers require server-side signature generation. These offers are for existing or lapsed subscribers. Pass the selected SubscriptionOffer.id as offerId when requesting the signature.

swift
import OpenIap

func purchaseWithPromoOffer(
    subscriptionId: String,
    offerId: String
) async throws {
    // 1. Generate signature on your backend
    let nonce = UUID().uuidString
    let timestamp = Int64(Date().timeIntervalSince1970 * 1000)

    let signatureResponse = try await generateSignatureOnServer(
        productId: subscriptionId,
        offerId: offerId,
        nonce: nonce,
        timestamp: timestamp
    )

    // 2. Purchase with the promotional offer
    _ = try await iapStore.requestPurchase(
        sku: subscriptionId,
        type: .subs,
        withOffer: DiscountOfferInputIOS(
            identifier: offerId,
            keyIdentifier: signatureResponse.keyIdentifier,
            nonce: nonce,
            signature: signatureResponse.signature,
            timestamp: timestamp
        ),
        autoFinish: false
    )
}

Purchase Subscription#

For iOS, simply request the purchase. Introductory offers are applied automatically when eligible.

swift
import OpenIap

func purchaseSubscription(subscriptionId: String) async throws {
    // iOS: Simply request purchase
    // Intro offer is applied automatically when eligible
    _ = try await iapStore.requestPurchase(
        sku: subscriptionId,
        type: .subs,
        autoFinish: false
    )
}

Handling Subscription#

After a successful subscription purchase, you need to handle the subscription lifecycle including verification, status checking, and renewal management.

iOS vs Android#

AspectiOSAndroid
Purchase DataCan get purchase data from Transaction.all (including expired). Client can check expiry/renewal info.Client cannot access expiry time. Must use Google Play Developer API for subscription status.
Cancellation StatusrenewalInfo.willAutoRenew available client-sideNo client-side API. Must use Google Play Developer API to detect cancellation.
Server ValidationRecommended for securityMandatory for proper subscription management

Subscription Scenarios#

Understanding how subscriptions behave in different scenarios is crucial for proper implementation:

Cancellation Scenario

  1. User cancels subscription on Day 1
  2. Subscription remains valid until Day 30 (end of billing period)
  3. getAvailablePurchases() still returns this purchase
  4. iOS: renewalInfo.willAutoRenew = false (client-side)
    Android: Must check via Google Play Developer API (server-side only)

Restore Purchase Scenarios

ScenarioResult
Day 15 (still valid)Purchase returned, access granted until Day 30 ✓
Day 35 (expired)iOS: currentEntitlements returns empty
Android: queryPurchases returns empty
No access granted ✓

Refund Scenario (Tricky Case)

When to Validate#

Server-side validation is needed:

  • After purchase - Verify purchase is legitimate. Consider using IAPKit to make your life easier - it provides backend verification with minimal setup via verifyPurchaseWithProvider.
  • On restore - Check current status (active/cancelled/refunded/expired). See Check Subscription Status for platform-specific implementation details.
  • Periodically - Detect refunds and cancellations for active subscriptions

Verify Subscription#

Always verify subscription purchases with your backend or IAPKit before granting access to premium content.

swift
import OpenIap

func verifySubscription(_ purchase: PurchaseIOS) async -> Bool {
    do {
        return try await yourBackend.verifyAppleSubscription(
            productId: purchase.productId,
            jws: purchase.purchaseToken ?? ""
        )
    } catch {
        print("Verification error: \(error.localizedDescription)")
        return false
    }
}

Check Active Subscriptions#

Check if the user has an active subscription to determine access.

swift
import OpenIap

let iapStore = OpenIapStore()

// Check if user has any active subscription
let hasActive = try await iapStore.hasActiveSubscriptions()
if hasActive {
    print("User has premium access")
}

// Get all active subscriptions
try await iapStore.getActiveSubscriptions()
let activeSubscriptions = iapStore.activeSubscriptions
for subscription in activeSubscriptions {
    print("Active subscription: \(subscription.productId)")
    if let expiration = subscription.expirationDateIOS {
        print("Expires: \(expiration)")
    }
}

Check Subscription Status (Active/Cancelled/Refunded/Expired)#

After restoring purchases or checking subscriptions, you need to determine the current status. Each platform handles this differently.

iOS: Using subscriptionStatusIOS

iOS provides detailed subscription status through subscriptionStatusIOS() which returns the StoreKit 2 subscription state.

StateDescriptionUser Access
subscribedActive subscription✅ Grant access
expiredSubscription has expired❌ Deny access
revokedRefunded by Apple❌ Deny access
inGracePeriodBilling failed but grace period active✅ Grant access (temporary)
inBillingRetryPeriodBilling retry in progress⚠️ Consider granting access
swift
import OpenIap

let iapStore = OpenIapStore()

// Method 1: Using subscriptionStatusIOS for detailed state
func checkSubscriptionStatus(sku: String) async throws -> (hasAccess: Bool, status: String) {
    let statuses = try await iapStore.subscriptionStatusIOS(sku: sku)

    for status in statuses {
        switch status.state {
        case "subscribed":
            print("✅ Active subscription")
            return (true, "active")

        case "expired":
            print("❌ Subscription expired")
            return (false, "expired")

        case "revoked":
            print("💰 Subscription was refunded")
            return (false, "refunded")

        case "inGracePeriod":
            print("⚠️ Billing issue - grace period active")
            return (true, "grace_period")

        case "inBillingRetryPeriod":
            print("🔄 Billing retry in progress")
            return (true, "billing_retry")

        default:
            continue
        }
    }

    return (false, "unknown")
}

// Method 2: Using ActiveSubscription for quick checks
func checkFromActiveSubscriptions() async throws {
    try await iapStore.getActiveSubscriptions()
    let subs = iapStore.activeSubscriptions

    for sub in subs {
        let renewalInfo = sub.renewalInfoIOS

        // Check if cancelled (will not auto-renew)
        let isCancelled = renewalInfo?.willAutoRenew == false

        // Check expiration reason
        let expirationReason = renewalInfo?.expirationReason

        // Check if expired
        let isExpired: Bool
        if let expiry = sub.expirationDateIOS {
            isExpired = expiry < Double(Date().timeIntervalSince1970 * 1000)
        } else {
            isExpired = false
        }

        print("Product: \(sub.productId)")
        print("  Active: \(sub.isActive)")
        print("  Cancelled: \(isCancelled)")
        print("  Expired: \(isExpired)")
        if let reason = expirationReason {
            print("  Expiration Reason: \(reason)")
        }
    }
}

For the complete list of subscription state values and expiration reasons, see SubscriptionStatusIOS in the Types reference.

Summary: Status Check Comparison

StatusiOS (Client)Android (Client)Android (Server)
Activestate == "subscribed"Purchase exists ⚠️expiryTimeMillis > now
CancelledwillAutoRenew == falseNot available ❌cancelReason is set
Expiredstate == "expired"Purchase not returnedexpiryTimeMillis < now
Refundedstate == "revoked"Not available ❌Check refund endpoint
Grace Periodstate == "inGracePeriod"Not available ❌expiryTimeMillis extended

Manage Subscriptions#

Allow users to manage their subscriptions through the platform's native UI.

swift
import OpenIap

let iapStore = OpenIapStore()

// Open subscription management page
func manageSubscriptions() async {
    try await iapStore.deepLinkToSubscriptionsIOS()
}

See Also#

Native References#