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.
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.
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.
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.
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.
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
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.
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.
Platform differences
Where the stores differ, the parameter is documented as iOS-only or Android-only and ignored on the other platform.
| iOS | Android | |
|---|---|---|
| Product IDs | App Store product ID | productId:basePlanId per base plan; plain ID for one-time products |
purchase(offer:) | Not used | One of the product's Play offers; defaults to its introductoryOffer |
purchase(promoOffer:) | Server-signed promotional offer | Not used |
| Account link | appAccountToken, a UUID | obfuscatedAccountId |
environment | From the signature | Overrides debuggable → sandbox, release → production |
consumableProductIds | Not used; StoreKit knows | Products RevFox consumes instead of acknowledging |
setPushToken | APNs token as hex, from FirebaseMessaging.getAPNSToken() | FCM registration token |
setTrackingAuthorizationStatus | ATT status | Ignored |
enableAdServicesAttributionTokenCollection | Apple Search Ads attribution | Ignored |
collectDeviceIdentifiers | IDFV; IDFA and ATT answer with AdSupport and consent | Google advertising ID, with play-services-ads-identifier |
