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:
- Basic understanding of Dart and Flutter
- Flutter SDK installed
- Appwrite CLI installed
- Xcode with a developer account (for Mac users)
- Either IOS Simulator, Android Studio, or Chrome web browser to run our application
- An Appwrite instance; check out this article on how to set up an instance locally or one-click install on DigitalOcean or Gitpod
- A Stripe account (signup for a trial account, it is completely free)
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.
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:
//remaining env variable goes here
_APP_FUNCTIONS_RUNTIMES=dart-2.17
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
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
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
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
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>
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
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>
The command will create a starter Dart 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
We also need to install the specified dependency by running the command below:
cd functions/function_stripe/
dart pub get
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!"
});
}
}
The snippet above does the following:
- Imports the required dependencies
-
Line 7-14: Creates an API
headers
object using the Secret Key to setupAuthorization
-
Line 16-20: Creates an API body that consists of
amount
,currency
, andpayment_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
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.
We can also confirm the deployment by navigating to the Function tab on the Appwrite console.
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.
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
Running the Project
First, we need to install the project dependencies by running the command below:
flutter pub get
Then, run the project using the command below:
flutter run
The command above will run the application on the selected device.
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 |
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.
Add Platform Support
To add support for our Flutter app, navigate to the Home menu and click the Flutter App button.
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
.
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.
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.
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>
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']);
}
}
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:
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";
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);
}
}
}
}
The snippet above does the following:
- Imports the required dependencies
- Creates a
UserService
class withclient
,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');
}
}
}
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
}
}
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 theUserService().getUserDetails
service, set states accordingly, and use theinitState
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
}
}
The snippet above does the following:
-
Lines 28-59: Creates a
_subscribe
that takes inname
as a parameter, uses theUserService().createSubscription
andUserService().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 inname
as a parameter, uses theUserService().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()
],
),
),
);
}
}
With that done, we restart the application using the code editor or run the command below:
flutter run
https://media.giphy.com/media/LBWeGYmcmfAF7AhaRI/giphy.gif
We can validate the subscription by checking the Appwrite Function Executions tab and 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: