Mobile wallets are virtual/digital wallets that store information from loyalty cards, coupons, credit cards, and debit cards on smartphones or tablets. They offer a convenient way for users to make online and in-store payments.
In this post, we will learn how to use Appwrite’s database features to build a mobile wallet functionality in a Flutter application. The project’s GitHub repository can be found here.
Prerequisites
To fully grasp the concepts presented in this tutorial, the following are required:
- Basic understanding of Dart and Flutter
- Flutter SDK 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 install with one click on DigitalOcean or Gitpod
Getting started
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/mobile_wallet.git
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.
Set up mobile wallet service on Appwrite
To get started, we need to log into our Appwrite console, click the Create project button, input mobile_wallet
as the name, and then click Create.
Create a database, collection, attributes, and add sample data
With our project created, we can set up our application database. First, navigate to the Database tab, click the Create database button, input wallet
as the name, and then click Create.
Secondly, we need to create two collections for storing user details and the transactions performed on the wallet. To do this, click the Create collection button, input users
as the name, and then click Create. Following the same approach, we must create a transactions
collection.
Thirdly, we need to create attributes to represent our database fields. To do this, we need to navigate to the Attributes tab and create attributes for each of the collections, as shown below:
For the users
collection:
Attribute key | Attribute type | Size | Required |
---|---|---|---|
name | string | 250 | YES |
balance | integer | 5000 | YES |
We also need to update our collection permission to manage them accordingly. To do this, we need to navigate to the Settings tab, scroll to the Update Permissions section, select Any
, mark accordingly, and then click Update.
For the transactions
collection:
Attribute key | Attribute type | Size | Required |
---|---|---|---|
userId | string | 250 | YES |
name | string | 250 | YES |
paymentMethod | string | 250 | YES |
amount | integer | 5000 | YES |
Following the same steps above, we also need to update the collection permission and update.
Lastly, we need to add sample data to simulate our wallet data. To do this, we need to navigate to the Document tab, click the Create document button and add data for each of the collections as shown below:
For users
document:
name | balance |
---|---|
John Travolta | 1200 |
On creating the document, we also need to copy the document ID, as it will serve as a reference to the user when creating transactions.
For the transactions
document:
userId | name | paymentMethod | amount |
---|---|---|---|
COPIED USER ID | Groceries | internet payment | 10 |
COPIED USER ID | Call subscription | NFT | 15 |
Add an index to the Transactions collection
Databases use indexes to improve the speed of data retrieval. To add an index to the transactions
collection, navigate to the Indexes tab, click Create index button, input userId
as the index key, select userId
as the attribute and DESC
as the order, and then click Create.
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 being used to run the 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 following path:
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.mobile_wallet">
<uses-permission android:name="android.permission.INTERNET"/>
<application ...>
<activity android:name="com.linusu.mobile_wallet.CallbackActivity" android:exported="true">
<intent-filter android:label="mobile_wallet">
<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 mobile wallet service
With all that done, let’s build the mobile wallet service. First, we need to create models to convert the response sent from Appwrite to a Dart object. The models 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 userCollectionId = "REPLACE WITH USERS COLLECTION ID";
final String transactionCollectionId = "REPLACE WITH TRANSACTIONS COLLECTION ID";
final String userId = "REPLACE WITH SAMPLE ID";
final String endpoint = "ENDPOINT";
}
class User {
String? $id;
String name;
int balance;
User({this.$id, required this.name, required this.balance});
Map<dynamic, dynamic> toJson() {
return {"name": name, "balance": balance};
}
factory User.fromJson(Map<dynamic, dynamic> json) {
return User(
$id: json['\$id'], name: json['name'], balance: json['balance']);
}
}
class Transaction {
String? $id;
String userId;
String name;
String paymentMethod;
int amount;
Transaction({
this.$id,
required this.userId,
required this.name,
required this.paymentMethod,
required this.amount,
});
Map<dynamic, dynamic> toJson() {
return {
"userId": userId,
"name": name,
"paymentMethod": paymentMethod,
"amount": amount
};
}
factory Transaction.fromJson(Map<dynamic, dynamic> json) {
return Transaction(
userId: json['userId'],
name: json['name'],
paymentMethod: json['paymentMethod'],
amount: json['amount']);
}
}
We’ll need to modify the endpoint
property so that it can 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
s, functionId
, and userId
by navigating through the Appwrite console.
Next, we must create a service file to separate the application core logic from the UI. To do this, create a wallet_service.dart
file inside the lib
directory. Then add the snippet below:
import 'package:appwrite/appwrite.dart';
import 'package:mobile_wallet/utils.dart';
class WalletService {
Client _client = Client();
Databases? _db;
WalletService() {
_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
WalletService
class with_client
,_db
properties to connect to the Appwrite instance and the database - Creates an
_init
method that configures Appwrite using the properties and also conditionally creates an anonymous user to access the Appwrite database
Lastly, we need to modify the WalletService
class by adding getUserDetails
, updateUserBalance
, createTransaction
, and getTransactions
methods that use the _db
property to manage wallet operations accordingly.
//imports go here
class WalletService {
Client _client = Client();
Databases? _db;
WalletService() {
_init();
}
//initialize the application
_init() async {
//init code goes here
}
Future<User> getUserDetails() async {
try {
var data = await _db?.getDocument(
databaseId: AppConstant().databaseId,
collectionId: AppConstant().userCollectionId,
documentId: AppConstant().userId);
var user = data?.convertTo((doc) => User.fromJson(doc));
return user!;
} catch (e) {
throw Exception('Error getting user details');
}
}
Future updateUserBalance(
String userId,
String name,
int balance,
int amount,
) async {
int newBalance = balance - amount;
try {
User updatedUserBalance = User(name: name, balance: newBalance);
var result = await _db?.updateDocument(
databaseId: AppConstant().databaseId,
collectionId: AppConstant().userCollectionId,
documentId: userId,
data: updatedUserBalance.toJson());
return result;
} catch (e) {
throw Exception('Error updating user balance!');
}
}
Future createTransaction(String userId, int amount) async {
try {
Transaction updatedTransaction = Transaction(
userId: userId,
name: "Stamp duty",
paymentMethod: "internet payment",
amount: amount,
);
var data = await _db?.createDocument(
databaseId: AppConstant().databaseId,
collectionId: AppConstant().transactionCollectionId,
documentId: ID.unique(),
data: updatedTransaction.toJson(),
);
return data;
} catch (e) {
throw Exception('Error creating transaction!');
}
}
Future<List<Transaction>> getTransactions() async {
try {
var data = await _db?.listDocuments(
databaseId: AppConstant().databaseId,
collectionId: AppConstant().transactionCollectionId,
queries: [Query.orderDesc('userId')],
);
var transactionList = data?.documents
.map((transaction) => Transaction.fromJson(transaction.data))
.toList();
return transactionList!;
} catch (e) {
throw Exception('Error getting list of transactions!');
}
}
}
P.S.: In the createTransaction
method, we hard-coded transaction details for simulation purposes.
Consuming the service
With that done, we can start using the service to perform the required operation.
Get users’ details and create a transaction
To get started, we need to modify the home.dart
file in the screens
directory and update it by doing the following:
First, we need to import the required dependencies and create methods to get user details and create a transaction:
import 'package:flutter/material.dart';
import 'package:mobile_wallet/screens/transactions.dart';
import 'package:mobile_wallet/utils.dart';
import 'package:mobile_wallet/wallet_service.dart';
class Home extends StatefulWidget {
const Home({super.key});
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
late User user;
bool _isLoading = false;
bool _isTransacting = false;
bool _isError = false;
@override
void initState() {
_getUserDetails();
super.initState();
}
_getUserDetails() {
setState(() {
_isLoading = true;
});
WalletService().getUserDetails().then((value) {
setState(() {
user = value;
_isLoading = false;
});
}).catchError((e) {
setState(() {
_isLoading = false;
_isError = true;
});
});
}
_createTransaction(String userId, String name, int balance) {
setState(() {
_isTransacting = true;
});
int amount = 20;
WalletService().createTransaction(userId, amount).then((value) {
WalletService()
.updateUserBalance(userId, name, balance, amount)
.then((value) {
//update balance in app
int newBalance = user.balance - amount;
setState(() {
_isTransacting = false;
user.balance = newBalance;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Transaction created successfully!')),
);
}).catchError((e) {
setState(() {
_isTransacting = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Error creating transaction!')),
);
});
}).catchError((e) {
setState(() {
_isTransacting = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Error creating transaction!')),
);
});
}
@override
Widget build(BuildContext context) {
//UI code goes here
}
}
The snippet above does the following:
- Lines 1-4: Import the required dependencies
-
Line 13-16: Create the
user
,_isLoading
,_isTransacting
, and_isError
properties to manage application state -
Lines 18-39: Create a
_getUserDetails
method to get the user details using theWalletService().getUserDetails
service, set states accordingly, and use theinitState
method to call the_getUserDetails
-
Lines 41-75: Create a
_createTransaction
method to simulate a transaction using theWalletService().createTransaction
service, and on successful creation, uses theWalletService().updateUserBalance
service to update the wallet balance
Lastly, we need to modify the UI to conditionally show the users' details and simulate a transaction using the methods and states created above.
//import goes here
class Home extends StatefulWidget {
//code goes here
}
class _HomeState extends State<Home> {
//states goes here
@override
void initState() {
_getUserDetails();
super.initState();
}
_getUserDetails() {
//code goes here
}
_createTransaction(String userId, String name, int balance) {
//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(
backgroundColor: Colors.white,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset('images/wallet.jpg'),
const SizedBox(height: 40.0),
const Text(
'WALLET BALANCE',
style: TextStyle(
fontSize: 12.0, fontWeight: FontWeight.w500),
),
const SizedBox(height: 5.0),
Text(
'\$ ${user.balance}',
style: TextStyle(
fontSize: 32.0, fontWeight: FontWeight.bold),
),
const SizedBox(height: 40.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
OutlinedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const Transactions()),
);
},
child: Text(
'View transactions',
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black),
),
style: ButtonStyle(
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5.0),
),
),
side: MaterialStateProperty.all(
const BorderSide(
color: Colors.black,
width: 1.5,
style: BorderStyle.solid),
),
),
),
const SizedBox(width: 20.0),
TextButton(
onPressed: _isTransacting
? null
: () {
_createTransaction(
user.$id.toString(),
user.name,
user.balance,
);
},
child: Text(
'Spend from wallet',
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.white),
),
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(Colors.black),
side: MaterialStateProperty.all(
const BorderSide(
color: Colors.black,
width: 1.5,
style: BorderStyle.solid),
),
padding:
MaterialStateProperty.all<EdgeInsets>(
const EdgeInsets.symmetric(
horizontal: 15.0),
)),
),
],
),
)
],
),
),
);
}
}
Get the list of transactions
To obtain the list of transactions, we need to modify the transactions.dart
file in the same screens
directory as shown below:
import 'package:flutter/material.dart';
import 'package:mobile_wallet/utils.dart';
import 'package:mobile_wallet/wallet_service.dart';
class Transactions extends StatefulWidget {
const Transactions({super.key});
@override
State<Transactions> createState() => _TransactionsState();
}
class _TransactionsState extends State<Transactions> {
late List<Transaction> transaction;
bool _isLoading = false;
bool _isError = false;
@override
void initState() {
_getTransactions();
super.initState();
}
_getTransactions() {
setState(() {
_isLoading = true;
});
WalletService().getTransactions().then((value) {
setState(() {
transaction = value;
_isLoading = false;
});
}).catchError((e) {
setState(() {
_isLoading = false;
_isError = true;
});
});
}
@override
Widget build(BuildContext context) {
return _isLoading
? const Center(
child: CircularProgressIndicator(
color: Colors.blue,
))
: _isError
? const Center(
child: Text(
'Error getting list of transactions',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
)
: Scaffold(
appBar: AppBar(
title: const Text('Transactions'),
backgroundColor: Colors.black,
),
body: ListView.builder(
itemCount: transaction.length,
itemBuilder: (context, index) {
return Container(
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(width: .5, color: Colors.grey),
),
),
padding: EdgeInsets.fromLTRB(10, 20, 10, 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
flex: 7,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
transaction[index].name,
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w800),
),
SizedBox(height: 10.0),
Text(transaction[index].paymentMethod)
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
SizedBox(height: 10.0),
Text('\$ ${transaction[index].amount}')
],
),
],
),
);
},
),
);
}
}
The snippet above does the following:
- Lines 1-3: Import the required dependencies
-
Lines 11-13: Create the
transactions
,_isLoading
, and_isError
properties to manage the application state -
Lines 15-36: Create a
_getTransactions
method to get the list of transactions using theWalletService().getTransactions
service, set states accordingly, and use theinitState
method to call the_getTransactions
method when the object is inserted into the tree - Modifies the UI widgets to show the transactions stored on Appwrite
With that done, we restart the application using the code editor or run the command below:
flutter run
https://media.giphy.com/media/f7i8RWsiXAIdXDOoBW/giphy.gif
Conclusion
This post discussed how to use Appwrite’s database features to build a mobile wallet functionality in a Flutter application. We built a base implementation to demonstrate Appwrite's support for scaffolding a working prototype. With Appwrite, we can further extend the application's functionality by leveraging features like real-time communication, localization, authentication, etc
These resources may be helpful: