Flutter SDK Purchases
Tracking a purchase
trackExternalPurchase(MagifyPurchase purchase) reports a purchase — one-off or
subscription. Per-product limit counters are reset, and, depending on what you pass in
ios/android, the purchase may be queued for server-side validation — see
Server-side validation below.
MagifyPurchase has two factories:
MagifyPurchase.inApp({
required String productId,
required String price, // decimal number as a string
required String currency,
required String transactionId,
MagifyIosPurchase ios = const MagifyIosPurchase(),
MagifyAndroidPurchase android = const MagifyAndroidPurchase(),
});
MagifyPurchase.subscription({
required String productId,
required String price,
required String currency,
required String transactionId,
required bool isTrial,
MagifyIosPurchase ios = const MagifyIosPurchase(),
MagifyAndroidPurchase android = const MagifyAndroidPurchase(),
});
await MagifyClient.instance.trackExternalPurchase(
MagifyPurchase.subscription(
productId: 'week_premium_subscription',
price: '4.99',
currency: 'USD',
transactionId: '2000000123456789',
isTrial: false,
),
);
MagifyIosPurchase({originalTransactionId, needValidation = true}) and
MagifyAndroidPurchase({purchaseToken, billingCountryCode}) carry the platform-only fields.
The constructors throw ArgumentError for an empty productId, transactionId or
currency, and for a price that does not parse as a decimal number.
Trusted purchases
trackExternalTrustedPurchase(MagifyTrustedPurchase purchase, {bool? isSubscription}) sends
a store-signed purchase record for server-side validation, mirroring the native
TrustedPurchaseRecord field for field:
await MagifyClient.instance.trackExternalTrustedPurchase(
MagifyTrustedPurchase(
productId: 'week_premium_subscription',
transactionId: '2000000123456789',
originalTransactionId: '2000000123456789',
purchasedAt: DateTime.now(),
price: '4.99',
currency: 'USD',
commissionAmount: '1.49',
commissionCurrency: 'USD',
type: MagifyTrustedPurchaseType.initialPurchase,
periodType: MagifyTrustedPeriodType.normal,
productIdType: MagifyTrustedProductIdType.subscription,
),
isSubscription: true,
);
isSubscription picks what happens beyond validation itself, and both native platforms read
it the same way:
Activation always takes the external path, for the same reason as trackExternalPurchase.
MagifyAndroidTrustedPurchase({billingCountryCode}) holds the one field the iOS structure
has no place for; pass it through MagifyTrustedPurchase.android.
Checking purchase state
Future<bool> isPurchaseProcessed(String productId) reports whether the SDK has already
counted a purchase of that product.
final alreadyCounted = await MagifyClient.instance.isPurchaseProcessed('week_premium_subscription');
Server-side validation
trackExternalPurchase only queues a purchase for server-side validation when you pass the
store's own proof of purchase through the platform-specific section of MagifyPurchase:
- Android —
MagifyAndroidPurchase(purchaseToken: ...). Passing the Google Play Billing purchase token (alongsidetransactionId) is what triggers validation against Google Play. - iOS —
MagifyIosPurchase(originalTransactionId: ..., needValidation: true).needValidationdefaults totrue, so validation runs as soon asoriginalTransactionIdis set.
await MagifyClient.instance.trackExternalPurchase(
MagifyPurchase.subscription(
productId: 'week_premium_subscription',
price: '4.99',
currency: 'USD',
transactionId: '2000000123456789',
isTrial: false,
android: MagifyAndroidPurchase(
purchaseToken: purchase.purchaseToken, // Google Play Billing purchase token
),
),
);
Leave ios/android at their defaults (const MagifyIosPurchase() / const MagifyAndroidPurchase())
to track the purchase without server-side validation.
This only applies to trackExternalPurchase — Trusted purchases are
always sent for server-side validation, since that is the entire point of
trackExternalTrustedPurchase.
Server-side verification
Verification verdicts arrive on Stream<MagifyPurchaseVerification> get purchaseVerifications
— each event carries a productId and a MagifyPurchaseVerificationCode. This stream is
observation only.
The retry behavior after a non-terminal verdict is controlled separately with
setPurchaseVerificationPolicy(Map<MagifyPurchaseVerificationCode, MagifyRepeatState> policy)
— set this map up front rather than reacting to each verdict individually.
- A code left out of the map keeps each platform's built-in behavior, and those differ:
invalidCredentialsdrops the record on iOS and retries later on Android. Pass the built-inmagifyAlignedPurchaseVerificationPolicyconstant to even that out. doesntSupportandcancelledonly take effect on iOS; Android handles both itself.- An empty map hands retry behavior back to each platform's built-in default.
- Passing
successorinvalidthrowsArgumentError— both are terminal on both platforms.magifyPurchaseVerificationPolicyCodesis the set of codes the map accepts.
await MagifyClient.instance.setPurchaseVerificationPolicy(magifyAlignedPurchaseVerificationPolicy);
MagifyClient.instance.purchaseVerifications.listen((verification) {
debugPrint('${verification.productId}: ${verification.code}');
});
Purchase and subscription status
Next step
Continue to Analytics for custom events and transactions, or to Advertisement for ad revenue reporting.