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
| Parameter | Type | Required | Description |
|---|---|---|---|
apiKey | string | Yes | Your project's public API key. Prefix rov_pub_. |
baseUrl | string | No | API endpoint. Defaults to the hosted endpoint https://api.rovenue.io; override only when self-hosting (https://; http:// allowed for localhost only). |
logLevel | string | No | Verbosity of SDK logs: off / error / warn / info / debug / trace. Defaults to warn. |
appVersion | string | No | App 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
| Method | Signature (RN) | Returns | Throws |
|---|---|---|---|
currentUser | currentUser() | User | — |
identify | identify(appUserId) | void | InvalidArgument, SubscriberUnresolvable |
logOut | logOut() | 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() calledlet 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
| Parameter | Type | Required | Description |
|---|---|---|---|
appUserId | string | Yes | Your 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 funUse 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 funSee also: Identifying users guide, Identity consent guide
Entitlements
Method summary
| Method | Signature (RN) | Returns | Throws |
|---|---|---|---|
entitlement | entitlement(id) | Entitlement | null | — |
entitlementsAll | entitlementsAll() | Entitlement[] | — |
refreshEntitlements | refreshEntitlements() | void | NetworkUnavailable, 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
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The 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 funErrors: NetworkUnavailable · ServerError
See also: Entitlements guide
Virtual Currencies
Method summary
| Method | Signature (RN) | Returns | Throws |
|---|---|---|---|
virtualCurrencies | virtualCurrencies() | Promise<Record<string, number>> | — |
virtualCurrency | virtualCurrency(code) | Promise<number> | — |
refreshVirtualCurrencies | refreshVirtualCurrencies() | void | NetworkUnavailable, 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
| Parameter | Type | Required | Description |
|---|---|---|---|
code | string | Yes | The 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 funErrors: 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
| Parameter | Type | Required | Description |
|---|---|---|---|
eventType | string | Yes | The event name (e.g. "purchase", "lead", "registration"). |
params | TrackParams | No | Optional event metadata. |
TrackParams
| Field | Type | Description |
|---|---|---|
occurredAt | string | ISO-8601 timestamp of when the event occurred. Defaults to now. |
subscriberId | string | Override the subscriber scope. Defaults to the current user / anonymous install. |
productId | string | Associated product identifier. |
amount | string | Decimal string (e.g. "9.99"). |
currency | string | ISO-4217 currency code (e.g. "USD"). |
eventSourceUrl | string | URL of the page or screen where the event occurred. |
identityContext | object | Identity 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
| Method | Signature (RN) | Returns | Throws |
|---|---|---|---|
getOfferings | getOfferings() | Offerings | NetworkUnavailable, ServerError |
purchase | purchase(pkg) | PurchaseResult | PurchaseCanceled, ProductNotAvailable, StoreProblem, ReceiptInvalid |
restorePurchases | restorePurchases() | void | NetworkUnavailable, 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
| Parameter | Type | Required | Description |
|---|---|---|---|
pkg | Package | Yes | A package from getOfferings(). |
Android: Kotlin requires the current
Activityas 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
| Error | When it fires |
|---|---|
PurchaseCanceled | User dismissed the store sheet without completing payment |
ProductNotAvailable | The product is not available in the user's storefront |
StoreProblem | The store returned a non-specific error (e.g. billing issue) |
ReceiptInvalid | Server-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() afterwardstry await Rovenue.shared.restorePurchases()Rovenue.shared.restorePurchases(activity) // suspend fun; needs the current ActivityErrors: 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
| Method | Signature (RN) | Returns | Throws |
|---|---|---|---|
setFallbackPlacements | setFallbackPlacements(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
| Parameter | Type | Required | Description |
|---|---|---|---|
placementId | string | Yes | The placement identifier configured in the dashboard. |
locale | string | No | BCP-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:
- 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. - 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 recentgetFunnelcall.
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
| Parameter | Type | Required | Description |
|---|---|---|---|
json | string | Yes | The 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
| Method | Signature (RN) | Returns | Throws |
|---|---|---|---|
refreshRemoteConfig | refreshRemoteConfig() | Promise<void> | NetworkUnavailable, Timeout, RateLimited, InvalidApiKey, ServerError |
getRemoteConfig | getRemoteConfig() | Promise<RemoteConfig> | — |
getFlag | getFlag(key, fallback) | Promise<T> | — |
getExperiment | getExperiment(key) | Promise<ExperimentAssignment | null> | — |
getExperiments | getExperiments() | 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 funErrors: 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 }:
| Field | Type | Description |
|---|---|---|
flags | Record<string, unknown> | Flag key → evaluated value (boolean, number, string, object or array). |
experiments | Record<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
| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | Yes | The flag key as configured in the dashboard. |
fallback | T | Yes | Value 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
| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | Yes | The experiment key as configured in the dashboard. |
Returns — Promise<ExperimentAssignment | null>. null when this subscriber has no assignment for that key.
| Field | Type | Description |
|---|---|---|
experimentId | string | The experiment's ID. |
key | string | The experiment key you looked up. |
variantId | string | The assigned variant's ID. |
variantName | string | The assigned variant's display name. |
value | unknown | The 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
| Method | Signature (RN) | Returns | Throws |
|---|---|---|---|
setAttributes | setAttributes(attributes) | Promise<void> | Storage |
setEmail | setEmail(email) | Promise<void> | Storage |
setDisplayName | setDisplayName(name) | Promise<void> | Storage |
setPhoneNumber | setPhoneNumber(phone) | Promise<void> | Storage |
setPushToken | setPushToken(token) | Promise<void> | Storage |
flushAttributes | flushAttributes() | 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
| Parameter | Type | Required | Description |
|---|---|---|---|
attributes | Record<string, string | null> | Yes | Key → 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
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | null | Yes | The 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
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | null | Yes | The 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
| Parameter | Type | Required | Description |
|---|---|---|---|
phone | string | null | Yes | The 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
| Parameter | Type | Required | Description |
|---|---|---|---|
token | string | null | Yes | The APNs or FCM token; null clears it. |
Returns — Promise<void>
Throws — Storage
await Rovenue.setPushToken(deviceToken);try await Rovenue.shared.setPushToken(deviceToken) // → $apnsTokensRovenue.shared.setPushToken(deviceToken) // → $fcmTokensSee 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 requestval token = Rovenue.shared.getAppAccountToken()
// Pass as setObfuscatedAccountId(token) in a custom BillingFlowParams builderErrors: Storage
Lifecycle
Method summary
| Method | Signature (RN) | Returns | Throws |
|---|---|---|---|
setForeground | setForeground(isForeground) | void | — |
shutdown | shutdown() | 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
| Parameter | Type | Required | Description |
|---|---|---|---|
isForeground | boolean | Yes | true 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 funSee 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
| Parameter | Type | Required | Description |
|---|---|---|---|
handler | (entry: LogEntry) => void | Yes | Callback 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 / Property | Platform | Description |
|---|---|---|
addChangeListener(handler) | React Native | Register a callback; returns an unsubscribe function. |
changes | Swift | AsyncStream<ChangeEvent> — iterate with for await. |
changes | Kotlin | SharedFlow<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
| Method | Signature (RN) | Returns | Throws |
|---|---|---|---|
claimFunnelToken | claimFunnelToken(token) | FunnelClaimResult | FunnelTokenNotFound, FunnelTokenExpired, FunnelTokenAlreadyClaimed |
claimInstall | claimInstall(params) | FunnelClaimResult | null | — |
resolveFunnelClaim | resolveFunnelClaim(opts?) | FunnelClaimResult | null | — |
claimFromClipboard | claimFromClipboard() | FunnelClaimResult | null | — |
claimFromUrl | claimFromUrl(url) | FunnelClaimResult | null | FunnelTokenNotFound, FunnelTokenExpired, FunnelTokenAlreadyClaimed |
claimViaEmail | claimViaEmail(email) | void | — |
installId | installId() | string | — |
addFunnelClaimListener | addFunnelClaimListener(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
| Parameter | Type | Required | Description |
|---|---|---|---|
token | string | Yes | The 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
| Error | When it fires |
|---|---|
FunnelTokenNotFound | The token does not match any known funnel session |
FunnelTokenExpired | The token has passed its expiry window |
FunnelTokenAlreadyClaimed | The 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).
| Parameter | Type | Required | Description |
|---|---|---|---|
platform | "ios" | "android" | No (RN) / Yes (Swift, Kotlin) | Platform of the current install. Auto-collected on RN's Android bridge. |
locale | string | No (RN) / Yes (Swift, Kotlin) | IETF BCP 47 locale tag (e.g. "en-US"). Auto-collected on RN's Android bridge. |
timezone | string | No (RN) / Yes (Swift, Kotlin) | IANA timezone identifier (e.g. "America/New_York"). Auto-collected on RN's Android bridge. |
screenDims | string | No (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. |
deviceModel | string | null | No | Device 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. |
installReferrer | string | null | No | Android 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:
- 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. - 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
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | No | The 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
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | Yes | The 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
| Parameter | Type | Required | Description |
|---|---|---|---|
cb | (result: FunnelClaimResult) => void | Yes | Callback 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:
| Shape | Example |
|---|---|
| Universal Link / App Link path segment | https://funnels.example.com/funnels/open/<token> |
| Query parameter (any host) | https://example.com/promo?rovenue_funnel_token=<token> |
token= on the onboarding-complete deep link | myapp://onboarding-complete?token=<token> |
Params
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Any 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.