Rovenue SDK
Reference

Methods

Every Rovenue SDK method, grouped by capability.

Quick navigation: Configuration · Identity · Entitlements · Virtual Currencies · Events · Purchases · Funnels · Remote Config · Subscriber Attributes · Account Token · Lifecycle · Logging · Change Stream · Funnel Attribution


Configuration

configure

Initialise the SDK. Call this once at app startup, before any other Rovenue call. On React Native, pass a single config object. On Swift and Kotlin, use labelled parameters.

Params

ParameterTypeRequiredDescription
apiKeystringYesYour project's public API key. Prefix rov_pub_.
baseUrlstringNoAPI endpoint. Defaults to the hosted endpoint https://api.rovenue.io; override only when self-hosting (https://; http:// allowed for localhost only).
logLevelstringNoVerbosity of SDK logs: off / error / warn / info / debug / trace. Defaults to warn.
appVersionstringNoApp version string forwarded to the server for analytics. Defaults to the value reported by the OS.

Returns — void

Throws — InvalidApiKey if the key format is invalid at call time (a malformed prefix). The key is not validated against the server until the first network request.

import { Rovenue } from '@rovenue/react-native-sdk';

Rovenue.configure({
  apiKey: 'rov_pub_…',
  logLevel: 'info',
  appVersion: '3.1.0',
  // baseUrl: 'https://api.acme.com', // optional — only when self-hosting
});
import Rovenue

Rovenue.configure(
    apiKey: "rov_pub_…",
    logLevel: .info,
    appVersion: "3.1.0"
    // baseUrl: "https://api.acme.com"  // optional — only when self-hosting
)
import dev.rovenue.sdk.Rovenue

Rovenue.configure(
    apiKey = "rov_pub_…",
    logLevel = LogLevel.INFO,
    appVersion = BuildConfig.VERSION_NAME
    // baseUrl = "https://api.acme.com"  // optional — only when self-hosting
)

configure must be called before any other Rovenue method. This is a programming error to prevent by call-site ordering, not a runtime error to catch: on iOS, accessing the SDK before configure is a hard crash (fatalError, not a throwable error — no catch anywhere can intercept it); on Android, it throws a plain IllegalStateException that is not a RovenueException. See Calling the SDK before configure() for the full per-platform breakdown, including React Native and Flutter.

See also: Configuring the SDK guide


Identity

Method summary

MethodSignature (RN)ReturnsThrows
currentUsercurrentUser()User—
identifyidentify(appUserId)voidInvalidArgument, SubscriberUnresolvable
logOutlogOut()void—

currentUser

Synchronous cache read — no network call. Returns the User for the current install.

Params — none

Returns

type User = {
  rovenueId: string;      // always present, prefix "rov_"
  appUserId: string | null; // null until identify() has been called
};

→ Full type reference: User

Throws — nothing

import { Rovenue } from '@rovenue/react-native-sdk';

const user = await Rovenue.currentUser();
console.log(user.rovenueId);  // e.g. "rov_01abc..."
console.log(user.appUserId);  // null until identify() called
let user = await Rovenue.shared.currentUser()
print(user.rovenueId)   // "rov_01abc..."
print(user.appUserId ?? "anonymous")
val user = Rovenue.shared.currentUser()
Log.d("App", user.rovenueId)         // "rov_01abc..."
Log.d("App", user.appUserId ?: "anonymous")

identify

Bind a backend user ID to the current install. Rovenue attaches the appUserId label to the rovenueId and, if that appUserId already exists on another install, transfers that install's purchases and credits to this device. The call is optimistic — local state updates immediately; the server sync completes in the background.

Params

ParameterTypeRequiredDescription
appUserIdstringYesYour backend's stable user identifier. Must be non-empty.

Returns — void

Throws — InvalidArgument if appUserId is empty or blank; SubscriberUnresolvable if the server cannot resolve this install's subscriber because its merge history is corrupt. In that case the local link is still written and stays unsynced, and it is sent once the project owner repairs the data. Every other server failure (offline, 5xx) is retried in the background and not thrown.

// After your auth flow resolves:
await Rovenue.identify('user_123');
try await Rovenue.shared.identify("user_123")
Rovenue.shared.identify("user_123") // suspend fun

Use opaque, unguessable IDs — never an email address or username. Because identify() can transfer purchases between installs, a guessable ID is an account-takeover surface. See the Identifying users guide for the recommended token-based flow.

Errors: InvalidArgument


logOut

Mint a fresh anonymous rovenueId, clear appUserId, and drop all scope-bound caches. Call when your user signs out so the next user on a shared device starts clean.

Params — none

Returns — void

Throws — nothing

await Rovenue.logOut();
try await Rovenue.shared.logOut()
Rovenue.shared.logOut() // suspend fun

See also: Identifying users guide, Identity consent guide


Entitlements

Method summary

MethodSignature (RN)ReturnsThrows
entitlemententitlement(id)Entitlement | null—
entitlementsAllentitlementsAll()Entitlement[]—
refreshEntitlementsrefreshEntitlements()voidNetworkUnavailable, ServerError

entitlement() and entitlementsAll() are synchronous cache reads — instant, no network. The cache is populated on configure and kept current by the change stream. Call refreshEntitlements() to force a server round-trip (e.g. after a server-side grant).


entitlement

Return a single cached entitlement by its dashboard identifier, or null if the user holds no such entitlement.

Params

ParameterTypeRequiredDescription
idstringYesThe entitlement key as configured in the Rovenue dashboard (e.g. "pro", "credits_pack").

Returns

// Entitlement | null

→ Full type reference: Entitlement

Throws — nothing

const ent = await Rovenue.entitlement('pro');
if (ent?.isActive) {
  // unlock Pro features
}
let ent = await Rovenue.shared.entitlement(id: "pro")
if ent?.isActive == true {
    // unlock Pro features
}
val ent = Rovenue.shared.entitlement(id = "pro")
if (ent?.isActive == true) {
    // unlock Pro features
}

entitlementsAll

Return every cached entitlement for the current user.

Params — none

Returns

// Entitlement[]

→ Full type reference: Entitlement

Throws — nothing

const all = await Rovenue.entitlementsAll();
const active = all.filter(e => e.isActive);
let all = await Rovenue.shared.entitlementsAll()
let active = all.filter { $0.isActive }
val all = Rovenue.shared.entitlementsAll()
val active = all.filter { it.isActive }

refreshEntitlements

Force a server round-trip to refresh the entitlement cache. Fires an entitlements-changed ChangeEvent (RN: 'ENTITLEMENTS_CHANGED'; Swift: .entitlementsChanged; Kotlin: ChangeEvent.ENTITLEMENTS_CHANGED) when the cache is updated.

Params — none

Returns — void

Throws — NetworkUnavailable, ServerError

// After a server-side entitlement grant, force-refresh:
await Rovenue.refreshEntitlements();
try await Rovenue.shared.refreshEntitlements()
Rovenue.shared.refreshEntitlements() // suspend fun

Errors: NetworkUnavailable · ServerError

See also: Entitlements guide


Virtual Currencies

Method summary

MethodSignature (RN)ReturnsThrows
virtualCurrenciesvirtualCurrencies()Promise<Record<string, number>>—
virtualCurrencyvirtualCurrency(code)Promise<number>—
refreshVirtualCurrenciesrefreshVirtualCurrencies()voidNetworkUnavailable, ServerError

React Native also exposes useVirtualCurrencies() and useVirtualCurrency(code) hooks. Swift uses virtualCurrencyBalances() -> [String: Int64] and virtualCurrency(_ code:) -> Int64. Kotlin uses virtualCurrencyBalances(): Map<String, Long> and virtualCurrency(code): Long.

virtualCurrencies() and virtualCurrency(code) resolve immediately from the local cache (no network round-trip), but are async — await them. refreshVirtualCurrencies() forces a server round-trip. Spending virtual currency is server-side only — call POST /v1/virtual-currencies/:appUserId/:code/transactions from your backend using your secret API key.


virtualCurrencies

Return the cached virtual-currency balances for the current user as a map of currency code to amount.

Params — none

Returns — Promise<Record<string, number>> (resolves to empty object if not yet loaded)

Throws — nothing

const balances = await Rovenue.virtualCurrencies();
console.log('COINS:', balances['COINS'] ?? 0);
let balances = await Rovenue.shared.virtualCurrencyBalances()
print("COINS:", balances["COINS"] ?? 0)
val balances = Rovenue.shared.virtualCurrencyBalances()
Log.d("App", "COINS: ${balances["COINS"] ?: 0}")

virtualCurrency

Return the cached balance for a single currency code. Returns 0 if the currency is not present.

Params

ParameterTypeRequiredDescription
codestringYesThe currency code as configured in the Rovenue dashboard (e.g. "COINS", "GEMS").

Returns — Promise<number> (resolves to 0 if absent or not yet loaded)

Throws — nothing

const coins = await Rovenue.virtualCurrency('COINS');
console.log(`You have ${coins} coins`);
let coins = await Rovenue.shared.virtualCurrency("COINS")
print("You have \(coins) coins")
val coins = Rovenue.shared.virtualCurrency("COINS")
Log.d("App", "You have $coins coins")

refreshVirtualCurrencies

Force a server round-trip to refresh the virtual-currency balance cache. Fires a virtual-currencies-changed ChangeEvent (RN: 'VIRTUAL_CURRENCIES_CHANGED'; Swift: .virtualCurrenciesChanged; Kotlin: ChangeEvent.VIRTUAL_CURRENCIES_CHANGED) when the cache is updated.

Params — none

Returns — void

Throws — NetworkUnavailable, ServerError

await Rovenue.refreshVirtualCurrencies();
try await Rovenue.shared.refreshVirtualCurrencies()
Rovenue.shared.refreshVirtualCurrencies() // suspend fun

Errors: NetworkUnavailable · ServerError

See also: Virtual Currencies guide


Events

track

Emit a generic event to the backend. The server forwards it to any configured integrations (e.g. Meta Conversions API, TikTok Events API). The call performs the POST /v1/events request (retrying transient failures) and rejects if delivery ultimately fails — callers that don't need delivery confirmation can ignore the returned promise.

Params

ParameterTypeRequiredDescription
eventTypestringYesThe event name (e.g. "purchase", "lead", "registration").
paramsTrackParamsNoOptional event metadata.

TrackParams

FieldTypeDescription
occurredAtstringISO-8601 timestamp of when the event occurred. Defaults to now.
subscriberIdstringOverride the subscriber scope. Defaults to the current user / anonymous install.
productIdstringAssociated product identifier.
amountstringDecimal string (e.g. "9.99").
currencystringISO-4217 currency code (e.g. "USD").
eventSourceUrlstringURL of the page or screen where the event occurred.
identityContextobjectIdentity signals for ad-platform matching. Sent as plaintext; the server hashes them before forwarding to integrations. Fields: email, externalId, phone, ip, userAgent, firstName, lastName, city, countryCode.

Returns — void

Throws — rejects with a RovenueError if the event can't be delivered after retries.

The typed track(eventType, params) signature is the React Native API. On Swift and Kotlin, track(envelopeJson:) takes a pre-built camelCase JSON envelope string using the same field names (eventType and occurredAt required). The SDK stamps the wire version and a stable eventId for you.

import { Rovenue } from '@rovenue/react-native-sdk';

// Minimal — event type only:
await Rovenue.track('lead');

// With params:
await Rovenue.track('purchase', {
  amount: '9.99',
  currency: 'USD',
  productId: 'com.example.pro_monthly',
});

// With identity context for ad-platform matching:
await Rovenue.track('registration', {
  identityContext: {
    email: 'user@example.com',
    phone: '+15551234567',
  },
});
import Foundation

// Swift takes a pre-built camelCase JSON envelope string.
func envelope(_ fields: [String: String]) -> String {
    let data = try! JSONSerialization.data(withJSONObject: fields)
    return String(data: data, encoding: .utf8)!
}
let now = ISO8601DateFormatter().string(from: Date())

// Minimal:
try await Rovenue.shared.track(envelopeJson: envelope([
    "eventType": "lead", "occurredAt": now,
]))

// With params:
try await Rovenue.shared.track(envelopeJson: envelope([
    "eventType": "purchase",
    "occurredAt": now,
    "amount": "9.99",
    "currency": "USD",
    "productId": "com.example.pro_monthly",
]))
import org.json.JSONObject
import java.time.Instant

// Kotlin takes a pre-built camelCase JSON envelope string.
// track() is a suspend fun — call it from a coroutine.
val now = Instant.now().toString()

// Minimal:
Rovenue.shared.track(
    JSONObject(mapOf("eventType" to "lead", "occurredAt" to now)).toString()
)

// With params:
Rovenue.shared.track(
    JSONObject(mapOf(
        "eventType" to "purchase",
        "occurredAt" to now,
        "amount" to "9.99",
        "currency" to "USD",
        "productId" to "com.example.pro_monthly",
    )).toString()
)

track() is a client-side convenience wrapper around POST /v1/events. For server-side event ingestion (e.g. from your backend after a payment processor webhook), call the HTTP endpoint directly with your secret API key.


Purchases

Method summary

MethodSignature (RN)ReturnsThrows
getOfferingsgetOfferings()OfferingsNetworkUnavailable, ServerError
purchasepurchase(pkg)PurchaseResultPurchaseCanceled, ProductNotAvailable, StoreProblem, ReceiptInvalid
restorePurchasesrestorePurchases()voidNetworkUnavailable, ServerError

getOfferings

Fetch the offerings configured in the Rovenue dashboard. Each offering contains one or more packages, each wrapping a store product with localised pricing. Call this when your paywall opens.

Params — none

Returns

type Offerings = {
  current: Offering | null;           // the default offering, or null if none is set
  all: Record<string, Offering>;      // all offerings keyed by identifier
};

→ Full type reference: Offerings, Offering, Package, StoreProduct

Throws — NetworkUnavailable, ServerError

import { Rovenue } from '@rovenue/react-native-sdk';

const offerings = await Rovenue.getOfferings();
// offerings.current — default Offering | null
// offerings.all     — Record<string, Offering>

const packages = offerings.current?.packages ?? [];
packages.forEach(pkg => {
  console.log(pkg.product.displayName, pkg.product.priceString);
});
let offerings = try await Rovenue.shared.getOfferings()
// offerings.current — Offering?
// offerings.all     — [String: Offering]

let packages = offerings.current?.packages ?? []
for pkg in packages {
    print(pkg.product.displayName, pkg.product.priceString)
}
val offerings = Rovenue.shared.getOfferings()
// offerings.current — Offering?
// offerings.all     — Map<String, Offering>

val packages = offerings.current?.packages ?: emptyList()
packages.forEach { pkg ->
    Log.d("App", "${pkg.product.displayName} ${pkg.product.priceString}")
}

Errors: NetworkUnavailable · ServerError


purchase

Trigger a native store sheet for the given package. The SDK handles the full purchase lifecycle: StoreKit 2 on iOS, Play Billing on Android, server-side receipt validation. Resolves with updated entitlements and credit balance — no additional polling needed.

Params

ParameterTypeRequiredDescription
pkgPackageYesA package from getOfferings().

Android: Kotlin requires the current Activity as the first argument — purchase(activity, pkg). iOS (Swift) and React Native take only the package.

Returns

type PurchaseResult = {
  entitlements: Entitlement[];              // full set of active entitlements after the purchase
  virtualCurrencies: Record<string, number>; // virtual-currency balances after the purchase
  productId: string;                        // the store product ID purchased
  storeTransactionId: string;               // StoreKit 2 transaction ID or Play Billing order ID
  isDeferred: boolean;                      // true for Ask-to-Buy / parental-control holds — see below
};

→ Full type reference: PurchaseResult

Ask-to-Buy / parental-control holds are not an error. purchase() does not throw for a pending approval — it resolves normally with isDeferred: true (and empty entitlements / storeTransactionId, since nothing has been granted yet). See Handling a deferred purchase below. Do not write a catch branch for this condition; there is no kind for it.

Throws — see table below

ErrorWhen it fires
PurchaseCanceledUser dismissed the store sheet without completing payment
ProductNotAvailableThe product is not available in the user's storefront
StoreProblemThe store returned a non-specific error (e.g. billing issue)
ReceiptInvalidServer-side receipt validation failed
import { Rovenue, RovenueError } from '@rovenue/react-native-sdk';

const offerings = await Rovenue.getOfferings();
const pkg = offerings.current?.packages[0];

if (pkg) {
  try {
    const result = await Rovenue.purchase(pkg);
    if (result.isDeferred) {
      // Ask-to-Buy — tell the user to wait for approval (see below)
    } else {
      console.log('Purchase succeeded:', result.productId);
      // result.entitlements and result.virtualCurrencies are already up to date
    }
  } catch (e) {
    if (!(e instanceof RovenueError)) throw e;
    switch (e.kind) {
      case 'PurchaseCanceled':
        // User dismissed — no action needed
        break;
      case 'ReceiptInvalid':
        // Show a retry prompt
        break;
      default:
        throw e;
    }
  }
}
let offerings = try await Rovenue.shared.getOfferings()
if let pkg = offerings.current?.packages.first {
  do {
    let result = try await Rovenue.shared.purchase(pkg)
    if result.isDeferred {
      // Ask-to-Buy — tell the user to wait for approval (see below)
    } else {
      print("Purchase succeeded: \(result.productId)")
    }
  } catch let e as RovenueError {
    switch e.kind {
    case .purchaseCanceled:
      break // User dismissed — no action needed
    case .receiptInvalid:
      break // Show a retry prompt
    default:
      throw e
    }
  }
}
val offerings = Rovenue.shared.getOfferings()
val pkg = offerings.current?.packages?.firstOrNull()

if (pkg != null) {
  try {
    // `activity` is your current Activity — Play Billing requires it.
    val result = Rovenue.shared.purchase(activity, pkg)
    if (result.isDeferred) {
      // Ask-to-Buy — tell the user to wait for approval (see below)
    } else {
      Log.d("App", "Purchase succeeded: ${result.productId}")
    }
  } catch (e: RovenueException) {
    when (e.kind) {
      ErrorKind.PURCHASE_CANCELED -> { /* User dismissed — no action needed */ }
      ErrorKind.RECEIPT_INVALID -> { /* Show a retry prompt */ }
      else -> throw e
    }
  }
}

Errors: PurchaseCanceled · ProductNotAvailable · StoreProblem · ReceiptInvalid

Handling a deferred purchase

There is no purchase-specific notification for when a deferred (Ask-to-Buy) purchase resolves — the SDK reconciles it in the background and reports the result the same way as any other entitlement change. Listen for ChangeEvent's entitlements-changed value on each platform's change stream: RN Rovenue.addChangeListener((event) => { if (event === 'ENTITLEMENTS_CHANGED') { /* re-check entitlements */ } }); Swift/Kotlin — collect Rovenue.shared.changes and match .entitlementsChanged / ChangeEvent.ENTITLEMENTS_CHANGED.


restorePurchases

Re-validate the user's existing store receipts against the Rovenue server and update the entitlement cache. Required by App Store guidelines to offer a "Restore purchases" button.

Params — none on iOS / React Native. Android: Kotlin requires the current Activity — restorePurchases(activity).

Returns — void

Throws — NetworkUnavailable, ServerError

await Rovenue.restorePurchases();
// Entitlements are refreshed; check entitlement() afterwards
try await Rovenue.shared.restorePurchases()
Rovenue.shared.restorePurchases(activity) // suspend fun; needs the current Activity

Errors: NetworkUnavailable · ServerError

See also: Processing purchases guide


Funnels

Resolve a placement into the funnel to show. On React Native and Flutter there is deliberately no getFunnel — the native side resolves the placement itself and the widget only hosts the native view.

Method summary

MethodSignature (RN)ReturnsThrows
setFallbackPlacementssetFallbackPlacements(json)Promise<number>InvalidArgument

On React Native setFallbackPlacements lives on the Rovenue object — it is not a named export of @rovenue/react-native-sdk. Swift uses setFallbackPlacements(json:) / setFallbackPlacements(fileURL:). Kotlin uses setFallbackPlacements(json) / setFallbackPlacements(context, assetPath).

Rendering is native: host the resolved funnel in RovenueFunnelView — see iOS, Android, React Native and Flutter.


getFunnel

Resolve a placement identifier into the funnel the current subscriber should see — either a direct assignment or the winning variant of a client-drawn experiment over funnels. On Swift and Kotlin the session is already open when it returns; hand it to a FunnelDriver and host RovenueFunnelView.

Params

ParameterTypeRequiredDescription
placementIdstringYesThe placement identifier configured in the dashboard.
localestringNoBCP-47 tag naming which localization of the funnel document to serve.

Returns — the resolved funnel, or null when the placement resolved to nothing (retired, target: none, or an unknown identifier) — that is not an error, and an unknown placement never produces a 404.

Resolution order — network first; on a connectivity failure only (NetworkUnavailable / Timeout) the last cached response for that placement is re-resolved, and if there is no cached response either, the bundled fallback file (see setFallbackPlacements) is consulted. Auth and server errors are not absorbed this way: they propagate. Cache always beats fallback.

Side effects — two, both worth knowing:

  1. When the placement resolves through an experiment, the variant draw happens client-side and deterministically off the subscriber's rovenueId. There is no exposure beacon: merely resolving a funnel records nothing server-side.
  2. The resolved funnel's attribution snapshot is stamped into SDK state, so the next receipt POST carries it as presentedContext. Purchase attribution follows the most recent getFunnel call.

Throws — NetworkUnavailable, Timeout (both only when neither cache nor fallback could answer), RateLimited, InvalidApiKey, Forbidden, ServerError

if let funnel = try await Rovenue.shared.getFunnel(placementId: "onboarding_end", locale: "en") {
    showMyFunnel(funnel)
}
val funnel = Rovenue.shared.getFunnel(placementId = "onboarding_end", locale = "en")
if (funnel != null) {
    showMyFunnel(funnel)
}

Errors: NetworkUnavailable · Timeout · InvalidApiKey · ServerError

See also: Placements guide


setFallbackPlacements

Load a bundled fallback-placements export so placements keep resolving when the network and the disk cache both miss. Download the file from Dashboard → SDK → Offline fallback and ship it inside your app bundle.

The export covers every active placement. One file, one call. This matters most for funnels: a first-run onboarding that cannot open on a plane is the whole first impression.

Params

ParameterTypeRequiredDescription
jsonstringYesThe contents of the fallback-placements export file.

Returns — Promise<number> — the count of placement entries actually loaded. An individual entry that fails to decode is skipped, not fatal, so this can legitimately be lower than the number of entries in the file.

Ordering — call it once, at startup, before your first funnel resolution. Each call replaces the previously loaded set rather than merging into it.

Throws — InvalidArgument when the file does not parse at all, or its formatVersion is not literally 1. This is deliberate and loud: bundling a stale or mismatched export must fail at call time rather than degrade silently at the next outage.

const count = await Rovenue.setFallbackPlacements(jsonString);
// Synchronous and throwing on Swift, not async.
let count = try Rovenue.shared.setFallbackPlacements(fileURL: url)
// Not a suspend fun on Kotlin.
val count = Rovenue.shared.setFallbackPlacements(context, "rovenue-fallback.json")

Errors: InvalidArgument

See also: Placements guide


Remote Config

Feature flags and experiment assignments, evaluated server-side for the current subscriber and cached locally.

Method summary

MethodSignature (RN)ReturnsThrows
refreshRemoteConfigrefreshRemoteConfig()Promise<void>NetworkUnavailable, Timeout, RateLimited, InvalidApiKey, ServerError
getRemoteConfiggetRemoteConfig()Promise<RemoteConfig>—
getFlaggetFlag(key, fallback)Promise<T>—
getExperimentgetExperiment(key)Promise<ExperimentAssignment | null>—
getExperimentsgetExperiments()Promise<ExperimentAssignment[]>—

Cache vs. network: only refreshRemoteConfig() performs a network round-trip. The four read methods resolve from the local cache — they are async (the value crosses the native bridge) but they never fetch, so they work offline and never throw on a malformed payload; each degrades to its documented fallback instead.

React Native also exposes reactive hooks: useRemoteConfig(), useFlag(key, fallback) and useExperiment(key). They read the same local cache synchronously, prime it on first mount, and re-render on REMOTE_CONFIG_CHANGED.

Swift and Kotlin do not have a single getFlag — they expose typed getters instead: remoteConfigBool, remoteConfigString, remoteConfigInt, remoteConfigDouble and remoteConfigJson (Swift: remoteConfigJSON), plus remoteConfigKeys(). Nor do they have a struct-returning getRemoteConfig: remoteConfigAllJson() (Swift: remoteConfigAllJSON()) returns the whole bundle as a raw JSON string.

On React Native, structured error detail travels inside the native error message as a JSON envelope (the Expo JSI bridge forwards only code and message); the SDK unpacks it for you into RovenueError.serverCode, .httpStatus, .isRetryable and .data.retryAfter, so a RateLimited refresh still tells you how long to wait.


refreshRemoteConfig

Force an immediate fetch of the latest Remote Config from the server.

Params — none

Returns — Promise<void>

Side effect — fires a ChangeEvent (RN: 'REMOTE_CONFIG_CHANGED') on every successful refresh. Never call refreshRemoteConfig() from inside that listener: the refresh re-emits the event, which re-invokes the listener, forever.

On a connectivity failure the SDK hydrates the on-disk payload into memory before rethrowing, so reads keep serving the last-known config even though this call rejected.

Throws — NetworkUnavailable, Timeout, RateLimited, InvalidApiKey, ServerError

await Rovenue.refreshRemoteConfig();
try await Rovenue.shared.refreshRemoteConfig()
Rovenue.shared.refreshRemoteConfig() // suspend fun

Errors: NetworkUnavailable · Timeout · RateLimited · InvalidApiKey · ServerError

See also: Experiments guide


getRemoteConfig

Read the whole cached bundle — flags and experiment assignments together.

Params — none

Returns — Promise<RemoteConfig>, shaped { flags, experiments }:

FieldTypeDescription
flagsRecord<string, unknown>Flag key → evaluated value (boolean, number, string, object or array).
experimentsRecord<string, ExperimentAssignment>Experiment key → this subscriber's assignment.

A malformed or empty cached payload resolves to the empty bundle (flags: {}, experiments: {}) rather than throwing.

Throws — nothing

const config = await Rovenue.getRemoteConfig();
console.log(Object.keys(config.flags));
// Swift returns the bundle as a raw JSON string — decode it yourself.
let json = await Rovenue.shared.remoteConfigAllJSON()
// Kotlin returns the bundle as a raw JSON string — decode it yourself.
val json = Rovenue.shared.remoteConfigAllJson()

See also: Experiments guide


getFlag

Read one flag with a typed fallback.

Params

ParameterTypeRequiredDescription
keystringYesThe flag key as configured in the dashboard.
fallbackTYesValue returned when the flag is absent, or when its stored value cannot be read as T.

Returns — Promise<T>

How the type is chosen: the runtime type of fallback selects the read — a boolean fallback reads the flag as a boolean, a number fallback as a double, a string fallback as a string. Any other fallback (object, array, null) falls back to a JSON read, so object and array flags resolve too; if that JSON does not parse, you get your fallback back. There is no separate integer path on React Native — numbers always go through the double getter.

Throws — nothing

const showBanner = await Rovenue.getFlag('show_banner', false);
const maxItems = await Rovenue.getFlag('max_items', 10);
const theme = await Rovenue.getFlag<{ accent: string }>('theme', { accent: '#000' });
// Typed getters, one per type — there is no generic getFlag on Swift.
let showBanner = await Rovenue.shared.remoteConfigBool("show_banner", default: false)
let maxItems = await Rovenue.shared.remoteConfigInt("max_items", default: 10)
// Typed getters, one per type — there is no generic getFlag on Kotlin.
val showBanner = Rovenue.shared.remoteConfigBool("show_banner", fallback = false)
val maxItems = Rovenue.shared.remoteConfigInt("max_items", fallback = 10L)

See also: Experiments guide


getExperiment

Read the current subscriber's assignment for a single experiment.

Params

ParameterTypeRequiredDescription
keystringYesThe experiment key as configured in the dashboard.

Returns — Promise<ExperimentAssignment | null>. null when this subscriber has no assignment for that key.

FieldTypeDescription
experimentIdstringThe experiment's ID.
keystringThe experiment key you looked up.
variantIdstringThe assigned variant's ID.
variantNamestringThe assigned variant's display name.
valueunknownThe variant payload, already JSON-parsed. null when the payload cannot be parsed.

Reading an assignment does not record an exposure — resolving a funnel records nothing server-side either.

Throws — nothing

const assignment = await Rovenue.getExperiment('checkout_copy');
if (assignment) console.log(assignment.variantName);
if let assignment = await Rovenue.shared.experiment("checkout_copy") {
    print(assignment.variantName)
}
val assignment = Rovenue.shared.experiment("checkout_copy")
if (assignment != null) Log.d("App", assignment.variantName)

See also: Experiments guide


getExperiments

All of the current subscriber's experiment assignments.

Params — none

Returns — Promise<ExperimentAssignment[]> (empty array when there are none, or when the cache has not been primed yet)

Throws — nothing

const assignments = await Rovenue.getExperiments();
let assignments = await Rovenue.shared.experimentsAll()
val assignments = Rovenue.shared.experimentsAll()

See also: Experiments guide


Subscriber Attributes

Attach arbitrary key/value metadata to the current subscriber — plus a set of reserved, $-prefixed keys that map to first-class fields server-side and drive the integrations.

Method summary

MethodSignature (RN)ReturnsThrows
setAttributessetAttributes(attributes)Promise<void>Storage
setEmailsetEmail(email)Promise<void>Storage
setDisplayNamesetDisplayName(name)Promise<void>Storage
setPhoneNumbersetPhoneNumber(phone)Promise<void>Storage
setPushTokensetPushToken(token)Promise<void>Storage
flushAttributesflushAttributes()Promise<number>NetworkUnavailable, Timeout, RateLimited, InvalidApiKey, Forbidden, ServerError

Write model — read this before using any of them. Every setter is a buffered local write. The returned promise resolves once the mutation has been written to the on-device buffer, not once the server has it. A background dispatcher syncs the buffer every 30 seconds, and again when the app comes to the foreground; flushAttributes forces it early. So a resolved setEmail() means "durably queued", never "the server knows".

shutdown() does not flush the buffer. If you tear the SDK down and it matters that pending mutations land, call flushAttributes() first.

Passing null as a value deletes the key rather than storing an empty value. The four named setters are thin wrappers over setAttributes for the most common reserved keys.


setAttributes

Queue a batch of subscriber-attribute mutations.

Params

ParameterTypeRequiredDescription
attributesRecord<string, string | null>YesKey → value. A null value deletes that key. Keys prefixed with $ are reserved and map to first-class server-side fields.

Returns — Promise<void> (resolves on the local buffered write — see the write model above)

Throws — Storage if the local buffer cannot be written

await Rovenue.setAttributes({
  plan_interest: 'annual',
  beta_optin: null, // deletes the key
});
try await Rovenue.shared.setAttributes([
    "plan_interest": "annual",
    "beta_optin": nil, // deletes the key
])
Rovenue.shared.setAttributes(
    mapOf(
        "plan_interest" to "annual",
        "beta_optin" to null, // deletes the key
    ),
)

Errors: Storage

See also: Subscriber Identity Attributes


setEmail

Set the subscriber's email — the reserved $email attribute. Same buffered-write, background-flush contract as setAttributes.

Params

ParameterTypeRequiredDescription
emailstring | nullYesThe email address; null clears it.

Returns — Promise<void>

Throws — Storage

await Rovenue.setEmail('ada@example.com');
try await Rovenue.shared.setEmail("ada@example.com")
Rovenue.shared.setEmail("ada@example.com")

See also: Subscriber Identity Attributes


setDisplayName

Set the subscriber's display name — the reserved $displayName attribute. Same contract as setEmail.

Params

ParameterTypeRequiredDescription
namestring | nullYesThe display name; null clears it.

Returns — Promise<void>

Throws — Storage

await Rovenue.setDisplayName('Ada Lovelace');
try await Rovenue.shared.setDisplayName("Ada Lovelace")
Rovenue.shared.setDisplayName("Ada Lovelace")

See also: Subscriber Identity Attributes


setPhoneNumber

Set the subscriber's phone number — the reserved $phoneNumber attribute. Same contract as setEmail.

Params

ParameterTypeRequiredDescription
phonestring | nullYesThe phone number; null clears it.

Returns — Promise<void>

Throws — Storage

await Rovenue.setPhoneNumber('+15551234567');
try await Rovenue.shared.setPhoneNumber("+15551234567")
Rovenue.shared.setPhoneNumber("+15551234567")

See also: Subscriber Identity Attributes


setPushToken

Store the device's push token. The reserved key is platform-specific — $apnsTokens on iOS, $fcmTokens on Android — and the SDK picks the right one for the device it is running on, so you pass the token and nothing else. Same contract as setEmail.

Params

ParameterTypeRequiredDescription
tokenstring | nullYesThe APNs or FCM token; null clears it.

Returns — Promise<void>

Throws — Storage

await Rovenue.setPushToken(deviceToken);
try await Rovenue.shared.setPushToken(deviceToken) // → $apnsTokens
Rovenue.shared.setPushToken(deviceToken) // → $fcmTokens

See also: Subscriber Identity Attributes


flushAttributes

Force an immediate flush of buffered attribute mutations to the server, instead of waiting for the next background tick.

Params — none

Returns — Promise<number> — the number of mutations actually sent

Ordering — most apps never need this; the background dispatcher already flushes on its own schedule and on foreground. Reach for it when you need the server to have the attributes now — for example immediately before shutdown(), which does not flush on your behalf.

Throws — NetworkUnavailable, Timeout, InvalidApiKey, Forbidden, ServerError

const sent = await Rovenue.flushAttributes();
let sent = try await Rovenue.shared.flushAttributes()
val sent = Rovenue.shared.flushAttributes()

Errors: NetworkUnavailable · Timeout · ServerError

See also: Subscriber Identity Attributes


Account Token

getAppAccountToken

Return a stable UUID tied to the current rovenueId. Pass it to StoreKit's .appAccountToken(uuid) or Play Billing's setObfuscatedAccountId() before initiating a purchase so the store can attribute the transaction to the correct Rovenue subscriber. The token changes when identify() is called with a new user.

Params — none

Returns — string (UUID v4)

Throws — Storage if MMKV is unavailable.

const token = await Rovenue.getAppAccountToken();
// Pass to your custom StoreKit integration if needed.
// Most apps don't need to call this directly — purchase() handles it.
let token = try await Rovenue.shared.getAppAccountToken()
// Pass as .appAccountToken(UUID(uuidString: token)!) in a custom SKProduct request
val token = Rovenue.shared.getAppAccountToken()
// Pass as setObfuscatedAccountId(token) in a custom BillingFlowParams builder

Errors: Storage


Lifecycle

Method summary

MethodSignature (RN)ReturnsThrows
setForegroundsetForeground(isForeground)void—
shutdownshutdown()void—

Wire these into your platform's app lifecycle hooks so the SDK can pause background work when the app is in the background and trigger a refresh when it returns to the foreground.


setForeground

Notify the SDK of foreground/background transitions. When true, the SDK schedules a background entitlement sync to catch any server-side changes that occurred while the app was inactive.

Params

ParameterTypeRequiredDescription
isForegroundbooleanYestrue when the app enters the foreground; false when it goes to the background.

Returns — void

Throws — nothing

import { AppState } from 'react-native';
import { Rovenue } from '@rovenue/react-native-sdk';

AppState.addEventListener('change', (state) => {
  Rovenue.setForeground(state === 'active');
});
// In SceneDelegate or SwiftUI .scenePhase observer:
.onChange(of: scenePhase) { phase in
    Rovenue.shared.setForeground(phase == .active)
}
// In DefaultLifecycleObserver:
override fun onStart(owner: LifecycleOwner) {
    Rovenue.shared.setForeground(true)
}
override fun onStop(owner: LifecycleOwner) {
    Rovenue.shared.setForeground(false)
}

shutdown

Flush pending work and cleanly release SDK resources. Call when the process is about to exit or on explicit user logout if you are not reusing the process.

Params — none

Returns — void

Throws — nothing

// On app teardown:
await Rovenue.shutdown();
await Rovenue.shared.shutdown()
Rovenue.shared.shutdown() // suspend fun

See also: App lifecycle guide


Logging

setLogHandler

Install a custom log handler to receive structured SDK log entries. Useful for routing SDK logs into your own logging infrastructure. On React Native, returns an unsubscribe function.

Params

ParameterTypeRequiredDescription
handler(entry: LogEntry) => voidYesCallback invoked for each SDK log entry.

Returns — () => void on React Native (call to stop receiving logs); void on Swift and Kotlin (set to nil / null to detach).

Throws — nothing

The LogEntry shape:

type LogEntry = {
  level: 'debug' | 'info' | 'warn' | 'error';
  message: string;
  timestamp: string; // ISO 8601
};

→ Full type reference: LogEntry

const unsub = Rovenue.setLogHandler((entry) => {
  console.log(`[Rovenue ${entry.level}] ${entry.message}`);
});

// Later, to stop receiving logs:
unsub();
Rovenue.shared.setLogHandler { entry in
    print("[Rovenue \(entry.level)] \(entry.message)")
}

// To detach:
Rovenue.shared.setLogHandler(nil)
Rovenue.shared.setLogHandler { entry ->
    Log.d("Rovenue", "[${entry.level}] ${entry.message}")
}

// To detach:
Rovenue.shared.setLogHandler(null)

Change Stream

Subscribe to real-time state change notifications from the SDK. The SDK fires an event whenever the entitlement cache, identity, virtual-currency balances, or remote config are updated. React to these events to keep your UI in sync without polling.

Method / PropertyPlatformDescription
addChangeListener(handler)React NativeRegister a callback; returns an unsubscribe function.
changesSwiftAsyncStream<ChangeEvent> — iterate with for await.
changesKotlinSharedFlow<ChangeEvent> — collect with .onEach { }.launchIn(scope).

The ChangeEvent values (shown as the React Native string value — see ChangeEvent for the exact Swift case / Kotlin constant spelling of each):

Value (RN)When it fires
'ENTITLEMENTS_CHANGED'After any entitlement cache refresh (purchase, refreshEntitlements(), foreground sync).
'IDENTITY_CHANGED'After identify() succeeds or logOut() resets the user.
'VIRTUAL_CURRENCIES_CHANGED'After refreshVirtualCurrencies() or a server-side grant/spend that updates the cache.
'REMOTE_CONFIG_CHANGED'After every successful refreshRemoteConfig(). The SDK does not diff the payload, so this also fires when the refresh returned identical values.

→ Full type reference: ChangeEvent

import { Rovenue } from '@rovenue/react-native-sdk';

const unsub = Rovenue.addChangeListener((event) => {
  switch (event) {
    case 'ENTITLEMENTS_CHANGED':
      // Re-read entitlements and update gated UI
      break;
    case 'IDENTITY_CHANGED':
      // User identity has changed — re-read currentUser()
      break;
    case 'VIRTUAL_CURRENCIES_CHANGED':
      // Re-read virtualCurrencies() and update the balance display
      break;
    case 'REMOTE_CONFIG_CHANGED':
      // Re-read your flags/experiments
      break;
  }
});

// When your component unmounts:
unsub();
// Typically in a Task {} inside your SwiftUI view or view model:
for await event in Rovenue.shared.changes {
    switch event {
    case .entitlementsChanged:
        // Re-read entitlements and update gated UI
    case .identityChanged:
        // User identity has changed — re-read currentUser()
    case .virtualCurrenciesChanged:
        // Re-read virtualCurrencyBalances() and update the balance display
    case .remoteConfigChanged:
        // Re-read your flags/experiments
    }
}
// In a ViewModel or LifecycleCoroutineScope:
Rovenue.shared.changes
    .onEach { event ->
        when (event) {
            ChangeEvent.ENTITLEMENTS_CHANGED       -> { /* re-read entitlements */ }
            ChangeEvent.IDENTITY_CHANGED           -> { /* user identity changed */ }
            ChangeEvent.VIRTUAL_CURRENCIES_CHANGED -> { /* re-read balances */ }
            ChangeEvent.REMOTE_CONFIG_CHANGED      -> { /* re-read flags/experiments */ }
        }
    }
    .launchIn(viewModelScope)

See also: Reacting to changes guide


Funnel Attribution

Method summary

MethodSignature (RN)ReturnsThrows
claimFunnelTokenclaimFunnelToken(token)FunnelClaimResultFunnelTokenNotFound, FunnelTokenExpired, FunnelTokenAlreadyClaimed
claimInstallclaimInstall(params)FunnelClaimResult | null—
resolveFunnelClaimresolveFunnelClaim(opts?)FunnelClaimResult | null—
claimFromClipboardclaimFromClipboard()FunnelClaimResult | null—
claimFromUrlclaimFromUrl(url)FunnelClaimResult | nullFunnelTokenNotFound, FunnelTokenExpired, FunnelTokenAlreadyClaimed
claimViaEmailclaimViaEmail(email)void—
installIdinstallId()string—
addFunnelClaimListeneraddFunnelClaimListener(cb)() => void—

claimFunnelToken

Exchange a funnel token (obtained from a deep link or QR code) for the subscriber's funnel data. This is the primary attribution path for token-based funnels.

Params

ParameterTypeRequiredDescription
tokenstringYesThe funnel token from the deep link (e.g. rov_ft_…).

Returns

// React Native
type FunnelClaimResult = {
  subscriberId: string;      // Rovenue subscriber ID of the matched user
  funnelAnswers: Record<string, unknown>; // key-value map of funnel step answers, already parsed
};

Swift and Kotlin return the funnel answers as a raw JSON string, not a parsed object. Their FunnelClaimResult is { subscriberId: String, funnelAnswersJson: String } — decode funnelAnswersJson yourself (e.g. JSONSerialization on Swift, org.json.JSONObject/kotlinx.serialization on Kotlin) if you need the parsed shape. Only the React Native binding pre-parses it into funnelAnswers.

→ Full type reference: FunnelClaimResult

Throws — see table below

ErrorWhen it fires
FunnelTokenNotFoundThe token does not match any known funnel session
FunnelTokenExpiredThe token has passed its expiry window
FunnelTokenAlreadyClaimedThe token was already consumed by a previous install
import { Rovenue, RovenueError } from '@rovenue/react-native-sdk';

// Called from your deep-link handler:
try {
  const result = await Rovenue.claimFunnelToken(token);
  console.log('Claimed subscriber:', result.subscriberId);
  console.log('Funnel answers:', result.funnelAnswers);
} catch (e) {
  if (!(e instanceof RovenueError)) throw e;
  switch (e.kind) {
    case 'FunnelTokenNotFound':
      // Token unknown — may be an old or invalid link
      break;
    case 'FunnelTokenExpired':
      // Prompt the user to re-visit the funnel
      break;
    case 'FunnelTokenAlreadyClaimed':
      // Already claimed — proceed without re-claiming
      break;
    default:
      throw e;
  }
}
// Called from your deep-link handler (Scene(_:openURLContexts:) or onOpenURL):
do {
    let result = try await Rovenue.shared.claimFunnelToken(token)
    print("Claimed subscriber: \(result.subscriberId)")
    print("Funnel answers (raw JSON): \(result.funnelAnswersJson)")
} catch let e as RovenueError {
    switch e.kind {
    case .funnelTokenNotFound:
        break // Token unknown — may be an old or invalid link
    case .funnelTokenExpired:
        break // Prompt the user to re-visit the funnel
    case .funnelTokenAlreadyClaimed:
        break // Already claimed — proceed without re-claiming
    default:
        throw e
    }
}
// Called from your deep-link handler (Activity.onNewIntent or NavController):
try {
    val result = Rovenue.shared.claimFunnelToken(token) // suspend fun
    Log.d("App", "Claimed subscriber: ${result.subscriberId}")
    Log.d("App", "Funnel answers (raw JSON): ${result.funnelAnswersJson}")
} catch (e: RovenueException) {
    when (e.kind) {
        ErrorKind.FUNNEL_TOKEN_NOT_FOUND -> { /* Token unknown — may be an old or invalid link */ }
        ErrorKind.FUNNEL_TOKEN_EXPIRED -> { /* Prompt the user to re-visit the funnel */ }
        ErrorKind.FUNNEL_TOKEN_ALREADY_CLAIMED -> { /* Already claimed — proceed without re-claiming */ }
        else -> throw e
    }
}

claimInstall

Recover a prior funnel session deterministically from caller-supplied install context (e.g. an Android Install Referrer token) and claim it. Returns a FunnelClaimResult if a prior funnel session is matched, or null if no match can be recovered. This is deterministic — it does not use device fingerprinting. Call this once on first launch, immediately after configure.

On iOS, when no explicit token is available, recovery falls back to a deterministic server-side IP match within a 1-hour window — no device fingerprinting is used. For clipboard-based recovery on iOS, see claimFromClipboard.

Params — all optional on React Native; on native Swift and Kotlin every field must be supplied (see the callout below).

On React Native's Android bridge specifically, the SDK auto-collects platform, locale, timezone, screenDims, deviceModel, and the Google Play Install Referrer from the native layer — pass any field only to override the auto-collected value. On React Native's iOS bridge, callers still supply context explicitly (auto-collection for iOS will land in a later release).

Native Swift and Kotlin have no auto-collection at all. The auto-collection above is a convenience built into the React Native Android bridge specifically — the underlying packages/sdk-swift and packages/sdk-kotlin façades take one ClaimInstallParams struct with no default values, so a native caller must always compute and supply every field itself (see the Swift/Kotlin tabs below for how to derive them).

ParameterTypeRequiredDescription
platform"ios" | "android"No (RN) / Yes (Swift, Kotlin)Platform of the current install. Auto-collected on RN's Android bridge.
localestringNo (RN) / Yes (Swift, Kotlin)IETF BCP 47 locale tag (e.g. "en-US"). Auto-collected on RN's Android bridge.
timezonestringNo (RN) / Yes (Swift, Kotlin)IANA timezone identifier (e.g. "America/New_York"). Auto-collected on RN's Android bridge.
screenDimsstringNo (RN) / Yes (Swift, Kotlin)Screen dimensions in pixels, formatted "WIDTHxHEIGHT" (e.g. "1080x2400") — a plain string, not a width/height object. Auto-collected on RN's Android bridge.
deviceModelstring | nullNoDevice model string (e.g. "iPhone16,1"). Auto-collected on RN's Android bridge; pass null on native Swift/Kotlin if you have nothing to send.
installReferrerstring | nullNoAndroid Install Referrer string from the Play Store; auto-collected on RN's Android bridge, null on iOS and on native Kotlin unless you read it yourself.

Returns — FunnelClaimResult | null (null means no prior funnel session was matched)

Throws — nothing

import { Rovenue } from '@rovenue/react-native-sdk';

// Android: SDK auto-collects referrer + device context (platform, locale,
// timezone, screenDims, deviceModel). iOS: supply params explicitly.
const result = await Rovenue.claimInstall(); // Android: auto-collects referrer + context

if (result) {
  console.log('Matched funnel session:', result.subscriberId);
} else {
  console.log('No prior funnel session matched');
}
import UIKit

let screen = UIScreen.main.bounds
let scale = UIScreen.main.scale
let params = ClaimInstallParams(
    platform: "ios",
    locale: Locale.current.identifier,
    timezone: TimeZone.current.identifier,
    screenDims: "\(Int(screen.width * scale))x\(Int(screen.height * scale))",
    deviceModel: UIDevice.current.model,
    installReferrer: nil
)
let result = await Rovenue.shared.claimInstall(params)

if let result {
    print("Matched funnel session: \(result.subscriberId)")
} else {
    print("No prior funnel session matched")
}
import android.os.Build
import android.util.DisplayMetrics
import dev.rovenue.sdk.generated.ClaimInstallParams
import java.util.Locale
import java.util.TimeZone

val metrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(metrics)

val params = ClaimInstallParams(
    platform = "android",
    locale = Locale.getDefault().toLanguageTag(),
    timezone = TimeZone.getDefault().id,
    screenDims = "${metrics.widthPixels}x${metrics.heightPixels}",
    deviceModel = Build.MODEL,
    installReferrer = null, // or a Play Install Referrer string you've read yourself
)
val result = Rovenue.shared.claimInstall(params) // suspend fun

if (result != null) {
    Log.d("App", "Matched funnel session: ${result.subscriberId}")
} else {
    Log.d("App", "No prior funnel session matched")
}

resolveFunnelClaim

React Native only. Runs the no-gesture, deterministic first-launch attribution chain once per install and returns the result. The chain is:

  1. Direct deep link — if a URL is supplied (or the app was cold-started from one), extract and claim the embedded funnel token via claimFromUrl.
  2. Platform deferred match — if step 1 yields no result, call claimInstall, which on Android queries the Google Play Install Referrer and on iOS performs a server-side IP match within a 1-hour window. No device fingerprinting is used at any step.

Once a claim has succeeded for this install the method short-circuits immediately and returns the cached result without a network round-trip.

Internal claim errors (network failures, token not found, etc.) are treated as "unresolved" rather than thrown — the method never throws on its own. Resolved claims are also delivered to any listener registered with addFunnelClaimListener.

Params — all optional

ParameterTypeRequiredDescription
urlstringNoThe URL the app was launched with (e.g. from Linking.getInitialURL()). Pass undefined to skip the deep-link step.

Returns — FunnelClaimResult | null (null when neither step resolves a claim)

Throws — nothing

import { Rovenue } from '@rovenue/react-native-sdk';
import * as Linking from 'expo-linking';

// Recommended first-launch flow — call once after Rovenue.configure():
const url = await Linking.getInitialURL();
const result = await Rovenue.resolveFunnelClaim({ url: url ?? undefined });

if (result) {
  // Claim resolved deterministically — apply funnel-specific onboarding.
  console.log('Funnel claim resolved for subscriber:', result.subscriberId);
} else {
  // No deterministic match. Try gesture-gated fallbacks in sequence:

  // 1. iOS clipboard (show a "Continue" button, then on tap):
  //    const clipResult = await Rovenue.claimFromClipboard();

  // 2. Email magic-link (show an email field, then on submit):
  //    await Rovenue.claimViaEmail(email);
  //    // Result arrives via addFunnelClaimListener when the link is tapped.
}

claimFromClipboard

React Native only, and iOS only within that. There is no claimFromClipboard on the native Swift or Kotlin SDKs — Rovenue.shared has no equivalent method on either platform (confirmed against packages/sdk-swift/Sources/Rovenue/Rovenue.swift and packages/sdk-kotlin/.../Rovenue.kt; grepping both trees for claimFromClipboard returns nothing outside the RN package). It exists purely as a convenience in @rovenue/react-native-sdk, implemented on top of the standard claim flow: it reads a rovenue-funnel:<token> marker placed on the clipboard by the web funnel, extracts the token, calls the same claimFunnelToken every platform already has, then clears the marker from the clipboard so it can't be re-claimed or leaked. Returns a FunnelClaimResult if the marker is present and matched, or null on Android or when no marker is found.

Call from a user-gesture handler (e.g. a "Continue" tap on your first onboarding screen) — iOS shows a one-time paste-access banner on the clipboard read.

On native Swift or Kotlin, there is no built-in equivalent — replicate it yourself using claimFunnelToken: read the clipboard string (UIPasteboard.general.string on iOS; there's no Android equivalent, since the RN version is also a no-op there), check it starts with "rovenue-funnel:", strip the prefix to get the token, and call claimFunnelToken(token). Clear the clipboard afterward if you want the same can't-be-reclaimed behavior the RN version has.

Params — none

Returns — FunnelClaimResult | null (null on Android, or when no rovenue-funnel: marker is present)

Throws — nothing

import { Rovenue } from '@rovenue/react-native-sdk';

// Call from a user-gesture handler (e.g. a "Continue" button press):
const result = await Rovenue.claimFromClipboard();

if (result) {
  console.log('Clipboard funnel claim resolved:', result.subscriberId);
}
// Returns null on Android or when no rovenue-funnel: marker is present.

claimViaEmail

Initiate an email-based funnel claim. The SDK sends a magic-link email to the provided address; the claim resolves later when the user taps the link and the app handles the resulting deep link (see addFunnelClaimListener).

Params

ParameterTypeRequiredDescription
emailstringYesThe email address to send the claim link to. Must be a valid RFC 5322 address.

Returns — void (resolves as soon as the magic-link request is dispatched; the actual claim fires via addFunnelClaimListener when the deep link is opened)

Throws — InvalidArgument if the email address fails format validation.

import { Rovenue } from '@rovenue/react-native-sdk';

// After the user enters their email in your onboarding flow:
await Rovenue.claimViaEmail('user@example.com');
// A magic-link email is now on its way.
// Listen for the resolved claim via addFunnelClaimListener().
// After the user enters their email in your onboarding flow:
try await Rovenue.shared.claimViaEmail("user@example.com")
// A magic-link email is now on its way.
// Listen for the resolved claim via the funnelClaims AsyncStream.
// After the user enters their email in your onboarding flow:
Rovenue.shared.claimViaEmail("user@example.com") // suspend fun
// A magic-link email is now on its way.
// Listen for the resolved claim via addFunnelClaimListener().

installId

Return the stable, persisted identifier for this install. The install ID is generated on first launch, survives app restarts, and is sent as the idempotency key for claimInstall / claimViaEmail. It does not change when identify() or logOut() is called.

Params — none

Returns — string (UUID v4 format, generated once per install and persisted in MMKV)

Throws — nothing

import { Rovenue } from '@rovenue/react-native-sdk';

const id = await Rovenue.installId();
console.log('Install ID:', id); // e.g. "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
let id = await Rovenue.shared.installId()
print("Install ID: \(id)") // e.g. "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
val id = Rovenue.shared.installId() // suspend fun
Log.d("App", "Install ID: $id") // e.g. "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

addFunnelClaimListener

Register a callback that fires whenever a funnel claim resolves — whether triggered by a token deep link, a deterministic install match, or an email magic-link. On React Native, returns an unsubscribe function. On Swift, use the funnelClaims AsyncStream. On Kotlin, use the funnelClaims SharedFlow.

Params

ParameterTypeRequiredDescription
cb(result: FunnelClaimResult) => voidYesCallback invoked each time a funnel claim resolves.

Returns — () => void (call to unregister the listener)

Throws — nothing

import { Rovenue } from '@rovenue/react-native-sdk';

const unsub = Rovenue.addFunnelClaimListener((result) => {
  console.log('Funnel claim resolved for subscriber:', result.subscriberId);
  console.log('Funnel answers:', result.funnelAnswers);
  // Apply funnel-specific onboarding, unlock content, etc.
});

// When your component unmounts:
unsub();
// Typically in a Task {} inside your SwiftUI view or AppDelegate:
for await result in Rovenue.shared.funnelClaims {
    print("Funnel claim resolved for subscriber: \(result.subscriberId)")
    print("Funnel answers (raw JSON): \(result.funnelAnswersJson)")
    // Apply funnel-specific onboarding, unlock content, etc.
}
// In a ViewModel or LifecycleCoroutineScope:
Rovenue.shared.funnelClaims
    .onEach { result ->
        Log.d("App", "Funnel claim resolved for subscriber: ${result.subscriberId}")
        Log.d("App", "Funnel answers (raw JSON): ${result.funnelAnswersJson}")
        // Apply funnel-specific onboarding, unlock content, etc.
    }
    .launchIn(viewModelScope)

claimFromUrl

Parse and claim a funnel token from any URL the app receives via a Universal Link, App Link, or custom-scheme deep link. Call this whenever the OS hands your app a URL — it is safe to pass any URL; non-funnel URLs return null immediately without a network request.

Recognized URL shapes:

ShapeExample
Universal Link / App Link path segmenthttps://funnels.example.com/funnels/open/<token>
Query parameter (any host)https://example.com/promo?rovenue_funnel_token=<token>
token= on the onboarding-complete deep linkmyapp://onboarding-complete?token=<token>

Params

ParameterTypeRequiredDescription
urlstringYesAny URL the app received from the OS. Non-funnel URLs are ignored and resolve to null.

Returns — Promise<FunnelClaimResult | null> (null for URLs that contain no recognizable funnel token — no network call is made)

Throws — funnel claim errors when a recognized token fails server-side: FunnelTokenNotFound, FunnelTokenExpired, or FunnelTokenAlreadyClaimed. Non-funnel URLs resolve null and never throw — so always wrap the call (a fire-and-forget handler should .catch).

This is a React Native helper. On Swift/Kotlin, parse the token from the incoming URL yourself and call claimFunnelToken.

import { Rovenue } from '@rovenue/react-native-sdk';
import * as Linking from 'expo-linking';

// Warm start — app already open when the link is tapped:
Linking.addEventListener('url', ({ url }) => {
  Rovenue.claimFromUrl(url).catch(() => {
    // expired / already-claimed / not-found — recover via email or ignore
  });
});

// Cold start — app launched via the link:
const initial = await Linking.getInitialURL();
if (initial) {
  try {
    await Rovenue.claimFromUrl(initial);
  } catch {
    // recognized token failed server-side — fall back to email recovery
  }
}

The OS-level deep-link configuration — iOS Associated Domains (for the funnel Universal Link domain), Android App Links intent filter, and any custom URL scheme — lives in the consuming app, not the SDK. See your platform's documentation for setup instructions.

When a funnel token is found and successfully claimed, the result is delivered via addFunnelClaimListener. The return value of claimFromUrl itself is null for non-funnel URLs and the FunnelClaimResult for matched ones.

On this page