Flutter
Integrate Commerce in Flutter with webview_flutter
Our Commerce SDK works seamlessly in Flutter apps by loading Commerce in a WebView and bridging JavaScript callbacks to Dart.
Doing the same for your mobile app is pretty straightforward and we've outlined the steps below. π
Step 1: Obtain your API Key
To get set up with API Keys please email us at support@bidali.com with your request. We will provide you with API keys for our sandbox and production environments.
The Commerce URLs to use in the webview will be:
| Environment | URL |
|---|---|
| Sandbox | https://commerce.staging.bidali.com/dapp?key=YOUR_API_KEY |
| Production | https://commerce.bidali.com/dapp?key=YOUR_API_KEY |
When specifying the accepted payment options and providing balances, Bidali uses a unique internal identifier named protocol. You can view the list of supported protocols below.
| Environment | URL |
|---|---|
| Sandbox | https://docs.bidali.com/commerce/currencies?environment=testnet |
| Production | https://docs.bidali.com/commerce/currencies |
Step 2: Load the Commerce URL in a WebView
Use webview_flutter with JavaScript enabled, register a JavaScriptChannel for messages from Bidali, and inject window.bidaliProvider when the page starts loading.
late final WebViewController controller;
controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
'BidaliChannel',
onMessageReceived: onMessage,
)
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (url) {
controller.runJavaScript(initialJavascript);
},
),
)
..loadRequest(Uri.parse(commerceUrl));See full sample code below
Unlike React Native's window.ReactNativeWebView.postMessage, Flutter receives JS β native messages through a named JavaScriptChannel (e.g. BidaliChannel.postMessage(...)). Native β JS calls use controller.runJavaScript(...) instead of injectJavaScript.
Step 3: Add a bidaliProvider onto the window and specify the paymentCurrencies that you will be accepting
window.bidaliProvider = {
key: 'YOUR API KEY', // This is provided by Bidali
name: 'YOUR WALLET NAME', // This will be provided by Bidali with your apiKey
paymentCurrencies: ['stellar', 'stellarusdc'],
brandColor: '#35D07F',
email: 'test@test.com',
phoneNumber: '15554321234',
balances: {
'stellar': '5',
'usdcstellar': '20'
},
onPaymentRequest: (paymentRequest) => {
var payload = { method: 'onPaymentRequest', data: paymentRequest };
BidaliChannel.postMessage(JSON.stringify(payload));
},
openUrl: function (url) {
var payload = { method: 'openUrl', data: { url } };
BidaliChannel.postMessage(JSON.stringify(payload));
}
};window.bidaliProvider must have the following properties:
| Property | Type | Description |
|---|---|---|
name | String | The name of the wallet integrating (Provided by Bidali) |
key | String | This is your Bidali Commerce Publishable Key. The unique and public key that identifies your organization. It looks like pk_viqah4squsnuw6p0e1ue.(Provided by Bidali) |
paymentCurrencies | String[] | Limits the payment options to the currencies you list. It takes an Array of protocol strings (Bidali's unique identifier for each currency) |
brandColor | String | Hex color used to theme CTA button backgrounds, ex: #35D07F |
limitToCountries | String[] | Country codes of the countries to limit the products to, ex: [ "US", "CA" ] |
email | String | Allows us to pre-populate their email, user has to confirm during checkout flow |
phoneNumber | String | Allows us to pre-populate the phone number for mobile topups, the user has the chance to change this |
balances | object | The balances for the allowed currencies, ex. { stellar: '25.2', usdcstellar: '2000' } |
onPaymentRequest | Function | Provide a handler for this function that displays a payment confirmation modal for the user |
openUrl | Function | Opens a URL to our support docs, etc. Ideally should open in an external browser or a new in-app modal browser |
Step 4: Handle onPaymentRequest
Add a handler for onPaymentRequest that shows the user a payment confirmation modal using the information in the paymentRequest. If an extraId field is present on the paymentRequest it must be sent with the transaction (ex. as a memo)
This is what a paymentRequest looks like
{
amount: '0.0187',
symbol: 'ETH', // This is the payment currency your user chose
protocol: 'ethereum', // This is Bidali's unique identifier for a currency
address: '0x220b48eda85a0a067c769f0442dd2f4e03ba625a',
chargeId: 'vIQ807gvR',
extraId: 'vIQ807gvR',
extraIdName: 'Memo text',
description: '$50 Amazon US',
lineItems: [
{
name: 'Amazon US',
quantity: 1,
amount: '$50 USD',
image: 'https://assets.bidali.com/commerce/brands/amazon-com-us.png'
}
]
}A paymentRequest has the following properties:
| Property | Type | Description |
|---|---|---|
address | String | The address to send to |
amount | String | The amount to send, as a user readable string (ex. "25") |
symbol | String | The symbol of the currency the user has chosen to pay with, ex: USDC. Not unique to a currency: USDC on Ethereum and USDC on Stellar both have the symbol USDC |
protocol | String | The protocol of the currency the user has chosen to pay with, this is unique for each currency. ex: usdcstellar |
description | String | A comma-delimited list of products being purchased. Can be used on your payment approval screen so the user sees what they are approving payment for. Ex: $50 USD Amazon US, 2x $25 USD Apple US |
lineItems | Array | A list of products being purchased. Just like with description, you can use this on your payment approval screen. A line item looks like { name: 'Apple US', quantity: 1, amount: '$5 USD', image: 'https://assets.bidali.com/commerce/brands/apple.png' } |
chargeId | String | The users order reference, helpful to reference the order in the future or if there are any issues. Ex: vIQ807gvR |
extraId | String | The extraId that must be passed for the payment to be credited appropriately to the order. See memo / note requirements by network. |
extraIdName | String | The name of the extraId that must be sent with the transaction. See memo / note requirements by network. |
Networks that need a memo
On these networks, the payment request includes an extra field that MUST be sent with the transaction for the payment to be credited.
| Network | extraIdName |
|---|---|
| Stellar | Memo text |
| Solana | Memo |
| Algorand | Note/Memo |
| Cosmos | Memo |
| Osmosis | Memo |
Step 5: Notify Bidali of payment status
When the user sends the transaction call window.bidaliProvider.paymentSent() inside the WebView. If they cancel payment / dismiss the modal then call window.bidaliProvider.paymentCancelled()
await controller.runJavaScript('window.bidaliProvider.paymentSent();');
// or
await controller.runJavaScript('window.bidaliProvider.paymentCancelled();');Step 6: Update balances
After a user sends a transaction or their balances change, update the balances object on window.bidaliProvider by encoding it as JSON and running JavaScript in the WebView
await controller.runJavaScript(
'window.bidaliProvider.balances = ${jsonEncode(balances)};',
);Example code
Hereβs some sample code for Flutter that loads the Commerce URL in a webview_flutter WebView, injects javascript that sets window.bidaliProvider, and handles onPaymentRequest via a JavaScriptChannel.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class BidaliScreen extends StatefulWidget {
const BidaliScreen({super.key});
@override
State<BidaliScreen> createState() => _BidaliScreenState();
}
class _BidaliScreenState extends State<BidaliScreen> {
late final WebViewController _controller;
Map<String, String> balances = {
'stellar': '5.2',
'usdcstellar': '2000',
};
final user = {
'email': 'test@test.com',
'phoneNumber': '15554321234',
};
static const commerceUrl =
'https://commerce.staging.bidali.com/dapp?key=YOUR_API_KEY';
String get initialJavascript {
final email = user['email'];
final phoneNumber = user['phoneNumber'];
return '''
window.bidaliProvider = {
name: 'WALLET NAME', // This will be provided by Bidali with your apiKey
paymentCurrencies: ['stellar', 'usdcstellar'],
brandColor: '#35D07F',
email: ${email != null ? "'$email'" : 'null'},
phoneNumber: ${phoneNumber != null ? "'$phoneNumber'" : 'null'},
balances: ${jsonEncode(balances)},
onPaymentRequest: (paymentRequest) => {
var payload = { method: 'onPaymentRequest', data: paymentRequest };
BidaliChannel.postMessage(JSON.stringify(payload));
},
openUrl: function (url) {
var payload = { method: 'openUrl', data: { url } };
BidaliChannel.postMessage(JSON.stringify(payload));
}
};
''';
}
Future<void> _onMessage(JavaScriptMessage message) async {
final payload = jsonDecode(message.message) as Map<String, dynamic>;
final method = payload['method'] as String?;
final data = payload['data'] as Map<String, dynamic>?;
switch (method) {
case 'onPaymentRequest':
final amount = data?['amount'];
final address = data?['address'];
final symbol = data?['symbol'];
final protocol = data?['protocol'];
final description = data?['description'];
final chargeId = data?['chargeId'];
final memo = data?['extraId'];
// These 2 callbacks need to be called to notify Bidali of the status of the payment request
// so it can update the WebView accordingly.
Future<void> onPaymentSent() async {
await _controller.runJavaScript('window.bidaliProvider.paymentSent();');
}
Future<void> onPaymentCancelled() async {
await _controller
.runJavaScript('window.bidaliProvider.paymentCancelled();');
}
if (protocol == 'stellar') {
// Show a native transaction confirmation modal to the user
final result = await sendXLM(
amount: amount,
address: address,
symbol: symbol,
protocol: protocol,
memo: memo,
);
if (result.status == 'sent') {
await onPaymentSent();
} else if (result.status == 'cancelled') {
await onPaymentCancelled();
}
}
break;
case 'openUrl':
final url = data?['url'];
// Open the URL in an in-app browser or external browser
break;
}
}
Future<void> _updateBalances(Map<String, String> nextBalances) async {
setState(() {
balances = nextBalances;
});
await _controller.runJavaScript(
'window.bidaliProvider.balances = ${jsonEncode(nextBalances)};',
);
}
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
'BidaliChannel',
onMessageReceived: _onMessage,
)
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (url) {
_controller.runJavaScript(initialJavascript);
},
),
)
..loadRequest(Uri.parse(commerceUrl));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: WebViewWidget(controller: _controller),
),
);
}
}Please send any feedback to support@bidali.com