Purchases

The SDK allows applications to synchronize subscription status after successful purchases and subscription state changes.

Always update subscriptionStatus and call the appropriate tracking logic after a successful purchase.

Subscription status synchronization

Use the subscriptionStatus property to provide the current subscription state.

import UIKit
import Magify

class ViewController: UIViewController {

    func handleSubscription() {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            return
        }

        appDelegate.magify.subscriptionStatus = .active(isTrial: false)
    }
}

Update the subscription status whenever the user's subscription becomes active, expires, or changes state. Available states:

  • .active(isTrial:) — an active subscription (isTrial: true while in a trial).
  • .expired(isTrial:) — a previously active subscription that has lapsed (maps to paid_cancelled / trial_cancelled in analytics). Use this for churned subscribers, not .inactive.
  • .inactive — the user has never had a subscription.

Purchase flow

After a successful purchase, synchronize the subscription state with the SDK.

import UIKit
import Magify

class ViewController: UIViewController {

    func handlePurchase() {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            return
        }
    }
}

Tracking in-app purchases

Call trackInApp after a successful StoreKit transaction. Pass the price as a Decimal or as a pre-formatted string.

// Decimal overload — SDK converts to string internally
magify.trackInApp(
    productId: product.productIdentifier,
    price: Decimal(string: product.price.stringValue) ?? 0,
    currency: product.priceLocale.currencyCode ?? "USD",
    transactionId: transaction.transactionIdentifier,
    originalTransactionId: transaction.original?.transactionIdentifier
)

// String overload — supply needValidation: false to skip server-side validation
magify.trackInApp(
    productId: product.productIdentifier,
    priceString: product.localizedPrice,
    currency: "USD",
    transactionId: transaction.transactionIdentifier,
    originalTransactionId: nil,
    needValidation: false
)

Tracking subscription activations

Call trackSubscriptionActivation after a new subscription purchase or trial start.

magify.trackSubscriptionActivation(
    isTrial: false,
    productId: "com.example.premium_monthly",
    priceString: "9.99",
    currency: "USD",
    period: "P1M",
    transactionId: transaction.transactionIdentifier,
    originalTransactionId: transaction.original?.transactionIdentifier
    // needValidation defaults to true
)

Set needValidation: false to skip the receipt upload step — for example when validation is handled on your backend.

Server-side validation

The SDK automatically uploads the App Store receipt for server-side validation after every trackInApp or trackSubscriptionActivation call where needValidation is true (the default).

Enable receipt observation at init time so the SDK also processes background transactions:

let magify = MagifyClient(
    for: "MyApp",
    defaultConfigURL: configURL,
    isSandbox: false,
    receiptObservationEnabled: true   // default — observes StoreKit queue
)

Set receiptObservationEnabled: false if you drive all validation through trackInApp/trackSubscriptionActivation manually.

External purchases

Use trackExternalInApp and trackExternalSubscriptionActivation for purchases that originate outside the App Store (e.g. web, promotional credits).

// External one-time purchase
magify.trackExternalInApp(
    productId: "web_purchase_gold",
    priceString: "4.99",
    currency: "USD",
    transactionId: "web-txn-123",
    originalTransactionId: nil
)

// External subscription
magify.trackExternalSubscriptionActivation(
    isTrial: false,
    productId: "web_premium_monthly",
    priceString: "9.99",
    currency: "USD",
    period: nil,
    transactionId: "web-sub-456",
    originalTransactionId: nil
)

Both methods accept needValidation: Bool = true to control whether a receipt upload is triggered.

Trusted purchases

Use trusted purchases when your server has already validated the transaction and produced a signed record. The SDK skips re-validation and forwards the record directly.

TrustedPurchaseRecord

Build a TrustedPurchaseRecord from your server's response:

let record = TrustedPurchaseRecord(
    productId: "com.example.premium_monthly",
    transactionId: "70001234567890",
    originalTransactionId: "70001234567890",
    purchasedAt: 1700000000.0,          // Unix timestamp (seconds)
    price: "9.99",
    currency: "USD",
    commissionAmount: "3.00",
    commissionCurrency: "USD",
    storeFront: "US",                    // ISO 3166-1 alpha-2, optional
    storeName: .appStore,
    type: .initialPurchase,
    periodType: .normal,
    isTrialConversion: nil,
    productIdType: .subscription,
    environment: .production             // defaults to .production
)

Enums

Sending a trusted record

trackTrustedPurchase(_:isSubscription:isExternal:) routes the record to the correct event type:

// App Store in-app purchase (already validated server-side)
magify.trackTrustedPurchase(record)

// App Store subscription
magify.trackTrustedPurchase(record, isSubscription: true)

// External in-app purchase
magify.trackTrustedPurchase(record, isExternal: true)

// External subscription
magify.trackTrustedPurchase(record, isSubscription: true, isExternal: true)

When both isSubscription and isExternal are nil (the default), the SDK sends the record to the server for its own internal processing without emitting an analytics event. It does not update the local purchased-product registry, so inAppStatus, campaign limits, and purchasedProductIds are unaffected by this call.

Restoring purchases

Call the restore helpers after a StoreKit restore flow completes to update the SDK's internal purchase registry:

// Restore a previously purchased consumable / non-consumable
magify.trackRestoredInApp(productId: "com.example.gold_pack")

// Restore a subscription
magify.trackRestoredSubscription(productId: "com.example.premium_monthly")

These methods do not trigger server-side validation. Call them for every product that appears in the restored transactions list. Both methods update the SDK's local purchased-product registry, which affects inAppStatus, purchasedProductIds, and campaign targeting.

Custom purchase verification

Implement IPurchaseVerificationHandler to intercept validation results from the Magify server and control retry behavior.

import Magify

final class MyVerificationHandler: IPurchaseVerificationHandler {
    func handlePurchaseVerification(_ result: PurchaseVerificationResult) -> RepeatState? {
        print("Verified \(result.productId) — code: \(result.code)")

        switch result.code {
        case .success:
            return .finish    // validation succeeded — remove from queue
        case .invalid, .invalidCredentials:
            return .finish    // invalid receipt — stop retrying
        case .cancelled:
            return .retry     // server cancelled — retry immediately
        case .fail, .doesntSupport:
            return .wait      // transient error — defer to next flush tick
        }
    }
}

Register the handler at init time or after initialization:

// At init
let magify = MagifyClient(
    for: "MyApp",
    defaultConfigURL: configURL,
    isSandbox: false,
    purchaseHandler: MyVerificationHandler()
)

// Or later
magify.purchaseHandler = MyVerificationHandler()

PurchaseVerificationResult

PurchaseVerificationResultCode

RepeatState

Return nil to let the SDK apply its default handling.

Next step

For application-level event tracking, see the Analytics section.

Related articles

PurchaseInfo

Advanced

ICampaignHandler

MagifyPresenter

SubscriptionService

Aghanim purchases