DEV Community

Cover image for Stripe Payment with Flutter and Appwrite
Damodar Lohani for Appwrite

Posted on

Stripe Payment with Flutter and Appwrite

Stripe is one of the most popular payment providers to accept payments online. In this tutorial, we will integrate Stripe in our Appwrite Store mobile application built with Flutter and receive payments using Stripe. We will be writing the Appwrite cloud function using Dart to process the payments using Stripe.

If you don't know what Appwrite is, Appwrite is a self-hosted backend-as-a-service platform that provides developers with all the core APIs required to build any application.

Prerequisites

To continue with this tutorial and take full advantage of it, you need the following things:

💰 Setup Stripe

Let's start by properly setting up our Stripe account to ensure we have all secrets we need in the future. We will be using test mode for this example, but the same steps could be followed in production mode.
You start by visiting the Stripe website and signing up. Once in the dashboard, you can switch to the Developers page and enter the API keys tab. In there, copy the publishable key as well as a secret key. Click the Reveal key button to copy the secret key.

Stripe Setup

Please make sure you turn on the test mode, so we use test keys and not the real keys for testing purposes.

🔧 Setup Appwrite

Before we move on, we need to set up an Appwrite project. After following installation instructions and signing up, you can create a project with a custom project ID flutter-stripe.

Create Appwrite Project

Once the project is created, hop into the Settings page and copy the endpoint, we will need this next to set up our Appwrite CLI.

🧑‍💻 CLI Installation

Let's now install the Appwrite CLI, which we will use in the next section to set up the database required for our application.

Install the CLI using one of the methods from our CLI installation guide. Once the CLI is installed, you need to log in to authorize CLI and gain access to your Appwrite instance.

# Initialize the client
appwrite client --endpoint https://<API endpoint>/v1 

# Login, this command is interactive; login with your console email
# and password
appwrite login
Enter fullscreen mode Exit fullscreen mode

The endpoint is the URL you get from the project settings page.

📜 Source Code

The source code required for this tutorial is available in the GitHub repository. Clone the repository using git or download the zip directly from GitHub and extract. The project comes with an appwrite.json file. Next, let‘s set up collections. The collection is already defined in the appwrite.json, and you need to deploy it. Navigate to the folder containing appwrite.json from your terminal and run the following command to deploy the collection.

appwrite deploy collection
Enter fullscreen mode Exit fullscreen mode

This command will create a new collection in your project, and you can check it via the Appwrite console if you’d like. Next, let's add some products to our products collection.
Let's set up some base products using Appwrite CLI. Still, in the same folder from your terminal, use the following command to add new products.

appwrite database createDocument --collectionId 'products' --documentId 'unique()' --data '{"name":"Appwrite Backpack","price":20.0, "imageUrl": "https://cdn.shopify.com/s/files/1/0532/9397/3658/products/all-over-print-backpack-white-front-60abc9d286d19_1100x.jpg"}' --read 'role:all' 
Enter fullscreen mode Exit fullscreen mode

Use the same command to add a few more products with a different name, price, and image of your choice, so that we have something to buy in our shop application. Now we have our Appwrite instance ready and some products in our collection for us to buy. Next, let's set up our mobile application built with Flutter that allows us to log in and buy stuff.

🛠 Flutter Project Set up

In the project you downloaded from GitHub, you can find another folder named flutter_stripe that contains our Flutter shopping application. Let's set it up and run. First, let's set up the configurations. Open the project in your favorite Flutter IDE. Copy flutter_stripe/lib/utils/config.example.dart as flutter_stripe/lib/utils/config.dart and inside it, replace [ENDPOINT] and [PROJECT_ID] with your endpoint and project ID that you got from Appwrite project settings page. Replace [STRIPE_PUBLISHABLE_KEY] with the Stripe's publishable key that you got from the Stripe dashboard starting with sk_test_.

Now run the project in your emulator or your device, and you should see the following screen. We have already implemented registration, login, products display, and shopping cart so we can focus on the stripe integration.

App screenshots

You can create a new account from the registration page and log in to see the products page. There you can add products to your cart and view your cart. Tapping on the Checkout button in the cart will navigate you to an empty page. Let's implement a checkout page to allow payment with Stripe. First, add the [flutter_stripe](https://pub.dev/packages/flutter_stripe) package as a dependency in pubspec.yaml and run flutter pub get to download the dependencies.

Next, let’s initialize the stripe SDK. Open flappwrite_stripe/lib/main.dart and add the following imports.

import 'package:flutter_stripe/flutter_stripe.dart';
Enter fullscreen mode Exit fullscreen mode

Inside the main function, initialize the Stripe SDK

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  Stripe.publishableKey = config.publishableKey;
  Stripe.merchantIdentifier = 'merchent.flappwrite.test';
  Stripe.urlScheme = 'appwrite-callback-${config.projectId}';
  await Stripe.instance.applySettings();
  client.setEndpoint(config.endpoint).setProject(config.projectId);
    runApp(....);
}
Enter fullscreen mode Exit fullscreen mode

Now that Stripe is initialized let's set up the checkout page. Open flappwrite_stripe/lib/screens/checkout.dart and add CardField and an ElevatedButton to confirm the payment as the following.

import 'dart:convert';
import 'package:flappwrite_stripe/providers/cart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_stripe/flutter_stripe.dart';
import 'package:appwrite_auth_kit/appwrite_auth_kit.dart';

class CheckoutScreen extends ConsumerWidget {
  CheckoutScreen({Key? key}) : super(key: key);
  final _cardEditController = CardEditController();

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Checkout'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(16.0),
        children: <Widget>[
          if (context.authNotifier.user?.email != null &&
              context.authNotifier.user?.email != '')
            Text(
              context.authNotifier.user!.email,
              style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                    fontWeight: FontWeight.bold,
                  ),
            ),
          const SizedBox(height: 10.0),
          CardField(
            controller: _cardEditController,
          ),
          ListTile(
            title: const Text("Total Amount"),
            trailing: Text(
              '\$${ref.watch(cartTotalProvider).toStringAsFixed(2)}',
              style: const TextStyle(
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
          ElevatedButton(
            onPressed: () => __confirmPressed(context, ref),
            child: const Text("Confirm"),
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

When the confirm button is pressed, we need to charge the user for the total amount of products in their shopping cart. So let's begin.

Let’s define a new method in the widget _confirmPressed.

_confirmPressed(BuildContext context, WidgetRef ref) async {
    // Woops we can't do this yet 
}
Enter fullscreen mode Exit fullscreen mode

The first step here is to get a payment intent by sending Stripe's amount, currency, and customer details, which can only be done from the server-side using Stripe's secret key. So before we proceed further with payment in App, we will write an Appwrite cloud function that will accept the amount to create a payment intent and return the client secret.

☁ Create Payment Cloud Function

We will be using the Appwrite CLI to initialize our function. from the project folder (the folder that contains appwrite.json), spin up a terminal and run

appwrite init function
Enter fullscreen mode Exit fullscreen mode

It's an interactive command that will ask you for a name and runtime. Give it the name createPaymentIntent and choose Dart 2.16 as the runtime. If Dart 2.16 is not available in the list, it's probably not enabled in your Appwrite server. You can do that by updating the _APP_FUNCTIONS_RUNTIMES in the .env file in your Appwrite installation folder. Look at the environment docs to learn more about Appwrite’s environment variables.

Open pubspec.yaml and add dart_appwrite and stripedart under dependencies. Next, we validate the environment variables required by the function to work properly.

final client = appwrite.Client();
final account = appwrite.Account(client);

Future<void> start(final request, final response) async {
  if (request.env['STRIPE_SECRET_KEY'] == null ||
      request.env['STRIPE_PUBLISHABLE_KEY'] == null) {
    return response.send('Stripe payment keys are missing', 500);
  }

  if (request.env['APPWRITE_ENDPOINT'] == null) {
    return response.send('Appwrite endpoint is missing', 500);
  }
}
Enter fullscreen mode Exit fullscreen mode

Once the environment variables are validated, we can initialize the Appwrite SDK.

  final jwt = request.env['APPWRITE_FUNCTION_JWT'];

  client
      .setEndpoint(request.env['APPWRITE_ENDPOINT'])
      .setProject(request.env['APPWRITE_FUNCTION_PROJECT_ID'])
      .setJWT(jwt);
Enter fullscreen mode Exit fullscreen mode

Next, let's get the details of the user executing the function from Appwrite. Let's write a function that gets the user.

Future<User> getUser() async {
  return await account.get();
}
Enter fullscreen mode Exit fullscreen mode

Let's update our start function to get the user details and check if stripe customer id already exists for the user in their preferences.

Future<void> start(final request, final response) async {
    ...
    client
      .setEndpoint(request.env['APPWRITE_ENDPOINT'])
      .setProject(request.env['APPWRITE_FUNCTION_PROJECT_ID'])
      .setJWT(jwt);

    final user = await getUser();
  final prefs = user.prefs.data;
  String? customerId = prefs['stripeCustomerId'];
}
Enter fullscreen mode Exit fullscreen mode

Next, if the customerId doesn't exist, we will create a new account; we will set up Stripe SDK and create a customer.

    var stripe = Stripe(request.env['STRIPE_SECRET_KEY']);

  // create account for customer if it doesn't already exist
  dynamic customer;
  if (customerId == null) {
    customer =
        await stripe.core.customers.create(params: {"email": user.email});
    customerId = customer!['id'];
    if (customerId == null) {
      throw (customer.toString());
    } else {
      prefs['stripeCustomerId'] = customerId;
      await account.updatePrefs(prefs: prefs);
    }
  }
Enter fullscreen mode Exit fullscreen mode

Notice we also save customer ID in the user's preference for next-time access.

Now that we have the customer ID, we can create a payment intent from the amount and the currency provided during function execution.

     // data provided to the function during execution
    final data = jsonDecode(request.env['APPWRITE_FUNCTION_DATA']);
  final paymentIntent = await stripe.core.paymentIntents!.create(params: {
    "amount": data['amount'],
    "currency": data['currency'],
    "customer": customerId,
  });
  response.json({
    "paymentIntent": paymentIntent,
    "client_secret": paymentIntent!['client_secret'],
  });
Enter fullscreen mode Exit fullscreen mode

Our function is now ready, and the complete function looks like this.

import 'dart:convert';

import 'package:dart_appwrite/dart_appwrite.dart' as appwrite;
import 'package:dart_appwrite/models.dart';
import 'package:stripedart/stripedart.dart';

final client = appwrite.Client();
final account = appwrite.Account(client);

Future<User> getUser() async {
  return await account.get();
}

Future<void> start(final request, final response) async {
  if (request.env['STRIPE_SECRET_KEY'] == null ||
      request.env['STRIPE_PUBLISHABLE_KEY'] == null) {
    return response.send('Stripe payment keys are missing', 500);
  }

  if (request.env['APPWRITE_ENDPOINT'] == null) {
    return response.send('Appwrite endpoint is missing', 500);
  }

  // final userId = request.env['APPWRITE_FUNCTION_USER_ID'];
  final jwt = request.env['APPWRITE_FUNCTION_JWT'];

  client
      .setEndpoint(request.env['APPWRITE_ENDPOINT'])
      .setProject(request.env['APPWRITE_FUNCTION_PROJECT_ID'])
      .setJWT(jwt);

  final user = await getUser();
  final prefs = user.prefs.data;
  String? customerId = prefs['stripeCustomerId'];

  var stripe = Stripe(request.env['STRIPE_SECRET_KEY']);

  // create account for customer if it doesn't already exist
  dynamic customer;
  if (customerId == null) {
    customer =
        await stripe.core.customers.create(params: {"email": user.email});
    customerId = customer!['id'];
    if (customerId == null) {
      throw (customer.toString());
    } else {
      prefs['stripeCustomerId'] = customerId;
      await account.updatePrefs(prefs: prefs);
    }
  }
  final data = jsonDecode(request.env['APPWRITE_FUNCTION_DATA']);
  final paymentIntent = await stripe.core.paymentIntents!.create(params: {
    "amount": data['amount'],
    "currency": data['currency'],
    "customer": customerId,
  });
  response.json({
    "paymentIntent": paymentIntent,
    "client_secret": paymentIntent!['client_secret'],
  });
}
Enter fullscreen mode Exit fullscreen mode

Let's deploy the function using the Appwrite CLI. Still, in the project folder containing appwrite.json, use the following command to deploy the function.

appwrite deploy function
Enter fullscreen mode Exit fullscreen mode

This interactive command allows you to choose the functions to deploy. We can use space to select that function and press return to deploy as we have only one. If you now go to your Appwrite console and tap on the Functions from the sidebar, you should see your function in the list. Tap on the Settings button for the function and in the overview, make sure your function is built and ready for execution. If the build has failed, check the logs for the build to figure out what happened.

Finally, we need to set up the function's environment variable. Open your function's settings page and scroll below to find the Variables section. Please tap on the add variable button, add three variables as STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, and the APPWRITE_ENDPOINT and give them proper values obtained from Stripe and your Appwrite's endpoint, respectively.

Now let's get back to our Flutter app to set up the rest of the payment flow. Open flappwrite_stripe/lib/screens/checkout.dart and add new method in the widget _fetchClientSecret with the following details.

    Future<String> _fetchClientSecret(BuildContext context, double total) async {
    final functions = Functions(context.authNotifier.client);
    final execution = await functions.createExecution(
        functionId: '[FUNCTION_ID]',
        data: jsonEncode({
          'currency': 'usd',
          'amount': (total * 100).toInt(),
        }),
        xasync: false);
    if (execution.stdout.isNotEmpty) {
      final data = jsonDecode(execution.stdout);
      return data['client_secret'];
    }
    return '';
  }
Enter fullscreen mode Exit fullscreen mode

You can get the FUNCTION_ID from your appwrite.json or the function's settings page in the Appwrite console. Finally, we will complete the _confirmPressed function like the following.

if (!_cardEditController.complete) {
  ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
      content: Text("Card details not entered completely")));
  return;
}

final clientSecret = await fetchClientSecret(
    context, ref.watch(cartTotalProvider));
if(clientSecret.isEmpty) {
    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
      content: Text("Unable to fetch client secret")));
    return;
}

// you should get proper billing details from the user
final billingDetails = BillingDetails(
  email: context.authNotifier.user!.email,
  phone: '+48888000888',
  address: const Address(
    city: 'Kathmandu',
    country: 'NP',
    line1: 'Chabahil, Kathmandu',
    line2: '',
    state: 'Bagmati',
    postalCode: '55890',
  ),
);

try {
  final paymentIntent = await Stripe.instance.confirmPayment(
    clientSecret,
    PaymentMethodParams.card(
      billingDetails: billingDetails,
      setupFutureUsage: PaymentIntentsFutureUsage.OffSession,
    ),
  );
  if (paymentIntent.status == PaymentIntentsStatus.Succeeded) {
    ref.read(cartProvider.notifier).emptyCart();
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text(
            "Success!: The payment was confirmed successfully!"),
      ),
    );
    Navigator.pop(context);
  }
} on StripeException catch (e) {
  ScaffoldMessenger.of(context).showSnackBar(SnackBar(
      content: Text(e.error.localizedMessage ?? e.toString())));
}
Enter fullscreen mode Exit fullscreen mode

Our Application is now ready. Run the application, add some products to the shopping cart, and tap checkout. On the checkout page, add a test card details 4242 4242 4242 4242, and any valid validity date from the future, and any three-digit number is CCV. Finally, when you tap submit, you should be able to confirm the payment and see that you have received payment in the Stripe dashboard. That’s it; you can now build an application that can accept payment from your users.

🔗 Conclusion

I hope you enjoyed this tutorial. You can find the complete code of this project in the GitHub repository. To learn more about Appwrite, you can visit our documentation or join us on our discord.

Discussion (1)

Collapse
joeoheron profile image
Joe

Hey Damodar, love the post and all the work you do for the Appwrite and Flutter communities. One quick question:

How would you go about implementing infinite scrolling pagination for the 'products' collection as defined within this tutorial?

I've been spending a fair bit of time on it, and I fear I'm not making much progress as of yet.

Thanks!