Install the SDK
Add InfluTo to a Flutter app
Add the influto package to a Flutter app, detect referred installs, accept typed codes, and connect RevenueCat or report store purchases with influto_iap.
The influto package adds creator attribution to a Flutter app on iOS and Android. It tells you whether an install came from a creator's link or code, lets people type a creator's code, and gets each purchase to InfluTo — through RevenueCat, or straight from the App Store and Google Play with the companion package influto_iap. Plan for an afternoon; most of it goes into the purchase side.
| Package | influto, plus the optional companion influto_iap for automatic purchase capture |
| Latest version | 1.1.1 |
| Registry | pub.dev |
| Requires | Flutter 3.22+, Dart 3.4+ |
| Dependencies | http and shared_preferences (influto_iap adds in_app_purchase) |
| Source | github.com/influto/influto-flutter (MIT) |
Install
flutter pub add influto
# optional: automatic store-direct purchase capture
flutter pub add influto_iapAdd influto_iap only if your app is store-direct (no RevenueCat) and sells through the in_app_purchase plugin. RevenueCat apps using purchases_flutter only need influto.
Initialize
import 'package:influto/influto.dart';
// Call once at startup
await InfluTo.instance.initialize(
InfluToConfig(
apiKey: 'YOUR_API_KEY',
// RevenueCat apps: let the SDK tag the subscriber
// revenueCatHook: (attrs) => Purchases.setAttributes(attrs),
),
);Call initialize once at startup and wait for it before calling anything else — if it hasn't succeeded, checkAttribution and reportPurchase throw an InfluToException. It throws when it fails (a wrong key, no network), so catch the error, log it, and carry on; the next launch tries again. Don't hold up your first frame for it. A typical startup, in order:
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:influto/influto.dart';
import 'package:purchases_flutter/purchases_flutter.dart'; // only if you use RevenueCat
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Purchases.configure(PurchasesConfiguration('appl_…')); // RevenueCat apps: configure it first
runApp(const MyApp());
unawaited(startInfluTo());
}
Future<void> startInfluTo() async {
try {
await InfluTo.instance.initialize(InfluToConfig(
apiKey: const String.fromEnvironment('INFLUTO_API_KEY'),
appVersion: '1.4.0',
revenueCatHook: (attrs) => Purchases.setAttributes(attrs),
));
} catch (e) {
debugPrint('InfluTo initialize failed: $e'); // retried on the next launch
return;
}
final attribution = await InfluTo.instance.checkAttribution();
await InfluTo.instance.identifyUser(await Purchases.appUserID);
}The API key starts with it_ and is on Setup & SDK; one key works for development and production. Keep it out of committed source — for example pass it at build time with --dart-define=INFLUTO_API_KEY=it_… and read it with String.fromEnvironment.
| Parameter | Type | Default | What it does |
|---|---|---|---|
apiKey |
String |
required | The app's SDK key (it_…) |
debug |
bool |
false |
Prints [InfluTo] lines with debugPrint |
appVersion |
String? |
null (sent as 'unknown') |
Your app version, sent with the startup call |
autoCapture |
bool |
true |
Store-direct apps: lets the influto_iap observer report purchases. Set false if you call reportPurchase yourself |
revenueCatHook |
Future<void> Function(Map<String, String>)? |
null |
RevenueCat apps: how the SDK sets the subscriber attributes |
httpClient |
http.Client? |
null |
A custom HTTP client, for tests |
Detect referred users
final attribution = await InfluTo.instance.checkAttribution();
if (attribution.attributed) {
// User came from a referral — show a special offer.
print('Referred by ${attribution.referralCode}');
}AttributionResult has these fields:
| Field | Meaning |
|---|---|
attributed |
true when the install came from a creator's link or code |
referralCode |
The creator's code, uppercase |
attributionMethod |
'ip_device_match' (link-click matching) or 'manual_entry' (a code the user applied) |
clickedAt |
When the link was clicked, or the code applied |
message |
A short explanation, for logs |
Attribution is decided once per install and stored on the device; later calls return the stored answer without a request. Only a failed request is retried, on the next call. A reinstall starts over.
checkAttribution is fail-soft: on a network or server error it returns attributed: false and stores nothing. Link-click matching only counts taps from the last 72 hours — see attribution. To show referred users a better offer, see Reward referred users.
Identify the user
Call identifyUser as soon as you know who the user is, with the same ID your purchases are keyed on — for RevenueCat, the app user ID:
await InfluTo.instance.identifyUser(await Purchases.appUserID);It matters for three things: InfluTo uses it to credit a RevenueCat purchase when the influto_code attribute didn't make it, checkAccess uses it for free-access codes, and reportPurchase sends it by default. Call it once per sign-in, ideally in the user's first session — not from build.
Track events
await InfluTo.instance.trackEvent(TrackEventOptions(
eventType: 'paywall_viewed',
appUserId: userId,
properties: {'placement': 'onboarding'},
));Events show on Real-Time Events and help you read the funnel. They never create conversions or commission — only validated purchases do. Every call gets an event ID, so an accidental double call counts once. For trial_started, subscription_purchased and subscription_renewed the ID is derived from the event, user and properties, so the same event counts once even across launches; include something that changes per occurrence (a transaction ID, say) in properties when the event can repeat. For your own once-only events, pass eventId.
Let users type a code
// Validate + apply a code the user typed
final result = await InfluTo.instance.applyCode('FITGURU30');
if (result.valid && result.applied == true) {
// Unlock the offer
}
// Or the ready-made widget: ReferralCodeInput(onApplied: (result) { … })
// Free-access codes (after identifyUser): (await setReferralCode(code)).grantsAccess
// == true → unlock premium, and gate premium on (await checkAccess()).hasAccess.applyCode(code, appUserId: …) validates the code and applies it if it's valid; the result's applied tells you whether it was applied. Codes are trimmed and uppercased for you. An invalid code comes back with valid: false and an errorCode: INVALID_FORMAT, CODE_NOT_FOUND, CODE_EXPIRED or NETWORK_ERROR. If you build your own field, write your own copy: message and influencer can contain the creator's personal name.
The ready-made field is the ReferralCodeInput widget (Material). It validates as the user types and applies the code when they tap the button:
ReferralCodeInput(
appUserId: userId,
onApplied: (result) { /* the code is applied — unlock the offer */ },
)| Parameter | Default | Notes |
|---|---|---|
appUserId |
identified user | The user to bind the code to; free-access codes need one |
autoPrefill / autoValidate |
true / false |
Pre-fill the code from a link-attributed install, and validate it right away |
showCampaignName / showReferrerName |
false / false |
Show a creator's name only with their consent |
title, placeholder, applyLabel, validMessage, invalidMessage |
— | Texts, for translation |
onValidated, onApplied |
— | Called after each validation, and after the code is applied |
More on the UX: Let users enter a referral code. All parameters: UI components.
Purchases
InfluTo only pays commission on purchases it has verified with the store. Choose exactly one path per platform — see Choose a validation path.
RevenueCat
Pass a revenueCatHook, as in the startup example above. When checkAttribution finds a referral, or a code is applied, the SDK calls it with influto_code (the code) and influto_referral (the string "true"). Then connect the webhook — see RevenueCat.
Store-direct with influto_iap
Create one InfluToPurchaseObserver and enable it after initialize has finished:
import 'package:influto_iap/influto_iap.dart';
final observer = InfluToPurchaseObserver(
oneTimeProductIds: {'coins_100', 'remove_ads'}, // Android one-time products; omit if you only sell subscriptions
);
final sync = await observer.enable(); // listens for new purchases + back-fills existing onesThe observer only runs when the app is set to store-direct in Settings → Validation and autoCapture is true; otherwise enable() does nothing, so the same build is safe on RevenueCat apps. Each purchase is reported once. enable() returns a PurchaseSyncResult with fetched, sent and failed counts; syncExisting(), disable() and dispose() are there for manual control.
What the observer does in your app:
- It completes pending purchases (
completePurchase), so the store doesn't redeliver or refund them. Deliver content from your ownpurchaseStreamlistener as usual. - The back-fill calls
restorePurchases(), so your own listener also seesrestoredpurchases at startup. Callenable(backSync: false)if you don't want that. - On iOS it switches
in_app_purchaseto StoreKit 2, which store-direct needs.
To report purchases yourself instead, set autoCapture: false, skip the observer, and call reportPurchase:
// Store-direct only (no RevenueCat). For automatic capture add influto_iap and
// call InfluToPurchaseObserver().enable() after initialize(). Or report manually:
await InfluTo.instance.reportPurchase(
platform: 'ios', // or 'android'
signedTransaction: jws, // iOS: StoreKit 2 JWS
// purchaseToken: token, // Android: Play Billing purchaseToken
);platformis required:'ios'or'android'. On iOS pass the StoreKit 2 signed transaction,details.verificationData.serverVerificationData— within_app_purchaseon StoreKit 2 (InAppPurchaseStoreKitPlatform.enableStoreKit2()makes sure of it); on Android pass(details as GooglePlayPurchaseDetails).billingClientPurchase.purchaseToken.- It throws an
InfluToException.retryableis true for503(the exchange rate was briefly unavailable) — retry later;notConfiguredis true for400(the platform isn't set to store-direct yet). - Android one-time and consumable products need
productId, pluspriceandcurrencyfrom the product details; without a price the purchase is recorded at $0. A409means the purchase isn't in the PURCHASED state yet.
Set-up, credentials and testing: Store-direct.
Free-access codes
Free-access codes give a user premium without a purchase. Redeem one with setReferralCode(code, appUserId: userId) after identifyUser, or pass the user ID — a code with no user is refused. The SetCodeResult tells you what happened: grantsAccess, freeAccess, entitlement and expiresAt. applyCode doesn't return these flags; after it, result.campaign?.campaignType == 'FREE_ACCESS' tells you the code was a free-access code.
Gate premium on your store entitlement or InfluTo's answer:
final premium = entitlementActive || (await InfluTo.instance.checkAccess()).hasAccess;checkAccess(appUserId: …) uses the identified user when you don't pass one, caches a positive answer for 5 minutes (across launches too), never caches a negative one, and returns hasAccess: false on errors. See Free access codes.
Verify
After the first successful initialize, Setup & SDK shows "Integrated — SDK connected" with the platform and SDK version it last saw. With debug: true the SDK prints each step, and Real-Time Events lists your events and purchases. For the full end-to-end test and the integration status checklist, see Verify the integration.
To test attribution again on the same device, clearAttribution() forgets the stored attribution, code and user ID. It's for testing only — a reinstall gives a cleaner start.
Flutter notes
- One instance. Everything goes through
InfluTo.instance; there are no static calls. - Order matters: configure RevenueCat, then
initialize, thencheckAttribution; enable theinfluto_iapobserver only afterinitializehas finished, or it stays off. constconfigs:InfluToConfighas aconstconstructor, but a closure such as(attrs) => …isn't a constant — dropconstwhen you pass arevenueCatHook.- No automatic RevenueCat tagging. Unlike React Native, Flutter needs the
revenueCatHook. - Android one-time products go in the observer's
oneTimeProductIds, not inInfluToConfig.