iOS SDK
Install the RevenueHog Swift package, enable App Attest, and attribute StoreKit 2 purchases to your users automatically with one line of code at app launch.
The Swift SDK is a Swift Package with zero dependencies. It enrolls each install with App Attest, attributes every verified StoreKit 2 transaction automatically, and serves server-driven paywall SKUs.
Requirements
- iOS 15+, macOS 12+, tvOS 15+ or watchOS 8+ (the package manifest platforms).
- Swift tools 5.9.
- The app must already exist in App Store Connect under the key you connected to RevenueHog. The server maps your bundle ID to your workspace through that data. See App Store Connect.
- The App Attest capability on the app target.
Install
Add the package
In Xcode, choose File > Add Package Dependencies…, enter the repository URL and select the RevenueHog product:
https://github.com/Parra-Inc/revenue-hog-sdksThe repository root holds Package.swift, so the URL itself is the package. In a Package.swift project:
.package(url: "https://github.com/Parra-Inc/revenue-hog-sdks", from: "0.2.0")
// target dependency:
.product(name: "RevenueHog", package: "revenue-hog-sdks")Link the SDK to the app target only, not to extensions, widgets or watch targets.
Add the App Attest capability
In Signing & Capabilities, click + Capability and add App Attest. This sets the com.apple.developer.devicecheck.appattest-environment entitlement. Use production as the value: dev-signed builds attest against Apple's sandbox automatically.
Configure at launch
import RevenueHog
@main
struct MyApp: App {
init() {
RevenueHog.configure()
}
var body: some Scene { WindowGroup { ContentView() } }
}In a UIKit app, call it from application(_:didFinishLaunchingWithOptions:). For an app without accounts, this one line is the whole integration.
Identify users (apps with accounts)
// after login
RevenueHog.identify(userId: "user_42")
// on logout
RevenueHog.reset()How auth works
There is no API key. On first launch the SDK:
- Requests a single-use challenge from
POST /api/sdk/v1/attest/challenge. - Generates an App Attest key and asks Apple to attest it against the challenge.
- Sends the attestation to
POST /api/sdk/v1/attest. RevenueHog verifies it, checks that the attested<teamId>.<bundleId>matches an app it already knows from your App Store Connect data, and returns an opaque device token (dt_…).
The token is stored in the Keychain and sent as Authorization: Bearer dt_… on every request after that. It is scoped to one workspace, app and device, is worthless for reading data, and appears under Settings > SDK DEVICES, where you can revoke it.
Details of the enrollment state machine, from the code:
| Situation | What the SDK does |
|---|---|
| Token already in the Keychain | Uses it. No enrollment. |
| Running in the simulator | Skips enrollment; requests go out unattested with reason simulator. |
| Device does not support App Attest | Requests go out unattested with reason unsupported. |
| Apple's service temporarily unavailable | Retries up to 3 times in the session with backoff, keeping the key. |
| Apple rejects the key | Discards it and generates a fresh key once. |
Server answers 404 (app not connected) | Backs off enrollment for 24 hours; requests go out unattested. |
| Any other failure | Unattested for this session; retries next launch. |
A request gets 401 with a token | Clears the token and re-enrolls once per session. A second 401 continues unattested. |
| Enrollment still running when a call is made | The call waits up to 10 seconds for the token, then goes out unattested. |
Unattested requests carry an X-RevenueHog-Unattested header with the reason (pending, unsupported, simulator or enroll-failed). They are accepted on the quarantine path and labeled unverified. Attribution calls also carry the StoreKit 2 signed transaction (jws), which the server verifies independently, so the purchase link is trusted even from an unattested session.
Entitlement self-check
At configure() on a physical device, the SDK checks its own binary for the App Attest entitlement. If it is missing, DEBUG builds hit an assertion (RevenueHog: App Attest entitlement missing. Add the App Attest capability to the app target.) and release builds log the error once and run unattested.
To turn a later deletion of the entitlement into a build error, add a Run Script build phase to app targets that link the SDK:
if ! grep -q "com.apple.developer.devicecheck.appattest-environment" "${CODE_SIGN_ENTITLEMENTS:-/dev/null}"; then
echo "error: RevenueHog: App Attest entitlement missing. Add the App Attest capability to the app target."
exit 1
fiWith ENABLE_USER_SCRIPT_SANDBOXING = YES, declare $(SRCROOT)/$(CODE_SIGN_ENTITLEMENTS) as an input file of the phase, otherwise the sandbox blocks grep and the guard fails on a correct project.
Automatic attribution
When enableAutoAttribution is true (the default), the SDK starts a background task that reads Transaction.currentEntitlements once, then listens to Transaction.updates. For each verified transaction it calls /api/sdk/v1/attribute with:
originalTransactionId:String(transaction.originalID)productId:transaction.productIDjws:result.jwsRepresentation
Unverified transactions are skipped. A transaction already reported for the current user is not re-sent.
API reference
RevenueHog.configure(options:)
public static func configure(options: Options = Options())Sets up the SDK, starts enrollment, replays any queued requests, resolves the StoreKit environment in the background, and starts the transaction listener unless enableAutoAttribution is false. Safe to call again (it reconfigures and restarts the listener). If you never call it, every other method is a logged no-op.
RevenueHog.identify(userId:)
public static func identify(userId: String)Stores userId as the current user, sends /api/sdk/v1/identify with device context, then re-sends every transaction previously reported under a different ID so it moves to this user. An empty or whitespace-only ID is ignored with a warning. Changing the user clears the paywall cache.
RevenueHog.setAttributes(_:)
public static func setAttributes(_ attributes: [String: String])Sends an identify call carrying attributes. The server shallow-merges them into what is stored. The serialized object must be at most 8 KB.
RevenueHog.attributePurchase(originalTransactionId:productId:)
public static func attributePurchase(originalTransactionId: String, productId: String? = nil)Manual attribution, needed only when enableAutoAttribution is false or purchases happen outside StoreKit 2. Pass the original transaction ID, not a renewal's transaction ID. Manual calls carry no JWS.
RevenueHog.reset()
public static func reset()Forgets the user on logout: clears the user ID, the sent-transactions record, the paywall cache and the offline queue, and issues a fresh anonymous ID. Device enrollment and the install ID survive.
RevenueHog.paywall(for:fallback:forceVariant:)
public static func paywall(
for entitlement: String,
fallback: [String],
forceVariant: String? = nil
) async -> PaywallReturns the ordered SKU list for an entitlement. Never throws. Resolution order: fresh cache, a network fetch with a 2.5 second deadline (no enrollment wait), stale cache, then fallback. See Paywall.
RevenueHog.paywallShown(_:rendered:)
public static func paywallShown(_ paywall: Paywall, rendered: [String])Reports that the paywall appeared, with the product IDs that actually rendered (at most 50 are sent). No-op when paywall.experimentId is nil. Sent with up to 2 attempts and never queued.
Options
RevenueHog.configure(options: Options(
baseURL: URL(string: "https://hog.internal.example")!,
enableAutoAttribution: true,
logLevel: .debug
))Prop
Type
Paywall and PaywallSku
Prop
Type
paywall.products() (iOS 15+) loads the products through StoreKit and returns them sorted to menu order. Product.products(for:) returns products unordered, so never render its order directly.
What is stored on the device
| Value | Storage |
|---|---|
| Device token, App Attest key ID, install ID | Keychain, service dev.revenuehog.sdk, non-synchronizable and device-only |
| Anonymous ID, user ID, sent transactions, paywall cache, enrollment backoff | UserDefaults suite dev.revenuehog.sdk |
| Offline queue (cap 100) | Caches/revenuehog/revenuehog-queue.json |
The install ID survives reinstalls because it lives in the Keychain.
What is sent
Your user ID (or the anonymous ID), bundle ID, platform: "ios", OS version, device model, locale, attributes you pass, and the Apple-signed transaction for purchases. On paywall fetches, the install ID and StoreKit's AppTransaction.environment (iOS 16+; omitted until resolved).
Behavior notes
- Requests that fail with a network error,
429or5xxretry up to 3 times with exponential backoff (0.5 s, then 1 s), then queue. Any other non-2xx response is logged and dropped, never retried. - A queued request is replayed on the next launch or after the next successful call.
- Attribution works in the sandbox too.
Troubleshooting
Set logLevel: .debug and watch the [revenuehog] lines. Confirm the App Store Connect key is connected and the app's bundle ID matches exactly. An unknown bundle ID is accepted with 200 and dropped, so a typo looks like success.
Expected in the simulator. On a physical device it usually means the App Attest capability is missing; DEBUG builds assert at launch. Check the signed binary:
codesign -d --entitlements - MyApp.app | grep -A2 appattestCall identify(userId:) after login; earlier purchases re-attribute automatically. If you attribute manually, make sure you send the original transaction ID.
Point Options.baseURL at your deployment. Paths are under /api/sdk/v1/.
Related
Last updated on
SDKs overview
Optional client SDKs for iOS, Android and React Native that link purchases to your own user IDs, add device context and serve paywall SKUs from the dashboard.
Android SDK
Add the RevenueHog Kotlin SDK to an Android app to identify users, attach attributes, map Play purchases to users and serve paywall SKUs from the dashboard.