Build a Subscription-Based Service with Stripe and Appwrite Functions in Flutter

Demola Malomo - Jan 20 '23 - - Dev Community

A subscription is an agreement where a customer agrees to pay a recurring fee to a vendor for delivered goods or rendered services periodically. It is a business model adopted by many businesses and websites to better serve their customers.

In this post, we will learn how to create a newspaper subscription service in Flutter using Stripe and Appwrite Functions.

Technology Overview

Appwrite Function is a service that lets us use the Appwrite server functionality by executing a custom code using a language of choice like Node.js, Python, PHP, Dart, or Java.

Stripe is a software-as-a-service platform that offers sets of APIs for processing payments globally.

GitHub Links

The project source codes are below:

Prerequisites

The following requirements apply to follow along:

Set up a Stripe Account to Process Payment

To get started, we need to log into our Stripe account to get our Secret Key. The Secret Key will come in handy for processing payments in our application. To do this, navigate to the Developers tab and copy the Secret Key.

Stripe overview

Integrate Appwrite Function with Stripe

Enabling Dart runtime in Appwrite
By default, Dart is not included as part of the Appwrite runtime. To enable Dart as a supported runtime, we first need to navigate to the directory that was created when we installed Appwrite and edit the .env file by adding dart-2.17 under the appwrite service as shown below:

Appwrite folder with .env file

//remaining env variable goes here

_APP_FUNCTIONS_RUNTIMES=dart-2.17
Enter fullscreen mode Exit fullscreen mode

Updated runtime with Dart support

Lastly, we need to sync the changes we made on the .env file with our Appwrite server. To do this, we must run the command below inside the appwrite directory.

docker compose up -d --force-recreate
Enter fullscreen mode Exit fullscreen mode

Create Appwrite project
With our Appwrite instance up and running, we can now create a project that our function will use to process payments using Stripe. To do this, we first need to navigate to the desired directory and run the command below:

mkdir function_stripe && cd function_stripe
Enter fullscreen mode Exit fullscreen mode

The command creates a project folder called function_stripe and navigates into the folder.

Secondly, we need to log into the Appwrite server using the CLI.

appwrite login
Enter fullscreen mode Exit fullscreen mode

We will be prompted to input our email and password which need to be the credentials we used to sign up for the Appwrite console.

Lastly, we need to create a new Appwrite project using the command below:

appwrite init project
Enter fullscreen mode Exit fullscreen mode

We will be prompted with some questions on how to set up our project, and we can answer as shown below:

How would you like to start? <select "Create a new project">
What would you like to name your project? <input "appwrite_stripe">
What ID would you like to have for your project? (unique()) <press enter>
Enter fullscreen mode Exit fullscreen mode

Create Appwrite Function inside the project
With our project setup, we can proceed to create a function by running the command below:

appwrite init function
Enter fullscreen mode Exit fullscreen mode

We will also be prompted with some questions about how to set up our function; we can answer as shown below:

What would you like to name your function? <input "function_stripe">
What ID would you like to have for your function? (unique()) <press enter>
What runtime would you like to use? <scroll to dart-2.17 and press enter>
Enter fullscreen mode Exit fullscreen mode

The command will create a starter Dart project.

Starter project

Secondly, we need to install the required dependency by navigating to pubspec.yaml file and add the http package to the dependencies section.

http: ^0.13.5
Enter fullscreen mode Exit fullscreen mode

Install dependency

We also need to install the specified dependency by running the command below:

cd functions/function_stripe/
dart pub get
Enter fullscreen mode Exit fullscreen mode

Thirdly, we need to modify the main.dart file inside the lib folder as shown below:

import 'package:dart_appwrite/dart_appwrite.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;

Future<void> start(final req, final res) async {
  final client = Client();
  var uname = 'STRIPE SECRET KEY GOES HERE';
  var pword = '';
  var authn = 'Basic ' + base64Encode(utf8.encode('$uname:$pword'));

  var headers = {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Authorization': authn,
  };

  var data = {
    'amount': '100',
    'currency': 'usd',
    'payment_method': 'pm_card_visa',
  };

  var url = Uri.parse('https://api.stripe.com/v1/payment_intents');
  var response = await http.post(url, headers: headers, body: data);
  if (response.statusCode != 200) {
    res.json({
      "status": response.statusCode,
      "message": "Error processing subscription"
    });
  } else {
    res.json({
      "status": response.statusCode,
      "message": "Subscription processed successfully!"
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependencies
  • Line 7-14: Creates an API headers object using the Secret Key to setup Authorization
  • Line 16-20: Creates an API body that consists of amount, currency, and payment_method of type card and using a test card (pm_card_visa)
  • Line 22-34: Makes an API call to Stripe by passing the headers, body, and returning the appropriate response

Note: We hardcoded the amount and the payment method in this post. However, Stripe supports multiple payment methods and options we can adopt in a production environment.

Lastly, we must navigate to the project terminal and deploy our function.

cd ../..
appwrite deploy function
Enter fullscreen mode Exit fullscreen mode

We will also be prompted about the function we would like to deploy. We must select the appwrite_sendgrid function by pressing the spacebar key to mark and the enter key to confirm the selection.

Select function to deploy

Sample of a deployed function

We can also confirm the deployment by navigating to the Function tab on the Appwrite console.

Deployed function

Lastly, we must update the deployed function permission as we need to call it from our Flutter application. To do this, we need to navigate to the Settings tab, scroll to the Execute Access section, select Any, and click Update.

Update execute access

Create a Flutter App

With our function deployed and ready to accept payment via Stripe, we can set up a Flutter project.

To get started, we need to clone the project by navigating to the desired directory and running the command below:

git clone https://github.com/Mr-Malomz/appwrite_stripe.git && cd appwrite_stripe
Enter fullscreen mode Exit fullscreen mode

Running the Project
First, we need to install the project dependencies by running the command below:

flutter pub get
Enter fullscreen mode Exit fullscreen mode

Then, run the project using the command below:

flutter run
Enter fullscreen mode Exit fullscreen mode

The command above will run the application on the selected device.

Running app

Create a Database and Add Sample Data
First, we need to create a database with the corresponding collection, document, and add sample data as shown below:

name is_subscribed
John Travolta false

Collection

Lastly, we need to update our database permission to manage them accordingly. To do this, we need to navigate to the Settings tab, scroll to the Update Permissions section, select Any, and mark accordingly.

Select Any
Mark permission

Add Platform Support
To add support for our Flutter app, navigate to the Home menu and click the Flutter App button.

Add platform

Depending on the device we are using to run our Flutter application, we can modify it as shown below.

iOS
To obtain our Bundle ID, we can navigate using the path below:
ios > Runner.xcodeproj > project.pbxproj

Open the project.pbxproj file and search for PRODUCT_BUNDLE_IDENTIFIER.

Select new Flutter app

Next, open the project directory on Xcode, open the Runner.xcworkspace folder in the app's iOS folder, select the Runner project in the Xcode project navigator, select the Runner target in the primary menu sidebar, and then select iOS 11 in the deployment info’s target.

Change deployment target

Android
To get our package name, we can navigate to an XML file using the path below:
android > app > src > debug > AndroidManifest.xml

Open the AndroidManifest.xml file and copy the package value.

Android

Next, we need to modify the AndroidManifext.xml as shown below:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.real_time_comm">
    <uses-permission android:name="android.permission.INTERNET"/>
    <application ...>
    <activity android:name="com.linusu.apppwrite_stripe.CallbackActivity" android:exported="true">
      <intent-filter android:label="apppwrite_stripe">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="appwrite-callback-[PROJECT_ID]" />
      </intent-filter>
    </activity>
  </application>
</manifest>
Enter fullscreen mode Exit fullscreen mode

We must also replace the highlighted [PROJECT_ID] with our actual Appwrite project ID.

Building the Subscription-Based Service

To get started, we need to create a model to convert the response sent from Appwrite to a Dart object. The model will also cater to JSON serialization. To do this, we need to create a utils.dart file in the lib folder and add the snippet below:

class AppConstant {
  final String databaseId = "REPLACE WITH DATABASE ID";
  final String projectId = "REPLACE WITH PROJECT ID";
  final String collectionId = "REPLACE WITH COLLECTION ID";
  final String functionId = "REPLACE WITH FUNCTION ID";
  final String userId = "REPLACE WITH SAMPLE ID";
  final String endpoint = "ENDPOINT";
}

class User {
  String name;
  bool is_subscribed;

  User({required this.name, required this.is_subscribed});

  Map<dynamic, dynamic> toJson() {
    return {"name": name, "is_subscribed": is_subscribed};
  }

  factory User.fromJson(Map<dynamic, dynamic> json) {
    return User(name: json['name'], is_subscribed: json['is_subscribed']);
  }
}
Enter fullscreen mode Exit fullscreen mode

For the endpoint property, we need to modify it to work with our system's local network address. We can adjust accordingly:

iOS
Navigate to the Network section and copy the IP address:

IP address

Android
We can connect our Android emulator to the system’s IP using the 10.0.2.2 IP address.

  final String endpoint = "http://10.0.2.2/v1";
Enter fullscreen mode Exit fullscreen mode

Note: We can get the databaseId, projectId, collectionId, functionId, and userId by navigating through the Appwrite console.

Next, we need to create a service file to separate the application core logic from the UI. To do this, create a user_service.dart file inside the lib directory, and first, add the snippet below:

import 'package:appwrite/appwrite.dart';
import 'package:appwrite_stripe/utils.dart';

class UserService {
  Client client = Client();
  Databases? db;

  UserService() {
    _init();
  }

  //initialize the application
  _init() async {
    client
        .setEndpoint(AppConstant().endpoint)
        .setProject(AppConstant().projectId);

    db = Databases(client);

    //get current session
    Account account = Account(client);

    try {
      await account.get();
    } on AppwriteException catch (e) {
      if (e.code == 401) {
        account
            .createAnonymousSession()
            .then((value) => value)
            .catchError((e) => e);
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependencies
  • Creates a UserService class with client, db properties to connect to the Appwrite instance, and the database
  • Creates an _init method that configures the Appwrite using the properties and also conditionally creates an anonymous user to access the Appwrite database

Lastly, we need to modify Userservice class by adding createSubscription, getUserDetails, subscribeUser, and unSubscribeUser methods to manage subscriptions accordingly.

//imports go here

class UserService {
  Client client = Client();
  Databases? db;

  UserService() {
    _init();
  }

  //initialize the application
  _init() async {
    //init code goes here
  }

  Future createSubscription() async {
    Functions functions = Functions(client);
    try {
      var result =
          await functions.createExecution(functionId: AppConstant().functionId);
      return result;
    } catch (e) {
      throw Exception('Error creating subscription');
    }
  }

  Future<User> getUserDetails() async {
    try {
      var data = await db?.getDocument(
          databaseId: AppConstant().databaseId,
          collectionId: AppConstant().collectionId,
          documentId: AppConstant().userId);
      var user = data?.convertTo((doc) => User.fromJson(doc));
      return user!;
    } catch (e) {
      throw Exception('Error getting user details');
    }
  }

  Future subscribeUser(String name) async {
    try {
      User updateUser = User(name: name, is_subscribed: true);
      var data = await db?.updateDocument(
        databaseId: AppConstant().databaseId,
        collectionId: AppConstant().collectionId,
        documentId: AppConstant().userId,
        data: updateUser.toJson(),
      );
      return data;
    } catch (e) {
      throw Exception('Error subscribing user');
    }
  }

  Future unSubscribeUser(String name) async {
    try {
      User updateUser = User(name: name, is_subscribed: false);
      var data = await db?.updateDocument(
        databaseId: AppConstant().databaseId,
        collectionId: AppConstant().collectionId,
        documentId: AppConstant().userId,
        data: updateUser.toJson(),
      );
      return data;
    } catch (e) {
      throw Exception('Error unsubscribing user');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Consuming the Service
With that done, we can start using the service to perform the required operation. To get started, we need to open the main.dart file in the same lib directory and update it by doing the following:

First, we need to import the required dependencies and create a method to get users' details

import 'package:appwrite_stripe/user_service.dart';
import 'package:appwrite_stripe/utils.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  //app code goes here
  }
}

class MyHomePage extends StatefulWidget {
  //homepage code goes here
}

class _MyHomePageState extends State<MyHomePage> {
  late User user;
  bool _isLoading = false;
  bool _isFetching = false;
  bool _isError = false;

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

  _getUserDetails() {
    setState(() {
      _isLoading = true;
    });
    UserService().getUserDetails().then((value) {
      setState(() {
        user = value;
        _isLoading = false;
      });
    }).catchError((e) {
      setState(() {
        _isLoading = false;
        _isError = true;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    //UI code goes here
  }
}
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Line 1-3: Imports the required dependencies
  • Line 18-21: Creates the user, _isLoading, _isFetching, and _isError properties to manage application state
  • Lines 23-44: Creates a _getUserDetails method to get the user details using the UserService().getUserDetails service, set states accordingly, and use the initState method to call the _getUserDetails

Secondly, we need to add methods to manage subscriptions as shown below:

//import goes here

void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  //app code goes here
  }
}

class MyHomePage extends StatefulWidget {
  //homepage code goes here
}

class _MyHomePageState extends State<MyHomePage> {
  //properties go here

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

  _getUserDetails() {
    //code goes here
  }

  _subscribe(String name) {
    setState(() {
      _isFetching = true;
    });
    UserService().createSubscription().then(
      (value) {
        UserService().subscribeUser(name).then((value) {
          setState(() {
            _isFetching = false;
            user.is_subscribed = true;
          });
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('Subscribed successfully!')),
          );
        }).catchError((e) {
          setState(() {
            _isFetching = false;
          });
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('Error processing payment!')),
          );
        });
      },
    ).catchError((e) {
      setState(() {
        _isFetching = false;
      });
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Error processing payment!')),
      );
    });
  }

  _unsubscribe(String name) {
    setState(() {
      _isFetching = true;
    });
    UserService().unSubscribeUser(name).then((value) {
      setState(() {
        _isFetching = false;
        user.is_subscribed = false;
      });
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Unsubscribed successfully!')),
      );
    }).catchError((e) {
      setState(() {
        _isFetching = false;
      });
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Error unsubscribing!')),
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    //UI code goes here
  }
}
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Lines 28-59: Creates a _subscribe that takes in name as a parameter, uses the UserService().createSubscription and UserService().subscribeUser to process the payment using the Appwrite function, and updates the details of the subscription on the database.
  • Lines 61-81: Creates an _unsubscribe that takes in name as a parameter, uses the UserService().unSubscribeUser service to cancel the subscription, and updates the details of the subscription on the database

Lastly, we need to modify the UI to conditionally show the subscribe and unsubscribe and use the appropriate method.

//import goes here

void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  //app code goes here
  }
}

class MyHomePage extends StatefulWidget {
  //homepage code goes here
}

class _MyHomePageState extends State<MyHomePage> {
  //properties go here

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

  _getUserDetails() {
    //code goes here
  }

  _subscribe(String name) {
    //code goes here
  }

  _unsubscribe(String name) {
    //code goes here
  }

  @override
  Widget build(BuildContext context) {
    return _isLoading
        ? const Center(
            child: CircularProgressIndicator(
            color: Colors.blue,
          ))
        : _isError
            ? const Center(
                child: Text(
                  'Error getting users details',
                  style: TextStyle(
                    color: Colors.red,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              )
            : Scaffold(
                body: Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      const Image(image: AssetImage('images/subscription.png')),
                      const SizedBox(height: 20),
                      const Text(
                        'Manage subscription',
                        style: TextStyle(
                            fontSize: 20.0, fontWeight: FontWeight.bold),
                      ),
                      const SizedBox(height: 20),
                      const Text(
                        'By subscribing to our service, we will deliver newspaper to you weekly',
                        style: TextStyle(fontSize: 16),
                        textAlign: TextAlign.center,
                      ),
                      const SizedBox(height: 20),
                      Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: SizedBox(
                          height: 45,
                          width: double.infinity,
                          child: user.is_subscribed
                              ? const SizedBox()
                              : TextButton(
                                  onPressed: _isLoading
                                      ? null
                                      : () {
                                          _subscribe(user.name);
                                        },
                                  style: ButtonStyle(
                                    backgroundColor:
                                        MaterialStateProperty.all<Color>(
                                            Color(0xff1C4ED8)),
                                  ),
                                  child: const Text(
                                    'Subscribe to newspaper',
                                    style: TextStyle(
                                      color: Colors.white,
                                      fontWeight: FontWeight.bold,
                                      fontSize: 14.0,
                                    ),
                                  ),
                                ),
                        ),
                      ),
                      const SizedBox(height: 5),
                      user.is_subscribed
                          ? TextButton(
                              onPressed: _isLoading
                                  ? null
                                  : () {
                                      _unsubscribe(user.name);
                                    },
                              child: const Text(
                                'Unsubscribe',
                                style: TextStyle(
                                    fontSize: 16,
                                    color: Colors.black87,
                                    fontWeight: FontWeight.bold),
                              ))
                          : const SizedBox()
                    ],
                  ),
                ),
              );
  }
}
Enter fullscreen mode Exit fullscreen mode

With that done, we restart the application using the code editor or run the command below:

flutter run
Enter fullscreen mode Exit fullscreen mode

https://media.giphy.com/media/LBWeGYmcmfAF7AhaRI/giphy.gif

We can validate the subscription by checking the Appwrite Function Executions tab and Stripe Log tab.

Appwrite Execution tab
Stripe Log tab

Conclusion

This post discussed how to create a newspaper subscription service in Flutter using Stripe and Appwrite Functions. Appwrite Function offers developers the flexibility of building applications in an isolated environment.

These resources may be helpful:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .