Rovenue SDK
Platforms

Flutter

Install and wire the Rovenue SDK in a Flutter app.

Install the package

Add the app-facing package to your pubspec.yaml:

dependencies:
  rovenue_flutter: ^0.16.0

Then fetch it:

flutter pub get

rovenue_flutter is a federated plugin — rovenue_flutter_ios and rovenue_flutter_android are pulled in automatically as platform implementations and wrap the same Swift and Kotlin SDKs (and the shared Rust core, librovenue) that back the native iOS and Android SDKs. Android needs no further setup.

iOS: the CocoaPods dependency isn't published yet. rovenue_flutter_ios's podspec depends on the Rovenue pod (packages/sdk-swift), which has not been published to CocoaPods Trunk — its podspec still carries a placeholder release-archive checksum. Until that pod ships, flutter pub get alone will not resolve on iOS for an app outside this monorepo. In the meantime, override the dependency in your ios/Podfile to point at a local checkout of sdk-swift:

target 'Runner' do
  use_frameworks!
  pod 'Rovenue', :path => '/path/to/sdk-swift'
  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

Remove the override once Rovenue is published to Trunk. rovenue_flutter_ios also raises the deployment target to iOS 16.0 to match the Rovenue pod's own floor.

Configure the SDK

Call Rovenue.instance.configure once, before any other SDK method — the earliest point in main() is the right place:

import 'package:flutter/material.dart';
import 'package:rovenue_flutter/rovenue_flutter.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Rovenue.instance.configure(
    apiKey: 'rov_pub_YOUR_API_KEY',
  );
  runApp(const MyApp());
}

Self-hosting? baseUrl is optional and defaults to the hosted endpoint https://api.rovenue.io. Override it to point at your own deployment, e.g. Rovenue.instance.configure(apiKey: 'rov_pub_YOUR_API_KEY', baseUrl: 'https://api.acme.com'). The host must use https:// — plain http:// is accepted only for localhost during local development.

Types on this page

Future<void> configure({
  required String apiKey,      // public SDK key — prefix rov_pub_
  String? baseUrl,             // optional — defaults to https://api.rovenue.io
  RovenueLogLevel logLevel = RovenueLogLevel.warn,
  String? appVersion,          // override auto-detected version
  String? environment,
});
FieldTypeDescription
apiKeyStringPublic SDK key from your Rovenue dashboard. Prefix rov_pub_.
baseUrlString?Optional. Defaults to https://api.rovenue.io. Override only when self-hosting (https://; http:// allowed for localhost only).
logLevelRovenueLogLevelVerbosity of SDK logs (off/error/warn/info/debug/trace). Defaults to warn.
appVersionString?Overrides the version auto-read from the app bundle.
environmentString?Optional environment label forwarded with requests.

Wire foreground signals

The SDK uses setForeground to manage its internal polling cadence. Wire it to WidgetsBindingObserver:

import 'package:flutter/material.dart';
import 'package:rovenue_flutter/rovenue_flutter.dart';

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    Rovenue.instance.setForeground(true);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    Rovenue.instance.setForeground(state == AppLifecycleState.resumed);
  }

  @override
  Widget build(BuildContext context) => const MaterialApp(home: RootScreen());
}

Rovenue.instance.shutdown() stops all background work (polling, in-flight refreshes). Reserve it for full app teardown — do not call it on every background transition. setForeground(false) already pauses polling when the app backgrounds.


Identifying users

Anonymous subscribers get a permanent id automatically. Call identify once your own auth flow completes, to link that install to your backend user:

await Rovenue.instance.identify('user_123');

// When your user signs out:
await Rovenue.instance.logOut();

Unlike the iOS and Android examples, packages/sdk-flutter/example does not call identify() — it demonstrates the anonymous-subscriber path only. The method exists and works identically on Flutter; add this step if your app has its own auth. See Identifying Users for the full identity model.


Entitlements

final pro = await Rovenue.instance.entitlement('pro');
if (pro?.active ?? false) {
  // unlock the feature
}

final all = await Rovenue.instance.entitlementsAll();
// Entitlement
class Entitlement {
  final String id;
  final bool active;
  final String? expiresAt;   // ISO 8601 — absent for non-expiring grants
  final String? productId;
}

Listen for server-pushed updates with Rovenue.instance.changes and re-read on any event:

final sub = Rovenue.instance.changes.listen((event) {
  // event.kind: entitlementsChanged | identityChanged |
  //             virtualCurrenciesChanged | remoteConfigChanged
  Rovenue.instance.entitlementsAll().then((updated) {
    // update your local state with `updated`
  });
});

// Cancel on teardown:
await sub.cancel();

entitlementsAll() is a local cache read — never call refreshEntitlements() from inside this listener. refreshEntitlements() hits the network and, on success, re-emits entitlementsChanged itself, which would re-invoke this same listener forever.

See Entitlements and Reacting to Changes for the full guides.


Offerings & purchases

Use getOfferings() to fetch products configured in your Rovenue dashboard, then purchase(product) to drive the full purchase flow. The SDK triggers the native StoreKit 2 / Play Billing sheet, validates the receipt server-side, and resolves with updated entitlements and virtual-currency balances. On Android the SDK acknowledges or consumes the Play purchase on the device after the server verified it, and purchase resolves even when Play cannot take that acknowledgement yet — see Who acknowledges a Play purchase.

import 'package:rovenue_flutter/rovenue_flutter.dart';

final offerings = await Rovenue.instance.getOfferings();
final products = <StoreProduct>[
  for (final offering in offerings.offerings)
    for (final package in offering.packages) package.product,
];

if (products.isNotEmpty) {
  try {
    final result = await Rovenue.instance.purchase(products.first);
    // result.entitlements and result.virtualCurrencies are already up to date
  } on RovenueException catch (e) {
    if (e.kind == RovenueErrorKind.purchaseCanceled) {
      // User dismissed the store sheet — no action needed
    } else {
      rethrow;
    }
  }
}

To re-validate all prior transactions (e.g. after a reinstall):

await Rovenue.instance.restorePurchases();

purchase also accepts an optional Android SubscriptionOption for base-plan/offer selection: Rovenue.instance.purchase(product, option: option).

See Processing Purchases for the full API reference including all error kinds and the StoreProduct shape.


Funnels

Placements serve native funnels, drawn by RovenueFunnelView (see Placements). There is no Dart getFunnel — the platform side resolves the placement itself:

import 'package:rovenue_flutter/rovenue_flutter.dart';

final onboarding = RovenueFunnelView(placementIdentifier: 'onboarding_end');

Purchase attribution is automatic: the funnel's presentedContext is attached to the next purchase() for you.

iOS: declare the languages your app supports

On iOS the funnel draws in the device's language only if your app declares that language: iOS hands an app Locale.current from its declared localizations, so an app that declares only English reaches the SDK as en_XX (en_TR on a Turkish device) and shows the funnel's English copy. Add CFBundleLocalizations (for example en, tr) to ios/Runner/Info.plist, or add the localizations to the Runner project in Xcode. Android needs nothing.


Funnel attribution

Resolve a funnel token from a deep link, an explicit install claim, or — on iOS — the clipboard:

import 'package:rovenue_flutter/rovenue_flutter.dart';

const incomingUrl = '<the URL that opened the app>';

// From a deep link / Universal Link handler:
final claim = await Rovenue.instance.claimFromUrl(incomingUrl);

// Or resolve one explicitly:
final installClaim = await Rovenue.instance.claimInstall();
// iOS only — always resolves null on Android (see the callout below):
final pastedClaim = await Rovenue.instance.claimFromClipboard();
await Rovenue.instance.claimViaEmail('user@example.com');

if (claim != null) {
  // claim.subscriberId, claim.funnelAnswersJson (JSON-encoded string)
}

Claims resolved asynchronously on the native side (a deferred deep link or a clipboard token picked up after configure) arrive on the funnelClaims stream, which replays the most recent claim to a new listener:

final sub = Rovenue.instance.funnelClaims.listen((claim) {
  // handle claim.subscriberId / claim.funnelAnswersJson
});

claimFromClipboard is an iOS-only path. On Android it always resolves null without reading the clipboard — Android's deferred-attribution source is the Play Install Referrer, which claimInstall reads for you. The method exists on both platforms so shared code needs no branching, but only claimInstall can recover a deferred Android claim.

Flutter's funnel claim is single-shot: claimFromUrl/claimInstall/claimFromClipboard each make one resolution attempt and return (or throw). The React Native SDK's resolveFunnelClaim additionally chains a retry/fallback sequence across multiple claim sources — that chain has not been ported to Flutter. Callers that need the same fallback behavior should compose it themselves from the primitives above.


Error handling

Every public method throws RovenueException — never a raw PlatformException:

import 'package:rovenue_flutter/rovenue_flutter.dart';

try {
  await Rovenue.instance.identify('user_123');
} on RovenueException catch (e) {
  switch (e.kind) {
    case RovenueErrorKind.networkUnavailable:
    case RovenueErrorKind.timeout:
      // e.retryable is true for these — safe to retry
      break;
    case RovenueErrorKind.invalidApiKey:
    case RovenueErrorKind.forbidden:
      // configuration problem — do not retry
      break;
    default:
      print('Rovenue error: ${e.kind.name} — ${e.detail}');
  }
}
class RovenueException implements Exception {
  final RovenueErrorKind kind;
  final String detail;       // always non-null
  final String? serverCode;  // present for API-originated errors
  final int? httpStatus;     // present for API-originated errors
  final bool retryable;
}
FieldTypeDescription
kindRovenueErrorKindNormalized error category, shared across every Rovenue SDK façade.
detailStringHuman-readable message. Falls back to the kind's name when the native side sent none.
serverCodeString?Server-issued error code, when the error came from an API response.
httpStatusint?HTTP status code, when the error came from an API response.
retryableboolWhether the operation is safe to retry.

Feature parity with React Native

Flutter tracks the React Native SDK as its parity bar. As of this release:

AreaStatus
Configure, identity, entitlements, virtual currenciesFull parity
Offerings, purchase, restore, subscription optionsFull parity
Remote config, experimentsFull parity
setFallbackPlacementsFull parity
Funnel token extraction (extractFunnelToken), claimFromUrl/claimInstall/claimViaEmail/claimFromClipboardFull parity
resolveFunnelClaim retry/fallback chainNot ported — see Funnel attribution above

Funnels

The widget hosts the native renderer as a platform view — there is no Dart getFunnel (packages/sdk-flutter/rovenue_flutter/lib/src/funnel_view.dart:10-14) — full path: Show a funnel.

RovenueFunnelView buildOnboardingFunnel() {
  return const RovenueFunnelView(placementIdentifier: 'onboarding');
}

What's next

On this page