SDK / iOS

iOS SDK.

StoreKit 2 with async/await. RevFox reports every transaction to your server and tells you what each customer is entitled to. No paywall UI: you build the screen.

01

Requirements

  • iOS 15 or later, Swift 6, Xcode 16 or later.
  • A running RevFox server with an App Store app and its In-App Purchase key. See Dashboard setup.
  • The app's SDK API key (apps_…) from API keys in the panel.
02

Install

With Swift Package Manager, add the package in Xcode under File → Add Package Dependencies, or in Package.swift:

dependencies: [
    .package(url: "https://github.com/revfox-dev/revfox-ios.git", from: "0.1.0")
]

With CocoaPods:

pod 'RevFox', '~> 0.1'
03

Configure

Initialize once, as early in launch as you can, for example in your App initializer or application(_:didFinishLaunchingWithOptions:):

import RevFox

#if DEBUG
RevFox.logLevel = .debug
#endif

try RevFox.shared.initialize(
    RevFoxConfiguration(
        apiKey: "apps_…",
        baseURL: URL(string: "https://api.example.com")!
    )
)
ParameterDefaultDescription
apiKeyrequiredThe app's SDK API key.
baseURLrequiredYour RevFox API server, the api_url of your config. Must be https.
customerIdnilYour user ID, when someone is already signed in at launch. nil starts anonymously.
transactionFinisher.automatic.automatic: RevFox runs purchases and finishes transactions. .manual: observer mode.
allowInsecurefalseAllows an http base URL. For local development only.

initialize throws a RevFoxError for an invalid configuration, or already_initialized when called twice. On launch it starts watching StoreKit, reports anything an earlier run could not, and loads the customer. It re-reads the customer each time the app returns to the foreground.

04

Products and packages

Fetch a package you set up in the panel. RevFox gives its App Store product IDs; titles, prices and offers come from StoreKit.

let package = try await RevFox.shared.fetchPackage(id: "default")
for product in package.products {
    print(product.title, product.displayPrice, product.subscriptionPeriod as Any)
}

Or fetch products by ID. IDs the store does not know are left out, and the order follows ids.

let products = try await RevFox.shared.fetchProducts(ids: ["pro_monthly", "pro_yearly"])
RevFoxProductDescription
id, title, descriptionFrom App Store Connect.
kind.consumable, .nonConsumable, .autoRenewableSubscription or .nonRenewableSubscription.
price, displayPrice, currencyCode, currencySymbolIn the customer's storefront currency.
subscriptionPeriod, subscriptionGroupIdFor subscriptions: a unit (day, week, month, year) and value.
introductoryOffer, promotionalOffersRevFoxOffers with payment mode (free trial, pay as you go, pay up front), price and period. introductoryOffer is only set when the customer is eligible for it, as on Android, so a paywall can show it as is.
isFamilyShareableWhether Family Sharing is on for the product.
packageIdThe package the product was fetched through, or nil.
05

Purchase

switch try await RevFox.shared.purchase(product) {
case .success(let transaction, let customer):
    unlock(customer.benefits)
case .userCancelled:
    break
case .pending:
    // Ask to Buy or a payment that needs action. It arrives later through customerUpdates.
    showPendingMessage()
}

Once Apple approves a purchase, purchase returns .success even if your server could not be reached. The customer then comes from what the device can prove, and the transaction stays unfinished so the next launch reports it. A purchase is never reported as failed because of the network.

  • offer: takes a RevFoxPromoOffer (ID, key ID, nonce, signature, timestamp) that your server signed for a promotional offer.
  • appAccountToken: ties the purchase to your account's UUID. For an anonymous customer it defaults to the generated customer ID, which lets RevFox match store notifications to the customer.
  • Buying a product fetched through a package records that package as presented_package_id on the subscription's webhooks.

It throws purchase_failed when StoreKit fails, transaction_unverified when Apple's signature does not verify, and purchase_unavailable in observer mode.

06

Check benefits

Check benefits, not product IDs, so the products you sell can change without an app update. customerUpdates yields the cached customer first, then every change, from purchases, sign-ins, foreground refreshes and renewals:

for await customer in RevFox.shared.customerUpdates {
    isPro = customer.benefits.contains("pro")
}

RevFox.shared.currentCustomer is the latest snapshot, and currentCustomerId the ID in use.

RevFoxCustomerDescription
id, isAnonymousThe customer RevFox is running as.
benefitsBenefit identifiers active now.
purchasesOne RevFoxPurchaseStatus per subscription or one-time purchase, active or not.
RevFoxPurchaseStatusDescription
id, productId, storeWhich purchase, and from which store.
environment.production or .sandbox.
benefits, isActiveWhat it unlocks, and whether it does now.
willRenew, inGracePeriodAuto-renew status and billing grace period.
ownership.purchased or .familyShared.
purchasedAt, renewedAt, expiresAtFirst purchase, latest renewal, and the end of the current period. expiresAt is nil for lifetime purchases.
unsubscribedAt, billingIssueDetectedAtWhen auto-renew was turned off, and when a billing problem started.
07

Sign in and sign out

// When someone signs in. Anything bought anonymously moves to them.
let customer = try await RevFox.shared.setCustomer(id: "8f14e45f-ceea-4672-9a1f-2a1b0c3d4e5f")

// When they sign out. Benefits stop being reported at once.
try RevFox.shared.clearCustomer()

setCustomer switches only once your server has accepted it. If it throws, the SDK is still the previous customer, and calling it again carries the same purchases over. clearCustomer throws already_anonymous when nobody is signed in. See aliases and transfers.

08

Restore purchases

Apple requires a Restore Purchases button in apps that sell subscriptions or non-consumables. Connect it to:

let customer = try await RevFox.shared.restorePurchases()

It asks StoreKit for the purchases this Apple ID currently owns and links each to the current customer. Family-shared purchases are left with their purchaser. A customer with no purchases means there was nothing to restore. It throws when nothing could be linked, and calling it again is safe. It never finishes transactions.

09

Observer mode

Keep your own StoreKit code and let RevFox verify and record the results. Set transactionFinisher: .manual, then report each verified transaction:

let result = try await product.purchase()
if case .success(let verification) = result {
    try await RevFox.shared.submitTransaction(verification)
    if case .verified(let transaction) = verification {
        await transaction.finish()
    }
}

In observer mode RevFox never finishes a transaction, and purchase(_:) throws purchase_unavailable. RevFox still watches Transaction.updates, so renewals and purchases from other devices are reported, but it leaves your unfinished transactions alone.

10

Attributes

The SDK reports platform, OS, app version, locale, time zone and storefront by itself. Anything personal is sent only when you set it. Setters can be called before initialize; values are saved and sent with the next sync, and nil clears one.

RevFox.shared.setEmail("[email protected]")
RevFox.shared.setDisplayName("Ada Lovelace")
RevFox.shared.setPhoneNumber("+905551234567")
RevFox.shared.setPushToken(deviceToken)          // APNs token Data
RevFox.shared.setAttributes(["plan": "team"])    // custom; keys starting with $ are ignored

RevFox.shared.setMediaSource("Meta")
RevFox.shared.setCampaign("spring_sale")
RevFox.shared.setAdjustID(Adjust.adid())
RevFox.shared.setAppsflyerID(AppsFlyerLib.shared().getAppsFlyerUID())

There are setters for attribution (setMediaSource, setCampaign, setAdGroup, setAd, setKeyword, setCreative) and for the IDs of Adjust, AppsFlyer, Appstack, Facebook, Firebase, Kochava, Singular, Tenjin, Airship, Amplitude, Mixpanel, mParticle, OneSignal, CleverTap, PostHog, Customer.io, Iterable and Braze.

Device identifiers

collectDeviceIdentifiers() sends the IDFV, the device model and OS version, and has your server record the IP address. When the app links AdSupport and the user allowed tracking, it also sends the IDFA and the App Tracking Transparency answer. RevFox never shows the tracking prompt.

Setting an attribution tool's install ID, such as Adjust's or AppsFlyer's, also sends the IDFV, since those tools match installs by it. Set automaticDeviceIdentifierCollectionEnabled = false to stop that.

Apple Search Ads

RevFox.shared.enableAdServicesAttributionTokenCollection()

Call it on the first launch. The SDK sends Apple's AdServices token once per install, and RevFox records the campaign, ad group and keyword once Apple Search Ads is enabled under Integrations. It needs no tracking consent.

11

Logging

RevFox.logLevel = .debug   // .verbose, .debug, .info (default), .warn, .error
RevFox.logHandler = { level, message in
    MyLogger.log("[RevFox] \(level): \(message)")
}
12

Errors

Every failure is a RevFoxError with a stable code to branch on, a message for people, and userInfo with details.

CodeMeaning
not_initialized, already_initializedinitialize has not been called, or was called twice.
invalid_api_key, invalid_base_url, insecure_url, invalid_customer_idThe configuration or customer ID is invalid.
store_products_unavailable, product_not_foundStoreKit could not load products, or does not know one.
package_not_foundNo package with that identifier for this app.
purchase_failed, purchase_unavailableStoreKit failed the purchase, or purchase was called in observer mode.
transaction_unverifiedApple's signature on the transaction did not verify. It is left unfinished.
already_anonymousclearCustomer was called with nobody signed in.
network_failure, invalid_responseYour server could not be reached, or answered with something unexpected.
keychain_errorThe SDK could not read or write its keychain items.
13

Privacy

The SDK ships a privacy manifest for what it collects itself: purchase history, not linked to the user and not used for tracking, under a random customer ID. Xcode adds it to your app's privacy report. Declare what you pass in yourself in App Store Connect:

CallApp Store privacy category
customerId or setCustomer(id:) with your user IDsIdentifiers → User ID; mark Purchase History as linked to the user.
collectDeviceIdentifiers(), attribution install IDsIdentifiers → Device ID. With AdSupport and consent, also the IDFA, used for tracking.
setEmail, setDisplayName, setPhoneNumberContact Info → Email Address, Name, Phone Number.
setPushTokenIdentifiers → Device ID.
Attribution setters, enableAdServicesAttributionTokenCollectionUsage Data → Advertising Data.
Integration IDsIdentifiers → User ID or Device ID.

Also shipping on Android?

The Android SDK works the same way.

Android SDK