Rovenue SDK
Reference

Types

Data types returned by the Rovenue SDK.

Type gallery: RovenueConfig · User · Entitlement · ProductType · StoreProduct · Package · Offering · Offerings · PurchaseResult · ChangeEvent · LogEntry

consumable products in ProductType add virtual-currency balances — configured in the Rovenue dashboard — to the subscriber's account on purchase.


RovenueConfig

The configuration object passed to configure() on React Native. Swift and Kotlin use labelled parameters directly.

type RovenueConfig = {
  apiKey: string;
  baseUrl?: string;
  logLevel?: 'off' | 'error' | 'warn' | 'info' | 'debug' | 'trace';
  appVersion?: string;
};

Fields

FieldTypeRequiredDescription
apiKeystringYesYour project's public API key. Prefix rov_pub_. Obtain it from the Rovenue dashboard → API Keys.
baseUrlstringNoAPI endpoint URL. Defaults to the hosted endpoint https://api.rovenue.io; override only when self-hosting (https://; http:// allowed for localhost only).
logLevel'off' | 'error' | 'warn' | 'info' | 'debug' | 'trace'NoVerbosity of SDK logs. Defaults to 'warn' — turn up to 'info'/'debug' locally, never ship 'debug'/'trace' in production.
appVersionstringNoApp version string forwarded to the server. Defaults to the value reported by the OS.

Returned by / used by: configure()

Usage

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
});

User

Represents the current device's identity.

type User = {
  rovenueId: string;
  appUserId: string | null;
};

Fields

FieldTypeDescription
rovenueIdstringPermanent per-install identifier generated on first launch (prefix rov_). Always present; never changes for the lifetime of the install.
appUserIdstring | nullYour backend's user ID, attached after a successful identify() call. null until the user is identified.

Returned by: currentUser()

Usage

const user = await Rovenue.currentUser();

if (user.appUserId) {
  console.log(`Known user: ${user.appUserId}`);
} else {
  console.log(`Anonymous install: ${user.rovenueId}`);
}
let user = await Rovenue.shared.currentUser()

if let appUserId = user.appUserId {
    print("Known user: \(appUserId)")
} else {
    print("Anonymous install: \(user.rovenueId)")
}
val user = Rovenue.shared.currentUser()

if (user.appUserId != null) {
    Log.d("App", "Known user: ${user.appUserId}")
} else {
    Log.d("App", "Anonymous install: ${user.rovenueId}")
}

Entitlement

Represents a single access entitlement. An entitlement is active when the user has a valid subscription or has made a one-time unlock purchase for the corresponding product.

type Entitlement = {
  id: string;
  isActive: boolean;
  productIdentifier: string;
  store: string;
  expiresIso?: string;
};

Fields

FieldTypeDescription
idstringThe entitlement key as configured in the Rovenue dashboard (e.g. "pro", "credits_pack").
isActivebooleantrue if the entitlement is currently active (not expired, not refunded). Always check this field before granting access.
productIdentifierstringThe store product ID that granted this entitlement (e.g. "com.example.pro_monthly").
storestringThe store that originated the purchase. One of "app_store", "play_store", or "stripe".
expiresIsostring | undefinedISO 8601 expiry timestamp (e.g. "2026-12-31T23:59:59Z"). undefined for non-expiring (lifetime) entitlements.

Returned by: entitlement(id), entitlementsAll(), PurchaseResult.entitlements

Usage

const ent = await Rovenue.entitlement('pro');

if (ent?.isActive) {
  // Grant Pro access
  const expiresAt = ent.expiresIso
    ? new Date(ent.expiresIso)
    : null; // null = lifetime
}
let ent = await Rovenue.shared.entitlement(id: "pro")

if ent?.isActive == true {
    // Grant Pro access
    let expiresAt = ent?.expiresIso.flatMap { ISO8601DateFormatter().date(from: $0) }
}
val ent = Rovenue.shared.entitlement(id = "pro")

if (ent?.isActive == true) {
    // Grant Pro access
    val expiresAt = ent.expiresIso?.let { Instant.parse(it) } // null = lifetime
}

ProductType

A string enum describing what a store product delivers.

type ProductType = 'subscription' | 'consumable' | 'non_consumable';

Values

ValueDescription
subscriptionAuto-renewing subscription. Grants entitlements for the subscription period.
consumableOne-time purchase that adds credits to the user's balance.
non_consumableOne-time permanent unlock. Grants a lifetime entitlement.

Used by: StoreProduct.type


StoreProduct

Represents a store product with localised pricing and subscription metadata from the App Store or Google Play. You receive this as Package.product — you do not construct it directly.

Basic schema

type StoreProduct = {
  // Core fields
  id: string;
  type: ProductType;
  productCategory: 'subscription' | 'nonSubscription';
  displayName: string;
  description: string | null;
  priceString: string | null;
  price: number | null;
  currencyCode: string | null;

  // Subscription metadata
  subscriptionPeriod: Period | null;
  subscriptionGroupIdentifier: string | null;  // iOS only
  isFamilyShareable: boolean;  // iOS only
  introPrice: IntroPrice | null;  // iOS only
  discounts: Discount[];  // iOS promotional offers (empty on Android)
  isEligibleForIntroOffer: boolean | null;

  // Android subscription options (null on iOS)
  subscriptionOptions: SubscriptionOption[] | null;
  defaultOption: SubscriptionOption | null;

  // Per-unit pricing (approximated)
  pricePerWeek: number | null;
  pricePerMonth: number | null;
  pricePerYear: number | null;
  pricePerWeekString: string | null;
  pricePerMonthString: string | null;
  pricePerYearString: string | null;

  // Platform-native escape hatch (Swift & Kotlin only; null on React Native)
  rawStoreProduct: StoreKit.Product | ProductDetails | null;
};

Core Fields

FieldTypeDescription
idstringThe store product identifier (e.g. "com.example.pro_monthly").
typeProductTypeWhat the product delivers: subscription, consumable, or non-consumable.
productCategory'subscription' | 'nonSubscription'Product category from the store. Maps to type: subscriptions have category 'subscription'; consumables and non-consumables have 'nonSubscription'.
displayNamestringLocalised product name from the store (e.g. "Pro Monthly").
descriptionstring | nullLocalised product description. null if not provided by the store.
priceStringstring | nullLocalised, formatted price string (e.g. "$4.99", "€4,99"). Display this directly in your paywall. null if unavailable.
pricenumber | nullNumeric price in the user's currency (e.g. 4.99). null if unavailable.
currencyCodestring | nullISO 4217 currency code (e.g. "USD", "EUR"). null if unavailable.

Subscription Fields

FieldTypePlatformDescription
subscriptionPeriodPeriod | nullAllThe billing interval (e.g. 1 month, 1 year). null for non-subscription products.
subscriptionGroupIdentifierstring | nulliOSThe App Store subscription group. Groups related subscriptions so users can switch between them. null on Android and non-subscription products.
isFamilyShareablebooleaniOStrue if eligible for family sharing on iOS. Always false on Android.
introPriceIntroPrice | nulliOSThe introductory offer (free trial or discounted period). iOS only; null on Android.
discountsDiscount[]iOSiOS promotional offers (introductory + winback offers). Empty array on Android.
isEligibleForIntroOfferboolean | nullAlliOS: the real StoreKit value (true if intro offer eligible). Android: derived; true iff a free or intro phase is present. Play enforces actual eligibility at purchase time. null if unavailable.
subscriptionOptionsSubscriptionOption[] | nullAndroidAndroid subscription options (base plans + offers). null on iOS.
defaultOptionSubscriptionOption | nullAndroidThe recommended option for this product (e.g. the base plan without an offer). null on iOS.

Per-Unit Pricing (Approximated)

FieldTypeDescription
pricePerWeeknumber | nullEstimated weekly price, calculated from subscriptionPeriod and price (7 days per week). For a 2-week plan priced at $5, this is ~$3.50/week. An approximation only; exact billing varies by period.
pricePerMonthnumber | nullEstimated monthly price (30 days per month).
pricePerYearnumber | nullEstimated annual price (365 days per year).
pricePerWeekStringstring | nullLocalised price-per-week string.
pricePerMonthStringstring | nullLocalised price-per-month string.
pricePerYearStringstring | nullLocalised price-per-year string.

Platform-Native Escape Hatch

FieldTypePlatformDescription
rawStoreProductStoreKit.Product (iOS) / ProductDetails (Android) / null (RN)Swift & KotlinDirect reference to the platform's native product object. Use only when the Rovenue wrapper is insufficient. Not available in React Native.

Used by: Package.product

Usage

const pkg = offerings.current?.packages[0];
if (pkg) {
  const {
    displayName, priceString, type, description, subscriptionPeriod,
    pricePerMonthString, isEligibleForIntroOffer
  } = pkg.product;
  
  console.log(`${displayName} — ${priceString}`);
  console.log(`Description: ${description}`);
  
  if (subscriptionPeriod) {
    console.log(`Billing: every ${subscriptionPeriod.value} ${subscriptionPeriod.unit}`);
    console.log(`~${pricePerMonthString}/month`);
  }
  
  if (isEligibleForIntroOffer) {
    console.log('Eligible for intro offer');
  }
}
let pkg = offerings.current?.packages.first
if let product = pkg?.product {
  print("\(product.displayName) — \(product.priceString ?? "N/A")")
  
  if let period = product.subscriptionPeriod {
    print("Billing: every \(period.value) \(period.unit)")
    print("~\(product.pricePerMonthString ?? "N/A")/month")
  }
  
  if let intro = product.introPrice {
    print("Intro: \(intro.priceString ?? "free") for \(intro.cycles) cycles")
  }
  
  // Access promotional offers (iOS-specific)
  for discount in product.discounts {
    print("Promo: \(discount.priceString ?? "free")")
  }
  
  // Access native StoreKit.Product if needed
  if let native = product.rawStoreProduct {
    print("Native product: \(native.id)")
  }
}
val pkg = offerings.current?.packages.firstOrNull()
pkg?.product?.let { product ->
  println("${product.displayName} — ${product.priceString ?: "N/A"}")
  
  product.subscriptionPeriod?.let { period ->
    println("Billing: every ${period.value} ${period.unit}")
    println("~${product.pricePerMonthString ?: "N/A"}/month")
  }
  
  product.introPrice?.let { intro ->
    println("Intro: ${intro.priceString ?: "free"} for ${intro.cycles} cycles")
  }
  
  // Access Android subscription options
  product.subscriptionOptions?.forEach { option ->
    println("Option: ${option.id}")
    option.pricingPhases.forEach { phase ->
      println("  Phase: ${phase.priceString} (${phase.recurrenceMode})")
    }
  }
  
  // Access native ProductDetails if needed
  product.rawStoreProduct?.let { native ->
    println("Native product: ${native.productId}")
  }
}

Supporting types for StoreProduct

Period

Represents a billing or time interval.

type Period = {
  value: number;
  unit: 'day' | 'week' | 'month' | 'year';
  iso8601: string;
};

Fields

FieldTypeDescription
valuenumberThe magnitude (e.g. 1, 3, 12).
unit'day' | 'week' | 'month' | 'year'The time unit.
iso8601stringISO 8601 duration string (e.g. "P1M" for 1 month, "P1Y" for 1 year). Useful for standardised duration calculations.

Used by: StoreProduct.subscriptionPeriod, IntroPrice.period, Discount.period, PricingPhase.billingPeriod

Example

const period = product.subscriptionPeriod;
if (period?.unit === 'month' && period.value === 1) {
  console.log('Billing monthly'); // period.iso8601 → "P1M"
}

IntroPrice

The introductory offer on an iOS subscription (free trial or discounted period).

type IntroPrice = {
  price: number | null;
  priceString: string | null;
  currencyCode: string | null;
  period: Period;
  cycles: number;
  paymentMode: 'freeTrial' | 'payAsYouGo' | 'payUpFront';
};

Fields

FieldTypeDescription
pricenumber | nullThe price per cycle (e.g. 0 for free trial, 0.99 for discounted). null if unavailable.
priceStringstring | nullLocalised price string. null if unavailable.
currencyCodestring | nullISO 4217 code. null if unavailable.
periodPeriodThe duration of each intro cycle (e.g. 1 week free).
cyclesnumberHow many intro periods apply (e.g. 1 = 1-week free trial; 3 = 3-week trial).
paymentMode'freeTrial' | 'payAsYouGo' | 'payUpFront'When payment is charged: 'freeTrial' (after trial ends), 'payAsYouGo' (each cycle, starting now), 'payUpFront' (upfront).

Platform: iOS only. null on Android (use subscriptionOptions.introPhase instead).

Used by: StoreProduct.introPrice

Example

const intro = product.introPrice;
if (intro) {
  if (intro.paymentMode === 'freeTrial' && intro.price === 0) {
    console.log(`${intro.cycles}-${intro.period.unit} free trial`);
  } else {
    console.log(`${intro.priceString}/${intro.period.value} ${intro.period.unit} for ${intro.cycles} cycles`);
  }
}

Discount

An iOS promotional offer (winback or ongoing discount on a subscription).

type Discount = {
  identifier: string | null;
  price: number | null;
  priceString: string | null;
  currencyCode: string | null;
  period: Period;
  numberOfPeriods: number;
  paymentMode: 'freeTrial' | 'payAsYouGo' | 'payUpFront';
  type: 'introductory' | 'promotional' | 'winBack';
};

Fields

FieldTypeDescription
identifierstring | nullThe offer identifier from the App Store dashboard. null if unavailable.
pricenumber | nullThe discounted price per cycle. null if unavailable.
priceStringstring | nullLocalised price string.
currencyCodestring | nullISO 4217 code.
periodPeriodThe duration of each discount cycle.
numberOfPeriodsnumberHow many cycles the discount applies (e.g. 3 = 3 months at discount price).
paymentMode'freeTrial' | 'payAsYouGo' | 'payUpFront'When payment is charged.
type'introductory' | 'promotional' | 'winBack'Offer type: introductory (new subs only, applied automatically), promotional (all users), or winBack (lapsed subs). Note: only .promotional and .winBack can be passed to purchase(); .introductory is applied automatically by the App Store.

Platform: iOS only. Empty array on Android.

Used by: StoreProduct.discounts

See also: Redeeming promotional offers (iOS)


PricingPhase

An Android subscription pricing phase (free, intro, or full price; part of a SubscriptionOption).

type PricingPhase = {
  price: number | null;
  priceString: string | null;
  currencyCode: string | null;
  billingPeriod: Period;
  billingCycleCount: number | null;
  recurrenceMode: 'infiniteRecurring' | 'finiteRecurring' | 'nonRecurring';
  paymentMode: 'freeTrial' | 'payAsYouGo' | 'payUpFront' | null;
};

Fields

FieldTypeDescription
pricenumber | nullPrice per cycle in this phase. 0 for free phases. null if unavailable.
priceStringstring | nullLocalised price string.
currencyCodestring | nullISO 4217 code.
billingPeriodPeriodHow often the user is billed in this phase.
billingCycleCountnumber | nullHow many billing cycles this phase lasts (e.g. 1 for a 1-month free trial, 12 for year-long offer). null for infinite recurrence.
recurrenceMode'infiniteRecurring' | 'finiteRecurring' | 'nonRecurring''infiniteRecurring' (lasts forever), 'finiteRecurring' (ends after billingCycleCount cycles), 'nonRecurring' (one-time).
paymentMode'freeTrial' | 'payAsYouGo' | 'payUpFront' | nullWhen charged. null if unavailable.

Platform: Android only. Not used on iOS.

Used by: SubscriptionOption.pricingPhases, SubscriptionOption.freePhase, SubscriptionOption.introPhase, SubscriptionOption.fullPricePhase


SubscriptionOption

An Android subscription option (base plan + offer combination, or just base plan). Equivalent to an iOS product in a subscription group but with explicit phase structure.

type SubscriptionOption = {
  id: string;
  basePlanId: string | null;
  offerId: string | null;
  tags: string[];
  isBasePlan: boolean;
  isPrepaid: boolean;
  pricingPhases: PricingPhase[];
  freePhase: PricingPhase | null;
  introPhase: PricingPhase | null;
  fullPricePhase: PricingPhase | null;
};

Fields

FieldTypeDescription
idstringUnique identifier for this option (e.g. "base", "base:offer_30_day_trial").
basePlanIdstring | nullThe base plan this option belongs to (e.g. "base", "annual"). null if unavailable.
offerIdstring | nullThe offer ID if this is a base plan + offer combo (e.g. "30_day_trial"). null if this is the base plan without an offer.
tagsstring[]User-assigned tags from the Play Console (e.g. ["trial", "new_user"]). Empty if none.
isBasePlanbooleantrue if this option is the base plan (no offer applied).
isPrepaidbooleantrue if prepaid (e.g. annual upfront).
pricingPhasesPricingPhase[]All pricing phases in order (free → intro → full price, or just full price). Used to compute net price.
freePhasePricingPhase | nullThe free phase if present (e.g. 2-week free trial). null if none.
introPhasePricingPhase | nullThe introductory discounted phase if present. null if none.
fullPricePhasePricingPhase | nullThe full-price phase that lasts indefinitely (the recurring rate). null if unavailable.

Platform: Android only. null on iOS.

Used by: StoreProduct.subscriptionOptions, StoreProduct.defaultOption

Example

const option = product.defaultOption || product.subscriptionOptions?.[0];
if (option) {
  console.log(`Option: ${option.id}`);
  option.pricingPhases.forEach((phase, i) => {
    console.log(`Phase ${i}: ${phase.priceString} (${phase.recurrenceMode})`);
  });
  
  if (option.freePhase) {
    console.log(`Free trial: ${option.freePhase.billingCycleCount} ${option.freePhase.billingPeriod.unit}`);
  }
  
  if (option.fullPricePhase) {
    console.log(`Then: ${option.fullPricePhase.priceString}/${option.fullPricePhase.billingPeriod.value} ${option.fullPricePhase.billingPeriod.unit}`);
  }
}

PackageType

A convenience enum classifying packages into common categories (e.g. monthly, annual, lifetime) for UI presentation.

type PackageType =
  | 'unknown' | 'custom'
  | 'lifetime' | 'annual' | 'sixMonth' | 'threeMonth' | 'twoMonth' | 'monthly' | 'weekly';

Values

ValueDescription
'lifetime'One-time permanent unlock.
'annual'1-year subscription.
'sixMonth'6-month subscription.
'threeMonth'3-month subscription.
'twoMonth'2-month subscription.
'monthly'1-month subscription.
'weekly'1-week subscription.
'custom'Billed on a custom interval (e.g. 2 weeks, 90 days).
'unknown'Unable to determine. Fallback for unrecognized patterns.

Used by: Package.packageType


Offering convenience accessors

The Offering type provides accessors to quickly find packages by type:

React Native does not expose convenience accessors on the Offering object itself. Use Package.packageType to filter packages manually, or look them up by identifier:

const offerings = await Rovenue.getOfferings();
const offering = offerings.current;

// Find by packageType
const monthly = offering?.packages.find(p => p.packageType === 'monthly');

// Find by identifier
const annual = offering?.packages.find(p => p.identifier === 'annual');

Swift Offering includes convenience properties:

let offering = offerings.current

// Access by PackageType enum
let monthly = offering?.monthly    // Package? matching .monthly
let annual = offering?.annual      // Package? matching .annual
let weekly = offering?.weekly      // Package? matching .weekly
let sixMonth = offering?.sixMonth
let threeMonth = offering?.threeMonth
let twoMonth = offering?.twoMonth
let lifetime = offering?.lifetime

// Or look up by identifier
let custom = offering?.package(identifier: "pro_custom")

Kotlin Offering includes convenience properties:

val offering = offerings.current

// Access by PackageType enum
val monthly = offering?.monthly    // Package? matching MONTHLY
val annual = offering?.annual      // Package? matching ANNUAL
val weekly = offering?.weekly      // Package? matching WEEKLY
val sixMonth = offering?.sixMonth
val threeMonth = offering?.threeMonth
val twoMonth = offering?.twoMonth
val lifetime = offering?.lifetime

// Or look up by identifier
val custom = offering?.packageBy("\$rov_monthly")

Package

A wrapper around a StoreProduct as configured in the Rovenue dashboard. Packages are the unit you pass to purchase().

type Package = {
  identifier: string;
  packageType: PackageType;
  product: StoreProduct;
};

Fields

FieldTypeDescription
identifierstringThe package identifier as configured in the Rovenue dashboard (e.g. "monthly", "annual", "credits_100").
packageTypePackageTypeA convenience category for the package (e.g. 'monthly', 'annual', 'lifetime'). Use this to simplify paywall UI logic.
productStoreProductThe underlying store product with localised pricing and subscription metadata.

Returned by: Offering.packages (via getOfferings())

Usage

const offerings = await Rovenue.getOfferings();
const pkg = offerings.current?.packages.find(p => p.identifier === 'annual');

if (pkg) {
  console.log(`Annual plan: ${pkg.product.priceString}`);
  console.log(`Type: ${pkg.packageType}`);
  const result = await Rovenue.purchase(pkg);
}
let offerings = try await Rovenue.shared.getOfferings()
let pkg = offerings.current?.packages.first { $0.identifier == "annual" }

if let pkg = pkg {
  print("Annual plan: \(pkg.product.priceString ?? "N/A")")
  print("Type: \(pkg.packageType)")
  let result = try await Rovenue.shared.purchase(pkg)
}
val offerings = Rovenue.shared.getOfferings()
val pkg = offerings.current?.packages?.find { it.identifier == "annual" }

pkg?.let {
  println("Annual plan: ${it.product.priceString}")
  println("Type: ${it.packageType}")
  val result = Rovenue.shared.purchase(it)
}

Offering

A named group of packages configured in the Rovenue dashboard. The dashboard controls which offering is marked as current (default), allowing remote configuration changes without an app release.

type Offering = {
  id: string;
  packages: Package[];
};

Fields

FieldTypeDescription
idstringThe offering identifier as configured in the dashboard (e.g. "default", "sale_q4").
packagesPackage[]The packages belonging to this offering, in dashboard-configured order.

Used by: Offerings.current, Offerings.all

Usage

const offering = offerings.all['sale_q4'];
offering?.packages.forEach(pkg => {
  console.log(pkg.id, pkg.product.priceString);
});

Offerings

The top-level response from getOfferings().

type Offerings = {
  current: Offering | null;
  all: Record<string, Offering>;
};

Fields

FieldTypeDescription
currentOffering | nullThe offering marked as default in the dashboard. null if no default is configured.
allRecord<string, Offering>All configured offerings, keyed by their identifier.

Returned by: getOfferings()

Usage

const offerings = await Rovenue.getOfferings();

// Show the default paywall:
const packages = offerings.current?.packages ?? [];

// Or show a specific named offering (e.g. for an A/B test):
const saleOffering = offerings.all['sale_q4'];
let offerings = try await Rovenue.shared.getOfferings()

// Show the default paywall:
let packages = offerings.current?.packages ?? []

// Or show a specific offering:
let saleOffering = offerings.all["sale_q4"]
val offerings = Rovenue.shared.getOfferings()

// Show the default paywall:
val packages = offerings.current?.packages ?: emptyList()

// Or show a specific offering:
val saleOffering = offerings.all["sale_q4"]

PurchaseResult

Returned by purchase() after the store transaction and server-side validation both succeed. Contains the fully-refreshed state — no additional polling is needed.

type PurchaseResult = {
  entitlements: Entitlement[];
  virtualCurrencies: Record<string, number>;
  productId: string;
  storeTransactionId: string;
  isDeferred: boolean;
};

Fields

FieldTypeDescription
entitlementsEntitlement[]The user's full set of active entitlements after the purchase. Empty when isDeferred is true — nothing has been granted yet. Use this to update gated UI immediately.
virtualCurrenciesRecord<string, number>The user's virtual-currency balances after the purchase (currency code → amount). Reflects any balances added by a currency-pack product.
productIdstringThe store product ID that was purchased (e.g. "com.example.pro_monthly").
storeTransactionIdstringThe platform transaction ID: StoreKit 2 transaction ID on iOS, Play Billing order ID on Android. Empty string when isDeferred is true.
isDeferredbooleantrue when the purchase is pending external approval (e.g. Ask-to-Buy / parental controls) rather than completed. This is not an error — purchase() resolves normally. See Handling a deferred purchase.

Returned by: purchase()

Usage

const result = await Rovenue.purchase(pkg);

if (result.isDeferred) {
  // Ask-to-Buy — not an error. Wait for a change-stream event before granting anything.
} else {
  // Grant access based on updated entitlements:
  const hasPro = result.entitlements.some(e => e.id === 'pro' && e.isActive);
  console.log(`Pro: ${hasPro}, COINS: ${result.virtualCurrencies['COINS'] ?? 0}`);
  console.log(`Transaction: ${result.storeTransactionId}`);
}

ChangeEvent

Emitted by the change stream whenever internal SDK state is updated. React to these events to keep your UI in sync without polling.

// React Native — string union (UPPER_SNAKE_CASE — matches the Rust core's wire values)
type ChangeEvent =
  | 'ENTITLEMENTS_CHANGED'
  | 'IDENTITY_CHANGED'
  | 'VIRTUAL_CURRENCIES_CHANGED'
  | 'REMOTE_CONFIG_CHANGED';

On Swift, ChangeEvent is a Swift enum with cases .entitlementsChanged, .identityChanged, .virtualCurrenciesChanged, and .remoteConfigChanged, delivered as AsyncStream<ChangeEvent> via Rovenue.shared.changes.

On Kotlin, ChangeEvent is a Kotlin enum with constants ENTITLEMENTS_CHANGED, IDENTITY_CHANGED, VIRTUAL_CURRENCIES_CHANGED, and REMOTE_CONFIG_CHANGED, delivered as SharedFlow<ChangeEvent> via Rovenue.shared.changes.

Values

Value (RN)Swift caseKotlin constantWhen it fires
'ENTITLEMENTS_CHANGED'.entitlementsChangedENTITLEMENTS_CHANGEDAfter the entitlement cache is refreshed — e.g. after a successful purchase, refreshEntitlements(), or a foreground transition that triggers a background sync.
'IDENTITY_CHANGED'.identityChangedIDENTITY_CHANGEDAfter identify() succeeds or logOut() resets the user.
'VIRTUAL_CURRENCIES_CHANGED'.virtualCurrenciesChangedVIRTUAL_CURRENCIES_CHANGEDAfter refreshVirtualCurrencies() or a server-side grant/spend that updates the cache.
'REMOTE_CONFIG_CHANGED'.remoteConfigChangedREMOTE_CONFIG_CHANGEDAfter every successful refreshRemoteConfig(). The SDK does not diff the payload, so this also fires when the refresh returned identical values.

Emitted by: addChangeListener() (RN), Rovenue.shared.changes (Swift/Kotlin)

Usage

const unsub = Rovenue.addChangeListener(async (event) => {
  if (event === 'ENTITLEMENTS_CHANGED') {
    const ent = await Rovenue.entitlement('pro');
    setHasPro(ent?.isActive ?? false);
  }
  if (event === 'VIRTUAL_CURRENCIES_CHANGED') {
    const balances = Rovenue.virtualCurrencies();
    setCoins(balances['COINS'] ?? 0);
  }
});

return () => unsub(); // cleanup on unmount
Task {
  for await event in Rovenue.shared.changes {
    switch event {
    case .entitlementsChanged:
      let ent = await Rovenue.shared.entitlement(id: "pro")
      await MainActor.run { hasPro = ent?.isActive ?? false }
    case .virtualCurrenciesChanged:
      let balances = await Rovenue.shared.virtualCurrencyBalances()
      await MainActor.run { self.coins = balances["COINS"] ?? 0 }
    case .identityChanged:
      break
    case .remoteConfigChanged:
      break
    }
  }
}
Rovenue.shared.changes
    .onEach { event ->
        when (event) {
            ChangeEvent.ENTITLEMENTS_CHANGED -> {
                val ent = Rovenue.shared.entitlement(id = "pro")
                _hasPro.value = ent?.isActive == true
            }
            ChangeEvent.VIRTUAL_CURRENCIES_CHANGED -> {
                val balances = Rovenue.shared.virtualCurrencyBalances()
                _coins.value = balances["COINS"] ?: 0
            }
            ChangeEvent.IDENTITY_CHANGED -> { /* re-read user if needed */ }
            ChangeEvent.REMOTE_CONFIG_CHANGED -> { /* re-read flags/experiments */ }
        }
    }
    .launchIn(viewModelScope)

LogEntry

A structured log entry delivered to the handler registered via setLogHandler().

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

Fields

FieldTypeDescription
level'debug' | 'info' | 'warn' | 'error'Severity level. debug entries are only emitted when logLevel is set to 'debug' or 'trace' in RovenueConfig.
messagestringHuman-readable log message from the SDK.
timestampstringISO 8601 timestamp of when the entry was generated (e.g. "2026-06-15T10:30:00.000Z").

Used by: setLogHandler()

Usage

const unsub = Rovenue.setLogHandler((entry) => {
  if (entry.level === 'error' || entry.level === 'warn') {
    // Forward to your error tracking service
    Sentry.addBreadcrumb({
      message: entry.message,
      level: entry.level,
      timestamp: new Date(entry.timestamp).getTime() / 1000,
    });
  }
});

FunnelClaimResult

Returned by the funnel-claim methods (claimFunnelToken, claimInstall, resolveFunnelClaim, claimFromClipboard, claimFromUrl) when a prior funnel session is matched, or delivered via addFunnelClaimListener.

// 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
};

Fields

FieldTypeDescription
subscriberIdstringRovenue subscriber ID of the matched funnel session.
funnelAnswersRecord<string, unknown>Key-value map of funnel step answers, already parsed. React Native only — see below.

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.

Returned by: claimFunnelToken(), claimInstall(), resolveFunnelClaim(), claimFromClipboard(), claimFromUrl()

On this page