RevenueHogdocs
SDKs

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-sdks

The repository root holds Package.swift, so the URL itself is the package. In a Package.swift project:

Package.swift
.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

MyApp.swift
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:

  1. Requests a single-use challenge from POST /api/sdk/v1/attest/challenge.
  2. Generates an App Attest key and asks Apple to attest it against the challenge.
  3. 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:

SituationWhat the SDK does
Token already in the KeychainUses it. No enrollment.
Running in the simulatorSkips enrollment; requests go out unattested with reason simulator.
Device does not support App AttestRequests go out unattested with reason unsupported.
Apple's service temporarily unavailableRetries up to 3 times in the session with backoff, keeping the key.
Apple rejects the keyDiscards 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 failureUnattested for this session; retries next launch.
A request gets 401 with a tokenClears the token and re-enrolls once per session. A second 401 continues unattested.
Enrollment still running when a call is madeThe 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
fi

With 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.productID
  • jws: 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 -> Paywall

Returns 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

ValueStorage
Device token, App Attest key ID, install IDKeychain, service dev.revenuehog.sdk, non-synchronizable and device-only
Anonymous ID, user ID, sent transactions, paywall cache, enrollment backoffUserDefaults 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, 429 or 5xx retry 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

Last updated on

On this page