Using Packages Like flutter_stripe and in_app_purchase to Handle Payment Transactions in Flutter

Handling payment transactions in Flutter requires secure and reliable methods to process payments from users. Two popular packages, flutter_stripe and in_app_purchase, offer robust solutions for different types of payment scenarios. The flutter_stripe package is ideal for processing payments via Stripe, while the in_app_purchase package is tailored for handling in-app purchases through app stores like Google Play and the App Store.

Understanding Payment Options in Flutter

Before diving into the implementation details, it’s essential to understand the types of payment options available in Flutter:

  • Stripe Payments: Direct card payments processed through Stripe’s secure API.
  • In-App Purchases: Payments handled through the app stores (Google Play Store or Apple App Store).

Using the flutter_stripe Package for Stripe Payments

The flutter_stripe package simplifies integrating Stripe payment gateway into your Flutter application. Here’s how to implement it:

Step 1: Add Dependency

Add the flutter_stripe package to your pubspec.yaml file:

dependencies:
  flutter_stripe: ^9.1.0  # Use the latest version

Run flutter pub get to install the dependency.

Step 2: Configure Stripe in Your Project

You need to configure your Stripe account and obtain the necessary API keys. Follow the Stripe documentation to create an account and get your publishable key and secret key.

Step 3: Initialize Stripe in Your Flutter App

In your main.dart file, initialize the flutter_stripe package with your publishable key:

import 'package:flutter_stripe/flutter_stripe.dart';
import 'package:flutter/material.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  Stripe.publishableKey = 'YOUR_STRIPE_PUBLISHABLE_KEY';
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Stripe Example',
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Stripe Payment Example')),
      body: Center(
        child: ElevatedButton(
          onPressed: () => handlePayment(context),
          child: Text('Make Payment'),
        ),
      ),
    );
  }

  Future handlePayment(BuildContext context) async {
    try {
      // 1. Create a payment intent on the server
      final paymentIntentData = await createPaymentIntent('10', 'USD'); // Amount and currency

      // 2. Initialize Payment Sheet
      await Stripe.instance.initPaymentSheet(
        paymentSheetParameters: SetupPaymentSheetParameters(
          paymentIntentClientSecret: paymentIntentData['clientSecret'],
          style: ThemeMode.light,
          merchantDisplayName: 'Flutter Stripe Store',
        ),
      );

      // 3. Display Payment Sheet
      await Stripe.instance.presentPaymentSheet();

      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Payment successful!')),
      );
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Payment failed: ${e.toString()}')),
      );
    }
  }

  Future> createPaymentIntent(String amount, String currency) async {
    // Replace with your server endpoint URL
    final url = Uri.parse('https://your-server.com/create-payment-intent'); 
    final response = await http.post(
      url,
      headers: {
        'Content-Type': 'application/json',
      },
      body: jsonEncode({
        'amount': calculateAmount(amount),
        'currency': currency,
        'automatic_payment_methods[enabled]': true, // Enable automatic payment methods
      }),
    );
    
    return jsonDecode(response.body);
  }

  int calculateAmount(String amount) {
    final a = (int.parse(amount)) * 100;
    return a;
  }
}

The createPaymentIntent function should call your server-side endpoint to create a Stripe Payment Intent.

Step 4: Server-Side Implementation

On your server (Node.js, Python, etc.), you need to create a route that creates a Stripe Payment Intent. Here’s a basic example using Node.js:

const stripe = require('stripe')('YOUR_STRIPE_SECRET_KEY');
const express = require('express');
const app = express();
app.use(express.json());

app.post('/create-payment-intent', async (req, res) => {
  const { amount, currency } = req.body;

  try {
    const paymentIntent = await stripe.paymentIntents.create({
      amount: amount,
      currency: currency,
      automatic_payment_methods: {
        enabled: true,
      },
    });

    res.json({ clientSecret: paymentIntent.client_secret });
  } catch (e) {
    console.error('Error creating payment intent:', e);
    res.status(500).json({ error: e.message });
  }
});

app.listen(4242, () => console.log('Running on port 4242'));

Ensure you replace 'YOUR_STRIPE_SECRET_KEY' with your actual Stripe secret key.

Using the in_app_purchase Package for In-App Purchases

For handling in-app purchases (IAP) through the Google Play Store or Apple App Store, use the in_app_purchase package.

Step 1: Add Dependency

Add the in_app_purchase package to your pubspec.yaml file:

dependencies:
  in_app_purchase: ^3.1.6 # Use the latest version

Run flutter pub get to install the dependency.

Step 2: Configure Your App Store Accounts

Set up your products (subscriptions, consumables, or non-consumables) in the Google Play Console and/or App Store Connect.

Step 3: Initialize and Query Products

In your Flutter app, initialize the InAppPurchase instance and query for available products:

import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('In-App Purchase Example')),
        body: InAppPurchasePage(),
      ),
    );
  }
}

class InAppPurchasePage extends StatefulWidget {
  @override
  _InAppPurchasePageState createState() => _InAppPurchasePageState();
}

class _InAppPurchasePageState extends State {
  final InAppPurchase _inAppPurchase = InAppPurchase.instance;
  List _products = [];
  List _purchases = [];
  bool _isAvailable = false;
  bool _isLoading = true;

  final List _productIds = ['your_product_id']; // Replace with your product IDs

  @override
  void initState() {
    super.initState();
    _initialize();
  }

  Future _initialize() async {
    _isAvailable = await _inAppPurchase.isAvailable();
    if (_isAvailable) {
      await _getProducts();
      await _getPastPurchases();
      _listenToPurchaseUpdates();
    } else {
      setState(() => _isLoading = false);
    }
  }

  Future _getProducts() async {
    final ProductDetailsResponse response = await _inAppPurchase.queryProductDetails(
      _productIds.toSet(),
    );
    setState(() {
      _products = response.productDetails;
      _isLoading = false;
    });
  }

  Future _getPastPurchases() async {
    final QueryPastPurchasesResponse response = await _inAppPurchase.queryPastPurchases();
    setState(() {
      _purchases = response.pastPurchases;
    });
  }

  void _listenToPurchaseUpdates() {
    _inAppPurchase.purchaseStream.listen((List purchaseDetailsList) {
      purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
        if (purchaseDetails.status == PurchaseStatus.pending) {
          _showPendingUI();
        } else {
          if (purchaseDetails.status == PurchaseStatus.granted) {
            _verifyAndDeliverProduct(purchaseDetails);
          } else if (purchaseDetails.status == PurchaseStatus.error) {
            _handleError(purchaseDetails.error!);
          }

          if (purchaseDetails.pendingCompletePurchase) {
            await _inAppPurchase.completePurchase(purchaseDetails);
          }
        }
      });
    });
  }

  Future _verifyAndDeliverProduct(PurchaseDetails purchaseDetails) async {
    // Verify the purchase using your server (important for security)
    bool validPurchase = await _verifyPurchase(purchaseDetails);
    if (validPurchase) {
      // Deliver the product to the user
      _deliverProduct(purchaseDetails.productID);
    } else {
      // Handle invalid purchase
      _handleInvalidPurchase(purchaseDetails);
    }
  }

  Future _verifyPurchase(PurchaseDetails purchaseDetails) async {
    // Implement server-side verification logic here
    // This should communicate with Google Play Developer API or App Store Server API
    // to validate the purchase
    return true; // Replace with actual verification result
  }

  void _deliverProduct(String productId) {
    // Give the user access to the purchased product
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Product delivered: $productId')),
    );
  }

  void _handleInvalidPurchase(PurchaseDetails purchaseDetails) {
    // Handle scenarios where the purchase is invalid or fraudulent
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Invalid purchase: ${purchaseDetails.productID}')),
    );
  }

  void _showPendingUI() {
    // Show a loading indicator or message to the user
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Processing payment...')),
    );
  }

  void _handleError(IAPError error) {
    // Handle purchase errors
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Purchase error: ${error.message}')),
    );
  }

  void _buyProduct(ProductDetails product) {
    final PurchaseParam purchaseParam = PurchaseParam(productDetails: product);
    _inAppPurchase.buyNonConsumable(purchaseParam: purchaseParam); // Example for non-consumable
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoading) {
      return Center(child: CircularProgressIndicator());
    }

    if (!_isAvailable) {
      return Center(child: Text('In-app purchases are not available on this device.'));
    }

    return ListView.builder(
      itemCount: _products.length,
      itemBuilder: (context, index) {
        final product = _products[index];
        return ListTile(
          title: Text(product.title),
          subtitle: Text(product.description),
          trailing: ElevatedButton(
            onPressed: () => _buyProduct(product),
            child: Text('Buy for ${product.price}'),
          ),
        );
      },
    );
  }
}

Step 4: Handling Purchases

Listen for purchase updates using _inAppPurchase.purchaseStream and handle the different purchase states (pending, granted, error) accordingly.

Step 5: Verify Purchases

Implement server-side verification of purchases using the Google Play Developer API or App Store Server API. This is essential to prevent fraud and ensure that users receive the products they have paid for.

Conclusion

Handling payment transactions in Flutter can be effectively managed using the flutter_stripe and in_app_purchase packages. While flutter_stripe offers a seamless way to integrate direct card payments via Stripe, in_app_purchase simplifies the process of handling in-app purchases through the app stores. By following the outlined steps and best practices, you can build secure and reliable payment systems for your Flutter applications.