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.
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'
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")!
)
)
| Parameter | Default | Description |
|---|---|---|
apiKey | required | The app's SDK API key. |
baseURL | required | Your RevFox API server, the api_url of your config. Must be https. |
customerId | nil | Your 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. |
allowInsecure | false | Allows 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.
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"])
RevFoxProduct | Description |
|---|---|
id, title, description | From App Store Connect. |
kind | .consumable, .nonConsumable, .autoRenewableSubscription or .nonRenewableSubscription. |
price, displayPrice, currencyCode, currencySymbol | In the customer's storefront currency. |
subscriptionPeriod, subscriptionGroupId | For subscriptions: a unit (day, week, month, year) and value. |
introductoryOffer, promotionalOffers | RevFoxOffers 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. |
isFamilyShareable | Whether Family Sharing is on for the product. |
packageId | The package the product was fetched through, or nil. |
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 aRevFoxPromoOffer(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_idon 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.
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.
RevFoxCustomer | Description |
|---|---|
id, isAnonymous | The customer RevFox is running as. |
benefits | Benefit identifiers active now. |
purchases | One RevFoxPurchaseStatus per subscription or one-time purchase, active or not. |
RevFoxPurchaseStatus | Description |
|---|---|
id, productId, store | Which purchase, and from which store. |
environment | .production or .sandbox. |
benefits, isActive | What it unlocks, and whether it does now. |
willRenew, inGracePeriod | Auto-renew status and billing grace period. |
ownership | .purchased or .familyShared. |
purchasedAt, renewedAt, expiresAt | First purchase, latest renewal, and the end of the current period. expiresAt is nil for lifetime purchases. |
unsubscribedAt, billingIssueDetectedAt | When auto-renew was turned off, and when a billing problem started. |
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.
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.
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.
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.
Logging
RevFox.logLevel = .debug // .verbose, .debug, .info (default), .warn, .error
RevFox.logHandler = { level, message in
MyLogger.log("[RevFox] \(level): \(message)")
}
Errors
Every failure is a RevFoxError with a stable code to branch on, a message for people, and userInfo with details.
| Code | Meaning |
|---|---|
not_initialized, already_initialized | initialize has not been called, or was called twice. |
invalid_api_key, invalid_base_url, insecure_url, invalid_customer_id | The configuration or customer ID is invalid. |
store_products_unavailable, product_not_found | StoreKit could not load products, or does not know one. |
package_not_found | No package with that identifier for this app. |
purchase_failed, purchase_unavailable | StoreKit failed the purchase, or purchase was called in observer mode. |
transaction_unverified | Apple's signature on the transaction did not verify. It is left unfinished. |
already_anonymous | clearCustomer was called with nobody signed in. |
network_failure, invalid_response | Your server could not be reached, or answered with something unexpected. |
keychain_error | The SDK could not read or write its keychain items. |
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:
| Call | App Store privacy category |
|---|---|
customerId or setCustomer(id:) with your user IDs | Identifiers → User ID; mark Purchase History as linked to the user. |
collectDeviceIdentifiers(), attribution install IDs | Identifiers → Device ID. With AdSupport and consent, also the IDFA, used for tracking. |
setEmail, setDisplayName, setPhoneNumber | Contact Info → Email Address, Name, Phone Number. |
setPushToken | Identifiers → Device ID. |
Attribution setters, enableAdServicesAttributionTokenCollection | Usage Data → Advertising Data. |
| Integration IDs | Identifiers → User ID or Device ID. |
