masamune_purchase_mobile 3.1.6 copy "masamune_purchase_mobile: ^3.1.6" to clipboard
masamune_purchase_mobile: ^3.1.6 copied to clipboard

unlisted

Masamune plugin library to implement in-app purchases for GooglePlay and AppStore.

Masamune logo

Masamune Purchase Mobile

Follow on GitHub Follow on X Follow on YouTube Maintained with Melos

GitHub Sponsor


[GitHub] | [YouTube] | [Packages] | [X] | [LinkedIn] | [mathru.net]


Masamune Purchase #

Usage #

Installation #

Add the packages required for your target platforms.

# Core purchase models, controllers, and functions actions
flutter pub add masamune_purchase

# Mobile IAP adapter (Google Play / App Store)
flutter pub add masamune_purchase_mobile

Run flutter pub get after editing pubspec.yaml manually.

Register the Adapter #

Configure MobilePurchaseMasamuneAdapter with your products and backend integration.

// lib/adapter.dart

import 'package:masamune_purchase_mobile/masamune_purchase_mobile.dart';
import 'package:katana_functions_firebase/katana_functions_firebase.dart';
import 'package:katana_model_firestore/katana_model_firestore.dart';

final functionsAdapter = FirebaseFunctionsAdapter(
  options: DefaultFirebaseOptions.currentPlatform,
  region: FirebaseRegion.asiaNortheast1,
);

final modelAdapter = FirestoreModelAdapter(
  options: DefaultFirebaseOptions.currentPlatform,
);

/// Masamune adapters used in the application.
final masamuneAdapters = <MasamuneAdapter>[
  const UniversalMasamuneAdapter(),

  MobilePurchaseMasamuneAdapter(
    products: const [
      // Subscription product
      PurchaseProduct.subscription(
        productId: "premium_monthly",          // Must match App Store/Play Console
        title: LocalizedValue("Premium Monthly"),
        description: LocalizedValue("Unlock all features"),
        period: PurchaseSubscriptionPeriod.month,
      ),
      
      // Non-consumable (one-time purchase)
      PurchaseProduct.nonConsumable(
        productId: "lifetime_unlock",
        title: LocalizedValue("Lifetime Access"),
      ),
      
      // Consumable (coins, credits)
      PurchaseProduct.consumable(
        productId: "coin_pack_100",
        title: LocalizedValue("100 Coins"),
        amount: 100,
      ),
    ],
    onRetrieveUserId: () => currentUserId,    // Current authenticated user ID
    functionsAdapter: functionsAdapter,       // For backend validation
    modelAdapter: modelAdapter,               // For storing purchases
    initializeOnBoot: true,                   // Auto-initialize on app start
  ),
];

Important: Product IDs must match exactly with those configured in Google Play Console and App Store Connect.

Display Products and Purchase #

Use the Purchase controller to load and display available products:

class StorePage extends PageScopedWidget {
  @override
  Widget build(BuildContext context, PageRef ref) {
    final purchase = ref.app.controller(Purchase.query());

    // Initialize on page load
    ref.page.on(
      initOrUpdate: () {
        purchase.initialize();
      },
    );

    return Scaffold(
      appBar: AppBar(title: const Text("Store")),
      body: purchase.initialized
          ? ListView.builder(
              itemCount: purchase.products.length,
              itemBuilder: (context, index) {
                final product = purchase.products[index];
                
                return ListTile(
                  title: Text(product.title.value),
                  subtitle: Text(product.description.value),
                  trailing: ElevatedButton(
                    onPressed: () async {
                      try {
                        await purchase.purchase(
                          product: product,
                          onDone: () {
                            print("Purchase completed!");
                            // Show success dialog
                          },
                        );
                      } catch (e) {
                        print("Purchase failed: $e");
                        // Show error dialog
                      }
                    },
                    child: Text(product.price ?? "Buy"),
                  ),
                );
              },
            )
          : Center(child: CircularProgressIndicator()),
    );
  }
}

Restore Purchases #

Allow users to restore previous purchases:

ElevatedButton(
  onPressed: () async {
    await purchase.restore();
    print("Purchases restored");
  },
  child: const Text("Restore Purchases"),
)

Backend Validation #

Your Cloud Functions must validate purchase receipts with Google Play or App Store:

Android Validation (TypeScript):

// Cloud Functions
import { google } from 'googleapis';

export const validateAndroidPurchase = functions.https.onCall(async (data, context) => {
  const { packageName, productId, purchaseToken } = data;
  
  // Verify with Google Play Billing API
  const androidPublisher = google.androidpublisher('v3');
  const response = await androidPublisher.purchases.products.get({
    packageName,
    productId,
    token: purchaseToken,
    auth: googleAuth,
  });
  
  const isValid = response.data.purchaseState === 0;  // 0 = purchased
  
  if (isValid) {
    // Grant entitlement to user
    await grantPurchase(data.userId, productId);
  }
  
  return { result: isValid };
});

iOS Validation (TypeScript):

// Cloud Functions
export const validateIosPurchase = functions.https.onCall(async (data, context) => {
  const { receiptData, productId } = data;
  
  // Verify with App Store Server API
  const response = await axios.post(
    'https://buy.itunes.apple.com/verifyReceipt',  // or sandbox URL for testing
    {
      'receipt-data': receiptData,
      'password': process.env.APP_STORE_SHARED_SECRET,
    }
  );
  
  const isValid = response.data.status === 0;
  
  if (isValid) {
    await grantPurchase(data.userId, productId);
  }
  
  return { result: isValid };
});

StoreKit 2 Support: The adapter automatically detects StoreKit v1/v2 and sends appropriate payloads (transactionId, receiptData, storeKitVersion).

Check User Entitlements #

Query purchase models to check user access:

class PremiumFeaturePage extends PageScopedWidget {
  @override
  Widget build(BuildContext context, PageRef ref) {
    final purchaseUser = ref.app.model(
      PurchaseUserModel.document(userId: currentUserId),
    )..load();

    final hasPremium = purchaseUser.value?.hasPremiumAccess ?? false;

    return hasPremium
        ? PremiumContent()
        : UpgradePrompt();
  }
}

Custom Delegates #

Provide delegates for purchase lifecycle events:

// Consumable purchases (coins, credits)
class CoinsDelegate extends ConsumablePurchaseDelegate {
  @override
  Future<void> onDelivered({
    required PurchaseProduct product,
    required double amount,
  }) async {
    await userRepository.addCoins(currentUserId, amount.toInt());
  }
}

// Subscription purchases
class PremiumDelegate extends SubscriptionPurchaseDelegate {
  @override
  Future<void> onActivated({required PurchaseProduct product}) async {
    await userRepository.setPremiumStatus(currentUserId, true);
  }
  
  @override
  Future<void> onExpired({required PurchaseProduct product}) async {
    await userRepository.setPremiumStatus(currentUserId, false);
  }
}

Pass delegates to the adapter:

MobilePurchaseMasamuneAdapter(
  products: [...],
  consumableDelegate: CoinsDelegate(),
  subscriptionDelegate: PremiumDelegate(),
  ...
)

Platform Setup #

Google Play Console:

  1. Create in-app products in Play Console
  2. Note the product IDs
  3. Set up a service account for API access
  4. Upload a signed build to internal testing track

App Store Connect:

  1. Create in-app purchases in App Store Connect
  2. Note the product IDs
  3. Set up banking and tax information
  4. Create sandbox tester accounts for testing

Testing #

Android: Use internal testing track in Play Console

MobilePurchaseMasamuneAdapter(
  products: [...],
  automaticallyConsumeOnAndroid: true,  // Auto-consume for testing
)

iOS: Use sandbox testers or StoreKit Configuration files

MobilePurchaseMasamuneAdapter(
  products: [...],
  iosSandboxTesting: true,  // Enable sandbox mode
)

Troubleshooting #

  • Ensure product IDs match exactly between code and store configurations
  • Check that products are in "Active" state in store consoles
  • Verify backend Functions are deployed and accessible
  • Test restore functionality for subscription recovery
  • Handle purchase exceptions gracefully with user-friendly error messages

GitHub Sponsors #

Sponsors are always welcome. Thank you for your support!

https://github.com/sponsors/mathrunet