SDK / FLUTTER

Flutter SDK.

One Dart API over the native iOS and Android SDKs, so behaviour matches theirs exactly. Everything is a static method on RevFox.

01

Requirements

  • Flutter 3.24 or later, Dart 3.5 or later.
  • iOS 15 or later, Android 7.0 (API 24, Flutter's own minimum) or later.
  • A RevFox server with an App Store app and a Play Store app. Each has its own SDK API key.
02

Install

dependencies:
  revfox: ^0.1.0

iOS. RevFox uses StoreKit 2, so raise the platform in ios/Podfile:

platform :ios, '15.0'

Android. Nothing to do. The plugin adds the JitPack repository itself. If your settings.gradle(.kts) uses RepositoriesMode.FAIL_ON_PROJECT_REPOS, add maven("https://jitpack.io") to its repositories.

03

Configure

import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:revfox/revfox.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  if (kDebugMode) await RevFox.setLogLevel(RevFoxLogLevel.debug);
  await RevFox.configure(
    RevFoxConfiguration(
      apiKey: Platform.isIOS ? 'apps_…' : 'play_…',
      baseUrl: 'https://api.example.com',
    ),
  );
  runApp(const MyApp());
}

RevFoxConfiguration takes apiKey, baseUrl, customerId, transactionFinisher and allowInsecure, plus the Android-only environment and consumableProductIds. They mean the same as in the iOS and Android SDKs.

Calling configure again with the same configuration, as a hot restart does, does nothing. A different configuration throws already_initialized.

04

Products and purchases

final package = await RevFox.getPackage('default');

switch (await RevFox.purchase(package.products.first)) {
  case RevFoxPurchaseSuccess(:final customer):
    unlock(customer.benefits);
  case RevFoxPurchaseCancelled():
    break;
  case RevFoxPurchasePending():
    showPendingMessage();
}

RevFox.getProducts(ids) fetches products by ID. On Android a subscription ID can name one base plan, productId:basePlanId, or the whole subscription.

purchase takes optional offer (Android), promoOffer (iOS), appAccountToken (iOS) and obfuscatedAccountId (Android). As on the native SDKs, a purchase the store approved is never reported as failed because your server was unreachable.

05

Check benefits

RevFox.customerUpdates.listen((customer) {
  setState(() => isPro = customer.benefits.contains('pro'));
});

// Or with a callback:
void onCustomer(RevFoxCustomer customer) => setState(() => isPro = customer.benefits.contains('pro'));
RevFox.addCustomerUpdateListener(onCustomer);
// in dispose():
RevFox.removeCustomerUpdateListener(onCustomer);

customerUpdates starts with the cached customer, so a paywall can render before the network answers. await RevFox.getCustomer() returns the latest snapshot and await RevFox.customerId the ID in use.

06

Sign in, sign out and restore

await RevFox.logIn('8f14e45f-ceea-4672-9a1f-2a1b0c3d4e5f'); // anything bought anonymously moves over
await RevFox.logOut();                                     // throws already_anonymous if nobody is signed in
final customer = await RevFox.restorePurchases();          // for your Restore Purchases button
07

Observer mode

Keep your own purchase code, for example in_app_purchase. Set transactionFinisher: RevFoxTransactionFinisher.manual and report each completed purchase. RevFox then never finishes, acknowledges or consumes anything, and RevFox.purchase throws purchase_unavailable.

// iOS: the StoreKit transaction ID
await RevFox.submitAppStoreTransaction(details.purchaseID!);

// Android: the Play purchase, once its state is PURCHASED
final purchase = (details as GooglePlayPurchaseDetails).billingClientPurchase;
await RevFox.submitPlayStorePurchase(
  originalJson: purchase.originalJson,
  signature: purchase.signature,
);

submitAppStoreTransaction looks the transaction up in StoreKit on the device and sends its signed form, so only the ID crosses from Dart. It throws transaction_not_found when the device has no such transaction.

08

Attributes

await RevFox.setEmail('[email protected]');
await RevFox.setPushToken(token);
await RevFox.setAttributes({'plan': 'team'});
await RevFox.collectDeviceIdentifiers();
await RevFox.enableAdServicesAttributionTokenCollection(); // iOS only

Every native setter is available, including the attribution and integration ID setters. setAutomaticDeviceIdentifierCollectionEnabled(false) stops attribution ID setters from also collecting device identifiers.

09

Platform differences

Where the stores differ, the parameter is documented as iOS-only or Android-only and ignored on the other platform.

iOSAndroid
Product IDsApp Store product IDproductId:basePlanId per base plan; plain ID for one-time products
purchase(offer:)Not usedOne of the product's Play offers; defaults to its introductoryOffer
purchase(promoOffer:)Server-signed promotional offerNot used
Account linkappAccountToken, a UUIDobfuscatedAccountId
environmentFrom the signatureOverrides debuggable → sandbox, release → production
consumableProductIdsNot used; StoreKit knowsProducts RevFox consumes instead of acknowledging
setPushTokenAPNs token as hex, from FirebaseMessaging.getAPNSToken()FCM registration token
setTrackingAuthorizationStatusATT statusIgnored
enableAdServicesAttributionTokenCollectionApple Search Ads attributionIgnored
collectDeviceIdentifiersIDFV; IDFA and ATT answer with AdSupport and consentGoogle advertising ID, with play-services-ads-identifier
10

Errors and logging

Every failure is a RevFoxException with the native SDK's code, message and userInfo. See the iOS and Android lists.

await RevFox.setLogLevel(RevFoxLogLevel.debug);
await RevFox.setLogHandler((level, message) => debugPrint('[RevFox] $level $message'));

Writing your own client?

The SDK HTTP protocol.

Read the protocol