Tích hợp thêm thanh toán ngang hàng (P2P Payment) vào ứng dụng Flutter tạo ra trải nghiệm in-app phong phú hơn cho người dùng cuối của bạn. Tuy nhiên, bạn cần đảm bảo quá trình thanh toán của mình diễn ra nhanh chóng và an toàn.
Trong hướng dẫn này, bạn sẽ học cách làm thế nào để tích hợp giải pháp thanh toán ngang hàng cho ứng dụng Chat Stream Flutter sử dụng ví điện tử in-app cung cấp cả tốc độ và bảo mật.
Hướng dẫn sẽ đi vào chi tiết những phần sau:
- Dịch vụ thanh toán ngang hàng là gì?
- Cài đặt môi trường cho app Stream Flutter của bạn :
1.Cài đặt Stream
2.Cài đặt Rapyd - Tạo app Flutter
- Thêm một Tiện ích hành động tùy chỉnh
- Thêm một chức năng đầu vào thanh toán
- Thêm bản xem trước hình thu nhỏ của tệp đính kèm
- Tạo một ví điện tử với ứng dụng Rapyd phía khách:
1.Tạo một chữ ký
2.Chuyển tiền
3.Xác nhận một giao dịch - Thực hiện một giao dịch
- Tạo bản xem trước tệp đính kèm tủy chỉnh
- Kết thúc
Dịch vụ thanh toán ngang hàng là gì?
Dịch vụ thanh toán ngang hàng (P2P payment services) cung cấp một nền tảng an toàn để người dùng cuối thực hiện các giao dịch tài chính in-app với người dùng hoặc với doanh nghiệp khác (Venmo, Zelle hay PayPal, ở Việt Nam có ví Momo, ZaloPay, ViettelPay, Moca và rất nhiều).
Để những giao dịch di động này khả thi, người dùng phải liên kết thẻ tín dụng hoặc chi tiết tài khoản ngân hàng vào tài khoản ứng dụng của họ. Khi bạn gửi cho một người dùng khác một khoản thanh toán, họ có thể giữ tài khoản thanh toán P2P trong ví điện tử để sử dụng trong tương lai hoặc chuyển khoản đó vào tài khoản ngân hàng của họ.
Cài đặt môi trường cho ứng dụng chat Stream Flutter
Trước khi bắt đầu, bạn cần có:
- Tài khoản stream để truy cập Stream Chat Messaging API.
- Tài khoản Rapyd để tạo các thanh toán P2P sử dụng Rapyd Wallet API.
Cài đặt Stream
Nếu bạn chưa có tài khoản Stream nào, bạn có thể bắt đầu bản dùng thử Stream's Chat Messaging (1 tháng).
Nếu bạn đang làm dự án cá nhân hoặc làm chủ một doanh nghiệp nhỏ, bạn có thể đăng kí một tài khoản Stream Maker Account và truy cập Stream Chat miễn phí vô thời hạn
Sau khi tạo xong tài khoản Stream, bạn có thể xem bảng điều khiển và ứng dụng đầu tiên của bạn.
App của bạn sẽ có:
- **API Key **
- Secret
API Key của bạn chỉ là định danh app thôi và nó an toàn để chia sẻ công khai. Secret giúp bạn sinh ra những chuỗi token xác thực người dùng và nên giữ riêng tư.
Từ bảng điều khiển này, bạn cũng có thể chỉnh sửa tên app và tạo những app mới khác.
###Cài đặt Rapyd Bạn sẽ cần một tài khoản Rapyd để tạo giải pháp thanh toán P2P sử dụng API của ví Rapyd
Sau khi đăng ký, bạn sẽ được điều hướng về Rapyd Client Portal (Cổng khách hàng Rapyd )*
Trong cổng khách hàng:
- Bật nút Sandbox(điều này cung cấp cho bạn dữ liệu mẫu để làm việc, để bạn có thể kiểm tra các giao dịch bằng ví thử nghiệm của họ)
- Đến trang Developers và lưu lại Secret Key và Access Key (cả hai đều yêu cầu để truy cập vào API của Rapyd từ app của bạn).
Tạo app Flutter của bạn
Trong cửa sổ terminal, tạo một project Flutter bằng lệnh sau:
flutter create stream_payment
⚠️Lưu ý: Hướng dẫn thử nghiệm này dùng Flutter bản 2.2.3
Mở project trong IDE mà bạn ưa thích, và thêm các pakages sau vào file pubspec.yaml
:
dependencies:
stream_chat_flutter: ^2.0.0-nullsafety.8
http: ^0.13.3
loading_overlay: ^0.3.0
Thay thế code trong file main.dart
bằng dưới đây:
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'screens/channel_list_page.dart';
import 'secrets.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final client = StreamChatClient(
STREAM_KEY,
logLevel: Level.OFF,
);
const USER_ID = 'sbis04';
await client.connectUser(
User(
id: USER_ID,
extraData: {
'name': 'Souvik Biswas',
'image': '<https://i.pravatar.cc/150?img=8>',
},
),
USER_TOKEN,
);
final channel = client.channel('messaging', id: 'p2p-payment');
await channel.watch();
runApp(MyApp(client, channel));
}
class MyApp extends StatelessWidget {
final StreamChatClient client;
final Channel channel;
MyApp(this.client, this.channel);
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, widget) {
return StreamChat(
child: widget!,
client: client,
);
},
debugShowCheckedModeBanner: false,
home: ChannelListPage(channel),
);
}
}
Giải thích code: Trong đoạn code trên, bạn đã:
- Khởi tạo một StreamChatClient dùng Flutter SDK của Stream.
- Kết nối người dùng và cài đặt kênh bằng StreamChatClient.
- Gọi công cụ khởi tạo StreamChat bên trong
MyApp
và hiển thị ChannelListPage, cái mà chứa một danh sách kênh cho app Stream của bạn. STREAM_KEY và USER_TOKEN được định nghĩa trongsecrets.dart
, như sau:
// Stream secrets
const STREAM_KEY = 'key-here';
const USER_TOKEN = 'user-token-here';
Để lấy STREAM_KEY và USER_TOKEN:
- Copy API Secret của bạn từ bảng điều khiển Stream.
- Đi đến Stream's User JWT Generator.
- Trong trường Your secret, dán API Secret vào.
- Trong trường User ID, nhập chuỗi duy nhất để định danh người dùng của bạn.
⚠️Lưu ý: Trong kịch bản production, bạn phải sinh token dử dụng server của bạn và server SDKs của Stream. Bạn đừng bao giờ nên hardcode token trong ứng dụng production.
Kế tiếp, bạn sẽ hiển thị một danh sách kênh nơi mà người dùng hiện tại là một thành viên. Sẽ tốt hơn khi chia code thành nhiều file để dễ maintain hơn khi khối lượng code lớn lên.
Tạo file mới lấy tên làchannel_list_page.dart
và thêm vào code sau:
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'channel_page.dart';
class ChannelListPage extends StatelessWidget {
final Channel channel;
const ChannelListPage(this.channel);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Stream Chat'),
),
body: ChannelsBloc(
child: ChannelListView(
filter: Filter.in_('members', [StreamChat.of(context).user!.id]),
sort: [SortOption('last_message_at')],
pagination: PaginationParams(limit: 30),
channelWidget: Builder(
builder: (context) => ChannelPage(channel),
),
),
),
);
}
}
Để hiển thị màn hình danh sách tin nhắn, tạo file mới tên là channel_page.dart
và thêm vào code sau:
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class ChannelPage extends StatelessWidget {
final Channel channel;
const ChannelPage(this.channel);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: ChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: MessageListView(),
),
MessageInput(),
],
),
);
}
}
Chú ý rằng bạn đã truyền biến channel
vào như là một đối số của hàm tạo ; bạn sẽ cần nó cho những phần sau này.
Nếu bạn chạy app lúc này, bạn sẽ nhận thấy rằng - chỉ mới một lượng nhỏ code - bạn đã có một app nhắn tin tương đối mạnh mẽ và hoàn thiện bởi các chức năng cần thiết 😎.
https://i.imgur.com/tU2p2pO.gif?auto=format&fit=clip&ixlib=react-9.0.3&w=798
Thêm một Công cụ Tùy chỉnh Hành động (Custom Action Widget)
Tiếp theo, thêm một công cụ tùy chỉnh hành động để thuật tiện hơn cho người dùng khi kích hoạt một thanh toán. Ví dụ, trong MessageInput bạn có thể thêm một IconButton như dưới đây:
Để thêm một IconButton, thêm đối số tùy chỉnh actions
vào channel_page.dart
:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: ChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: MessageListView(),
),
MessageInput(
actions: [
IconButton(
icon: Icon(Icons.payment),
onPressed: _onPaymentRequestPressed,
), // custom action
],
),
],
),
);
}
Tiếp theo, định nghĩa điều gì sẽ xảy ra khi người dùng chọn vào icon thanh toán. Để xử lí logic này, ta làm như sau:
- Tạo phương thức _onPymentRequestPressed()
- Trong phương thức này, tạo một màn TransactionPage mà cho phép người dùng nhập vào số tiền mà họ sẽ gửi.
- Tạo một phương thức PageRouteBuilder mà bao quanh tiện ích bằng một hình nền bán trong suốt (semi-transparent background).
Future<void> _onPaymentRequestPressed() async {
final String? amount = await Navigator.of(context).push(
PageRouteBuilder(
opaque: false,
pageBuilder: (_, __, ___) => TransactionPage(
destinationWalletAddress: 'ewallet-123',
),
),
);
}
⚠️Lưu ý: Vào thời điểm này, bạn dùng một placeholder cho ví đích. Bạn sẽ update cái placeholder này sau khi định nghĩa Rapyd khách.
Sau khi nhận được số lượng:
- Tạo một Attachment (file đính kèm) Stream Chat.
- Phân loại type (loại) để thanh toán (payment).
- Đặt extraData là key của amount và value đặt thành amount mà người dùng vừa nhập vào.
Future<void> _onPaymentRequestPressed() async {
// ...
if (amount != null) {
_messageInputKey.currentState?.addAttachment(
Attachment(
type: 'payment',
uploadState: UploadState.success(),
extraData: {"amount": int.parse(amount)},
),
);
}
}
Để làm điều này, tạo một GlobalKey:
class _ChannelPageState extends State<ChannelPage> {
GlobalKey<MessageInputState> _messageInputKey = GlobalKey();
// ...
}
Sau đó, đặt key cho MessageInput
:
MessageInput(
key: _messageInputKey, // add key here
actions: [
IconButton(
icon: Icon(Icons.payment),
onPressed: _onPaymentRequestPressed,
),
],
)
Thêm đầu vào cho chức năng thanh toán
Tiếp theo, bạn sẽ tạo hàng loạt các elements cho tất cả chức năng thanh toán.
Đầu tiền, tạo TransactionPage nơi mà người dùng có thể nhập vào số tiền giao dịch.
Màn hình này sẽ chứa một TextField
để lấy input từ người dùng và xem trước địa chỉ ví đích sẽ truyền vào.
Tạo một file lấy tên là transaction_page.dart
, và thêm vào code sau:
import 'package:flutter/material.dart';
class TransactionPage extends StatelessWidget {
final String destinationWalletAddress;
TransactionPage({required this.destinationWalletAddress});
final TextEditingController _amountTextController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black26,
body: Center(
child: Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
child: Container(
width: double.maxFinite,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20.0),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: double.maxFinite,
decoration: BoxDecoration(
color: Color(0xFF4161ff),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.0),
topRight: Radius.circular(20.0),
),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Sending To',
style: TextStyle(
color: Colors.white70,
fontSize: 16.0,
),
),
SizedBox(height: 4),
Text(
destinationWalletAddress,
style: TextStyle(
color: Colors.white,
fontSize: 14.0,
),
),
],
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Container(
width: 150,
child: TextField(
// textAlign: TextAlign.center,
controller: _amountTextController,
decoration: InputDecoration(
isDense: true,
prefixIcon: Padding(
padding: const EdgeInsets.only(right: 4.0),
child: Text(
'\\$',
style: TextStyle(fontSize: 40.0),
),
),
prefixIconConstraints:
BoxConstraints(minWidth: 0, minHeight: 0),
),
keyboardType: TextInputType.number,
style: TextStyle(fontSize: 80.0),
),
),
),
Padding(
padding: const EdgeInsets.all(24.0),
child: Container(
width: double.maxFinite,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Color(0xFF4161ff),
),
onPressed: () {
if (_amountTextController.text != '') {
Navigator.pop(context, _amountTextController.text);
}
},
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Text(
'Pay',
style: TextStyle(color: Colors.white, fontSize: 24.0),
),
),
),
),
)
],
),
),
),
),
);
}
}
Tạo một tệp đính kèm xem trước thu nhỏ
Để hiển thị tệp đình kèm xem trước thu nhỏ tùy chỉnh, bạn phải dùng thuộc tính attachmentThumnailBuilders
bên trong công cụ MessageInput
. Điều này để bạn tạo ra một công cụ tùy chỉnh để hiển thị thanh toán xem trước trong khi người dùng nhập đang soạn trong tin nhắn mà họ muốn gửi kèm nó.
MessageInput(
key: _messageInputKey,
attachmentThumbnailBuilders: {
'payment': (context, attachment) => TransactionAttachment(
amount: attachment.extraData['amount'] as int,
)
},
actions: [
IconButton(
icon: Icon(Icons.payment),
onPressed: _onPaymentRequestPressed,
),
],
)
Chèn code vào công cụ TransactionAttachment:
class TransactionAttachment extends StatelessWidget {
final int amount;
const TransactionAttachment({required this.amount});
@override
Widget build(BuildContext context) {
return Container(
color: Color(0xFF4161ff),
width: double.maxFinite,
height: 300,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.payment,
color: Colors.white,
size: 40,
),
SizedBox(width: 8),
Column(
children: [
Text(
'$amount USD',
style: TextStyle(color: Colors.white, fontSize: 20.0),
),
],
),
],
),
);
}
}
Dưới đây là ví dụ đính kèm với số tiền 5$:
Với những cài đặt UI căn bản để xử lí thanh toán, bạn có thể tích hợp thanh toán P2P với ví Rapyd
Tạo một ví điện tử với Rapyd Client của bạn
Bạn phải dùng ví để xử lí thanh toán và kích hoạt giao dịch giữa những người dùng. Khi đã bật Sandbox trong Rapyd Client Portal, bạn có thể truy cập số lượng ví mẫu với số lượng Account Balance (số dư tài khoản
) mặc định.
Bạn có thể chọn 2 ví bất kì. Bạn sẽ chọn một ví là ví nguồn source wallet (ví gửi thanh toán) và ví kia là ví đích destination wallet(ví nhận thanh toán).
Vì địa chỉ ví là duy nhất cho mỗi user, bạn có thể lưu địa chỉ ví là extra data trong tài khoản người dùng Stream.
Trong file main.dart
, truyền vào địa chỉ ví nguồn trong extraData
của User:
// replace with your source wallet address
const String _myWalletId = 'ewallet_3d63cc520dff85b043914de569390fd1';
const USER_ID = 'sbis04';
await client.connectUser(
User(
id: USER_ID,
extraData: {
'wallet_id': _myWalletId, // pass it here
'name': 'Souvik Biswas',
'image': '<https://i.pravatar.cc/150?img=8>',
},
),
USER_TOKEN,
);
Điều này sẽ lưu địa chỉ ví được chỉ định, cho người dùng này, trong cơ sở dữ liệu của Stream. Một cách tương tự, bạn có thể tạo một người dùng khác với địa chỉ ví riêng trên Stream. Dưới đây là demo hiển thị nhiều người dùng và thông tin của họ:
Để xem người dùng của bạn:
- Đi đến Stream Dashboard.
- Chọn Options.
- Chọn Open in Chat Explorer.
Để tạo Rapyd Client, tạo một file tên là
apyd_client.dart
và định nghĩa lớpRapydClient
bên trong nó như sau:
class RapydClient {
final _baseURL = '<https://sandboxapi.rapyd.net>';
final _accessKey = RAPYD_ACCESS_KEY;
final _secretKey = RAPYD_SECRET_KEY;
// ...
}
Lấy Access Key và Secret Key Rapyd của bạn trong Rapyd Client Portal và lưu chúng bên trong file secrets.dart
:
// Rapyd secrets
const RAPYD_ACCESS_KEY = 'access-key-here';
const RAPYD_SECRET_KEY = 'secret-key-here';
Sinh một chữ ký (Generate a signature)
Để sử dụng Rapyd API, bạn phải tính toán và truyền vào một chữ kí trong request headers. Công thức được cung cấp bởi Rapyd để tính toán chữ ký như sau:
signature = BASE64 ( HASH ( http_method + url_path + salt + timestamp + access_key + secret_key + body_string ) )
⚠️Lưu ý: Vui lòng xem Rapyd documentation để biết thêm các ví dụ và cân nhắc về bảo mật.
Quá trình này sẽ có một chút khó khăn để thực thi chính xác bằng cách sử dụng Dart, nhưng đây là toàn bộ quá trình sinh chữ ký:
Map<String, String> _generateHeader({
required String method,
required String endpoint,
String body = '',
}) {
int unixTimetamp = DateTime.now().millisecondsSinceEpoch;
String timestamp = (unixTimetamp / 1000).round().toString();
var salt = _generateSalt();
var toSign = method + endpoint + salt + timestamp + _accessKey + _secretKey + body;
var keyEncoded = ascii.encode(_secretKey);
var toSignEncoded = ascii.encode(toSign);
var hmacSha256 = Hmac(sha256, keyEncoded); // HMAC-SHA256
var digest = hmacSha256.convert(toSignEncoded);
var ss = hex.encode(digest.bytes);
var tt = ss.codeUnits;
var signature = base64.encode(tt);
var headers = {
'Content-Type': 'application/json',
'access_key': _accessKey,
'salt': salt,
'timestamp': timestamp,
'signature': signature,
};
return headers;
}
String _generateSalt() {
final _random = Random.secure();
// Generate 16 characters for salt by generating 16 random bytes
// and encoding it.
final randomBytes = List<int>.generate(16, (index) => _random.nextInt(256));
return base64UrlEncode(randomBytes);
}
Phương thức _genetateHeader
được dùng để tạo ra các headers đúng, đặc biệt là chữ ký signature. Điều này yêu cầu phương thức HTTP
, endpoint, và JSON data (nếu có) để tạo chữ kí chính xác.
Giao dịch tiền
Với một phần khó khăn, bạn có thể tạo một phương thức giao dịch tiền từ một ví sang ví khác.
Điều này yêu cầu địa chỉ ví nguồn, địa chỉ ví đích, và số tiền giao dịch.
Future<Transfer?> transferMoney({
required String sourceWallet,
required String destinationWallet,
required int amount,
}) async {
Transfer? transferDetails;
var method = "post";
var transferEndpoint = '/v1/account/transfer';
final transferURL = Uri.parse(_baseURL + transferEndpoint);
}
Lớp Transfer
là một mô hình định nghĩa người dùng mà chp phét bạn lưu chi tiết của một giao dịch Rapyd, để có mô hình Transfer
, copy+paste code từ Peer-to-Peer Github repo.
Định nghĩa JSON data mà bạn sẽ gửi như là một phần của yêu cầu giao dịch:
Future<Transfer?> transferMoney({
required String sourceWallet,
required String destinationWallet,
required int amount,
}) async {
// ...
var data = jsonEncode({
"source_ewallet": sourceWallet,
"amount": amount,
"currency": "USD",
"destination_ewallet": destinationWallet,
});
}
Lấy headers bằng cách sử dụng phương thức _generateHeader()
được định nghĩa sớm hơn:
Future<Transfer?> transferMoney({
required String sourceWallet,
required String destinationWallet,
required int amount,
}) async {
//...
final headers = _generateHeader(
method: method,
endpoint: transferEndpoint,
body: data,
);
}
Bây giờ, dùng phương thức http_post()
để gửi request đến API:
Future<Transfer?> transferMoney({
required String sourceWallet,
required String destinationWallet,
required int amount,
}) async {
//...
try {
var response = await http.post(
transferURL,
headers: headers,
body: data,
);
print(response.body);
if (response.statusCode == 200) {
print('SUCCESSFULLY TRANSFERRED');
transferDetails = Transfer.fromJson(jsonDecode(response.body));
}
} catch (e) {
print('Failed to transfer amount');
}
return transferDetails;
}
Nếu gửi request thành công (mã trạng thái 200), body trả về sẽ có chứa chi tiết của giao dịch:
{
"status": {
"status": "SUCCESS",
"operation_id": "1845c31a-c1e6-4df4-8cb2-002d2dab250b"
},
"data": {
"id": "bf61f300-dc1d-11eb-b38b-02240218ee6d",
"status": "PEN",
"amount": 5,
"currency_code": "USD",
"destination_phone_number": "+611868065687",
"destination_ewallet_id": "ewallet_3d63cc520dff85b043914de569390fd1",
"destination_transaction_id": "",
"source_ewallet_id": "ewallet_b57a9c68acfea31b35990d215ba0eb8c",
"source_transaction_id": "wt_2d00834757d8255cd656379926f1ea21",
"created_at": 1625330588
}
}
Bạn có thể dán chuỗi JSON trả về và lưu nó lại ở bên trong đối tượng Transfer
. Ví dụ, final transfer = Transfer.fromJson(json)
.
JSON trả về sẽ chứa một transaction ID
với trạng thái PEN (đang chờ - pending). Để xác nhận giao dịch, bạn phải thực hiện thêm một POST request.
Xác nhận giao dịch
Để xác nhận giao dịch, bạn phải gửi một Transfer
request đến endpoint /v1/account/transfer/response
.
Để làm vậy, bạn phải định nghĩa một phương thức mới mà lấy những tham số sau:
- ID giao dịch *(`"id": id)
- Status ("status": response)
Future<Transfer?> transferResponse({
required String id,
required String response,
}) async {
Transfer? transferDetails;
var method = "post";
var responseEndpoint = '/v1/account/transfer/response';
final responseURL = Uri.parse(_baseURL + responseEndpoint);
var data = jsonEncode({
"id": id,
"status": response,
});
}
JSON data mà bạn sẽ gửi nên chứa ID và status. Dùng phương thức _generateHeader()
để sinh ra các header, và dùng phương thức http.post
để gửi đi request:
Future<Transfer?> transferResponse({
required String id,
required String response,
}) async {
// ...
final headers = _generateHeader(
method: method,
endpoint: responseEndpoint,
body: data,
);
try {
var response = await http.post(
responseURL,
headers: headers,
body: data,
);
print(response.body);
if (response.statusCode == 200) {
print('TRANSFER STATUS UPDATED: $response');
transferDetails = Transfer.fromJson(jsonDecode(response.body));
}
} catch (e) {
print('Failed to update transfer status');
}
return transferDetails;
}
Sau khi xác nhận giao dịch, trạng thái của nó sẽ được update thành một trong những cái sau:
CLO
(hoặc closed), nghĩa là bạn đã hoàn thành giao dịch thành côngDEC
(hoặc declined), nghĩa là bạn đã từ chối giao dịch. Bây giờ bạn đã định nghĩa xong Rapyd Client, bạn có thể dùng các phương thức trong phần tiếp theo để tạo ra một giao dịch thành công.
Thực hiện một giao dịch
Bạn nên thực hiện giao dịch trước khi gửi đi hoặc hiển thị tin nhắn bên trong MessageListView
. Nơi tốt nhất để làm điều này là callback preMessageSending
.
Trước khi thực hiện giao dịch, bạn phải lấy các địa chỉ ví nguồn và ví đích.
Định nghĩa một phương thức tên là getWallets()
bên trong lớp _ChannelPageState
:
late final String _sourceWalletId;
late final String _destinationWalletId;
getWallets() async {
var members = await widget.channel.queryMembers();
var destId = members.members[1].user!.extraData['wallet_id'] as String;
var sourceId = members.members[0].user!.extraData['wallet_id'] as String;
_sourceWalletId = sourceId;
_destinationWalletId = destId;
}
@override
void initState() {
super.initState();
getWallets();
}
Code này dùng channel
để lấy các member, và lưu địa chỉ ví riêng của mỗi member tương ứng.
Thực hiện giao dịch trong callback preMessageSending
:
MessageInput(
key: _messageInputKey,
preMessageSending: (msg) => _performTransaction(msg), // this
attachmentThumbnailBuilders: {
'payment': (context, attachment) => TransactionAttachment(
amount: attachment.extraData['amount'] as int,
)
},
actions: [
IconButton(
icon: Icon(Icons.payment),
onPressed: _onPaymentRequestPressed,
),
],
)
Trong phương thức _performTransaction()
, kiểm tra xem tin nhắn có chứa tệp đính kèm với amount
như là extra data hay không.
Bằng cách này, bạn có thể xác minh là tệp đính kèm liên quan đến thanh toán.
Boolean _isSending
cho biết liệu giao dịch có đang diễn ra hay không. Nếu giao dịch đang diễn ra, không cho phép người dùng gửi bất kì tin nhắn nào, thay vào đó, hiển thị một chỉ báo tiến trình.
Lấy amount
từ tệp đính kèm và thực thi giao dịch sử dụng RapydClient
. Nếu giao dịch thành công, cập nhật tin nhắn file đính kèm.
RapydClient _rapydClient = RapydClient();
Future<Message> _performTransaction(Message msg) async {
if (msg.attachments.isNotEmpty &&
msg.attachments[0].extraData['amount'] != null) {
setState(() {
_isSending = true;
});
// Retrieve the amount
int amount = msg.attachments[0].extraData['amount'] as int;
// Process the transaction
var transactionInfo = await _rapydClient.transferMoney(
amount: amount,
sourceWallet: _sourceWalletId,
destinationWallet: _destinationWalletId,
);
// Confirm the transaction
var updatedInfo = await _rapydClient.transferResponse(
id: transactionInfo!.data.id, response: 'accept');
// Update the attachment
msg.attachments[0] = Attachment(
type: 'payment',
uploadState: UploadState.success(),
extraData: updatedInfo!.toJson(),
);
setState(() {
_isSending = false;
});
}
return msg;
}
Khi giao dịch hoàn tất, gửi tin nhắn với tệp đình kèm tùy chỉnh sẽ hiển thị trong danh sách tin nhắn.
Tạo chức năng xem trước tệp đính kèm tùy chỉnh
Bạn có thể tạo tệp đính kèm tùy chỉnh và truyền nó vào MessageListView
sử dụng thuộc tính customAttachmentBuiders
MessageListView(
customAttachmentBuilders: {'payment': _buildPaymentMessage},
)
Công cụ tùy chỉnh được định nghĩa trong phương thức _buildPaymentMessage()
Widget _buildPaymentMessage(
BuildContext context,
Message details,
List<Attachment> _,
) {
final transaction = Transfer.fromJson(details.attachments.first.extraData);
final transactionInfo = transaction.data;
int amount = transactionInfo.amount;
String destWalletAddress = transactionInfo.destinationEwalletId;
String status = transactionInfo.status;
return wrapAttachmentWidget(
context,
TransactionWidget(
transaction: transaction,
destWalletAddress: destWalletAddress,
amount: amount,
status: status,
),
RoundedRectangleBorder(),
true,
);
}
Bây giờ, bạn lấy ra các thuộc tính yêu cầu của giao dịch lưu ở trong lớp Transfer
.
TransactionWidget
sẽ hiển thị tệp đính kèm giao dịch. Lấy code cho công cụ này trong Peer-to-peer Github repo.
Gói lại nội dung của TransactionWidget
bằng một InkWell
và điều hướng đến DetailPage
.
class TransactionWidget extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
Navigator.of(context).push(
PageRouteBuilder(
opaque: false,
pageBuilder: (_, __, ___) => DetailPage(
transaction: transaction,
),
),
);
}
// ...
);
}
}
DetailPage
chứa thông tin giao dịch. Bạn có thể tìm thấy UI code ở trang Peer-to-Peer GitHub repo.
Bước cuối cùng yêu cầu cờ Boolean (_isSending
) bạn dùng để chỉ ra tiến trình giao dịch.
Dùng cờ Boolean để cập nhật UI bằng cách hiển thị chỉ báo tiến trình với sự trợ giúp của gói loading_overlay
.
Gói lại nội dung của Scaffold
với công cụ LoadingOverlay
và sử dụng Boolean để chỉ ra liệu giao dịch có đang trong quá trình hay không.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: ChannelHeader(),
body: LoadingOverlay(
isLoading: _isSending,
color: Colors.black,
// ...
),
);
}
Xem quá trình giao dịch trong hành động: tại đây
Tóm lại
Chúc mừng🎉, bạn đã triển khai thành công một giải pháp thanh toán ngang hàng sử dụng Fluuter SDK của Stream và API của ví Rapyd.
Người dùng hiện nay có thể gửi và nhận thanh toán bên trong ứng dụng nhắn tin của bạn.
Link bài viết gốc: https://getstream.io/blog/p2p-integration-with-flutter/