BidaliDocs

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 staging and production environments.

The Commerce URLs to use in the webview will be:

EnvironmentURL
Staging / Testnethttps://commerce.staging.bidali.com/dapp?key=YOUR_API_KEY
Production / Mainnethttps://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.

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:

PropertyTypeDescription
nameStringThe name of the wallet integrating (Provided by Bidali)
keyStringThis is your Bidali Commerce Publishable Key. The unique and public key that identifies your organization. It looks like pk_viqah4squsnuw6p0e1ue.(Provided by Bidali)
paymentCurrenciesString[]Allows you to only show certain cryptocurrencies as payment options. It takes an Array of Strings that are the coin protocols (Bidali's unique identifier)
brandColorStringHex color used to theme CTA button backgrounds, ex: #35D07F
limitToCountriesString[]Country codes of the countries to limit the products to, ex: [ "US", "CA" ]
emailStringAllows us to pre-populate their email, user has to confirm during checkout flow
phoneNumberStringAllows us to pre-populate the phone number for mobile topups, the user has the chance to change this
balancesobjectThe balances for the allowed currencies, ex. { stellar: '25.2', usdcstellar: '2000' }
onPaymentRequestFunctionProvide a handler for this function that displays a payment confirmation modal for the user
openUrlFunctionOpens 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:

PropertyTypeDescription
addressStringThe address to send to
amountStringThe amount to send, as a user readable string (ex. "25")
symbolStringThe symbol of the currency the user has chosen to pay with, ex: BTC, not unique to a currency (for instance USDC ERC20 and USDC on Stellar both have the symbol USDC)
protocolStringThe protocol of the currency the user has chosen to pay with, this is unique for each currency. ex: usdcstellar
descriptionStringA 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
lineItemsArrayA 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' }
chargeIdStringThe users order reference, helpful to reference the order in the future or if there are any issues. Ex: vIQ807gvR
extraIdStringThe extraId that must be passed for the payment to be credited appropriately to the order. See memo / note requirements by network.
extraIdNameStringThe name of the extraId that must be sent with the transaction. See memo / note requirements by network.

Note for currencies on Stellar, Algorand, Solana, Cosmos, and Osmosis networks

Below are the additional fields that will be populated on the BidaliPaymentRequest and MUST be passed with the transaction for it to be successful.

NetworkextraIdName
StellarMemo text
SolanaMemo
AlgorandNote/Memo
CosmosMemo
OsmosisMemo

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

On this page