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 (alongside transactionId) is what triggers validation against Google Play.
  • iOS — MagifyIosPurchase(originalTransactionId: ..., needValidation: true). needValidation defaults to true, so validation runs as soon as originalTransactionId is 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: invalidCredentials drops the record on iOS and retries later on Android. Pass the built-in magifyAlignedPurchaseVerificationPolicy constant to even that out.
  • doesntSupport and cancelled only take effect on iOS; Android handles both itself.
  • An empty map hands retry behavior back to each platform's built-in default.
  • Passing success or invalid throws ArgumentError — both are terminal on both platforms. magifyPurchaseVerificationPolicyCodes is 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.

Related articles

LevelPlay / IronSource

Android SDK Integration with other SDK

PurchaseInfo

Unity SDK Campaigns

Android SDK Analytics

Android SDK Privacy & Consent