Skip to content

GlomoPay Flutter SDK v2

Official Flutter SDK for integrating GlomoPay payment checkout flows into your mobile applications.

Current version: v2.1.0. Upgrading from v2.0.0 is a version bump and nothing else - no API changed. See Document Downloads for what it added.

Upgrading from v1.11.x? See the Migration Guide. Every integration needs at least one change, and the compiler points at it.

Full Changelog is also available.

Prerequisites

Before using this SDK, you need:

  • API credentials (Public Key) from your GlomoPay dashboard
  • An order ID created via the GlomoPay API (or a subscription ID for subscription payments)

System Requirements

RequirementVersion
Flutter>= 3.7.0
Dart SDK>= 3.0.0

Installation

Add the dependency to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  glomopay_sdk: ^2.1.0

Or install via the CLI:

flutter pub add glomopay_sdk

Then import the package:

import 'package:glomopay_sdk/glomopay_sdk.dart';

Platform Setup

Android

Add the following permissions to android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />

iOS

Add the following keys to ios/Runner/Info.plist:

<key>NSCameraUsageDescription</key>
<string>This app requires access to the camera to upload documents required for payment verification.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app requires access to the photo library to select documents for payment verification.</string>

Document downloads (below) need no additional permission or Info.plist key on either platform - the SDK writes only to the destination the user picks in the system save dialog.

Quick Start

import 'package:flutter/material.dart';
import 'package:glomopay_sdk/glomopay_sdk.dart';

class PaymentScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Complete Payment')),
      body: GlomoPayCheckout(
        config: const GlomoPayConfig(
          publicKey: 'test_pk_12345',
          orderId: 'order_abc123',
        ),
        onPaymentSuccess: (GlomoPayPayload payload) {
          // A payment the backend confirms succeeded. Verify
          // payload.signature on your server before fulfilling.
          print('Payment succeeded! Payment ID: ${payload.paymentId}');
        },
        onPaymentFailure: (GlomoPayPayload payload) {
          // A payment the backend confirms failed - prompt for another card
          // or bank. A checkout that could not load arrives on onSdkError or
          // onConnectionError instead, not here.
          print('Payment failed with Order ID: ${payload.orderId}');
        },
        onUserJourneyCompleted: (GlomoPayUserJourneyPayload journey) {
          // The second happy path: the user submitted bank transfer details.
          // A settlement may not have happened
          // and there is no paymentId or signature, so
          // reconcile journey.orderId on your backend rather than treating
          // this as paid. Settlement is confirmed via webhooks.
          print('Journey completed: ${journey.journeyType.value} '
              'for order ${journey.orderId}');
        },
        onSdkError: (List<SdkError> errors) {
          print('SDK Error: ${errors.first.message}');
        },
        onConnectionError: (ConnectionError error) {
          print('Connection error: ${error.message}');
        },
        onPaymentTerminate: (TerminationSource source) {
          // Fires when the checkout ends without a payment result: the user
          // dismissed it, hit back, exited one of the SDK's error screens, or
          // a connection error closed it. The SDK closes itself, so this is
          // where your own cleanup goes - not the closing.
          print('Checkout ended without a payment: $source');
        },
      ),
    );
  }
}

Subscriptions Checkout

To process subscription payments, pass a subscriptionId instead of an orderId in GlomoPayConfig:

GlomoPayCheckout(
  config: const GlomoPayConfig(
    publicKey: 'live_pk_abc123',
    subscriptionId: 'sub_xyz789',
  ),
  onPaymentSuccess: (GlomoPayPayload payload) {
    print('Subscription payment success: ${payload.paymentId}');
  },
  onPaymentFailure: (GlomoPayPayload payload) {
    print('Subscription payment failed: ${payload.orderId}');
  },
  onUserJourneyCompleted: (GlomoPayUserJourneyPayload journey) {
    // Never fires for subscriptions today, but the callback is required.
  },
  onSdkError: (List<SdkError> errors) {
    errors.forEach((e) => print('${e.type}: ${e.message}'));
  },
  onConnectionError: (ConnectionError error) {
    print('Connection error: ${error.message}');
  },
)

When subscriptionId is provided:

  • The SDK skips order detection API calls
  • The subscriptionId must start with sub_
  • LRS-specific UI elements and flow logic are not applied
  • Do not pass both orderId and subscriptionId - the SDK will fire onSdkError
  • Existing order-based integrations remain backward compatible

Asynchronous Payment Journeys

Some checkout flows end without a confirmed payment. The user has done everything asked of them, but a settlement may not have happened yet payment confirmation happens async. These arrive through onUserJourneyCompleted.

Today there is exactly one such journey: bankTransferSubmitted.

onUserJourneyCompleted: (GlomoPayUserJourneyPayload journey) {
  switch (journey.journeyType) {
    case GlomoPayUserJourneyType.bankTransferSubmitted:
      // The user submitted their transfer details. Mark the order as
      // awaiting funds and reconcile it on your backend; settlement is
      // confirmed by webhook, not by this callback.
      print('Transfer reference: ${journey.transactionReference}');
      break;
  }
},

Before v2.0.0, a submitted bank transfer was delivered to onPaymentSuccess with paymentId and signature both null - a payment reported as received that your backend had nothing to verify against. It now reports here instead, and no longer reaches onPaymentSuccess.

One journey produces one callback. A submitted bank transfer is an ending in its own right: it reports through onUserJourneyCompleted and never also through onPaymentSuccess, and a user who closes what remains of the checkout afterwards does not additionally arrive as onPaymentTerminate.

Features

  • Glomo Payment Stack Support - handles standard checkout flows and overlay redirects (3DS, bank pages)
  • Robust Security - built-in jailbreak and root detection
  • Self-contained error reporting - diagnostics run over the SDK's own HTTP client. No global Flutter or platform error handlers are installed and no native crash handler is touched, so your app's own crash reporting is unaffected and your error-tracking dependencies are unconstrained.
  • Comprehensive Error Handling - gracefully manages connection drops, DNS issues, HTTP errors, validation mistakes
  • Native Support - full Android/iOS native features (camera, file pickers)
  • Document downloads - bank flows that offer a form or agreement download hand it to the system save dialog, so the user gets the file on their device. New in v2.1.0
  • Mock Mode - easy testing with test_ and mock_ prefixed keys

API Reference

GlomoPayCheckout Widget

The checkout widget. Place it in your widget tree to render the payment checkout.

GlomoPayCheckout(
  config: config,
  onPaymentSuccess: ...,
  onPaymentFailure: ...,
  onUserJourneyCompleted: ...,
  onSdkError: ...,
  onConnectionError: ...,
)

Props

PropTypeRequiredDescription
configGlomoPayConfigYesConfiguration details for the checkout session
onPaymentSuccessFunction(GlomoPayPayload)YesCalled when a payment the backend confirms succeeded
onPaymentFailureFunction(GlomoPayPayload)YesCalled when the backend confirms a payment failed. Reports failed payment attempts only - not checkouts that could not load or dropped midway
onUserJourneyCompletedFunction(GlomoPayUserJourneyPayload)YesCalled when an asynchronous journey ends the checkout without a confirmed payment - today only bankTransferSubmitted. The payload has no paymentId or signature, so reconcile the order. New and required in v2.0.0
onSdkErrorFunction(List<SdkError>)YesCalled when the SDK or the order could not proceed (validation, forbidden device, unusable order, unreachable backend)
onConnectionErrorFunction(ConnectionError)YesCalled on internet drop, DNS failure, HTTP error, or page load timeout
onPaymentTerminateFunction(TerminationSource)?NoCalled when the checkout ends without a payment result. A notification, not a handover - the SDK closes the checkout itself whether or not you supply this, and will not close twice if you also close from here
onUserRefusedDevicePermissionsFunction()?NoCalled when the user denies camera/storage permissions during file/document uploads or selfie verification requests
onEventFunction(String, Map)?NoDeprecated in v2.0.0. Carries internal debugging events that are subject to change and are not part of the integration contract. Scheduled for removal in a future major version
autoCloseOnConnectionErrorboolNoWhether a critical connection error auto-closes the checkout and reports onPaymentTerminate(connectionError). The SDK pops its own route on this path. Does not apply to ConnectionErrorType.timeout. Default: true
controllerGlomoPayController?NoSupply your own controller instead of letting the SDK create one

GlomoPayConfig

PropertyTypeDefaultDescription
publicKeyString-Your GlomoPay public key (must start with live_, mock_, or test_)
orderIdString?nullUnique tracking ID generated for the transaction on your server
subscriptionIdString?nullSubscription checkout ID; use instead of orderId for subscriptions
serverString?nullCustom LRS checkout URL. Leave unset to use the official environment URL

Exactly one of orderId or subscriptionId must be provided. If both or neither are set, onSdkError fires.

GlomoPayPayload

Delivered by onPaymentSuccess and onPaymentFailure.

class GlomoPayPayload {
  final String orderId;                     // System ID of the order
  final String? paymentId;                  // Transaction reference, if generated
  final String? signature;                  // Validation signature hash for backend verification
  final Map<String, dynamic>? rawResponse;  // The page's message exactly as it arrived
}

Verify signature on your backend before fulfilling. rawResponse carries the checkout page's own detail untouched, including the thinner shape the page emits for its own setup failures.

GlomoPayUserJourneyPayload

Delivered by onUserJourneyCompleted. New in v2.0.0.

class GlomoPayUserJourneyPayload {
  final GlomoPayUserJourneyType journeyType;  // Which journey ended
  final String orderId;                       // The order it belongs to. Always present
  final String? senderAccountNumber;          // Reported by the page, when present
  final String? transactionReference;         // Reported by the page, when present
  final String? status;                       // The page's own status string, when present
  final Map<String, dynamic>? rawResponse;    // The page's message exactly as it arrived
}

There is deliberately no paymentId or signature. The user has submitted their transfer details and the money has not moved, so there is nothing to verify a payment against - reconcile the order on your backend instead.

GlomoPayUserJourneyType

ValueWire valueDescription
bankTransferSubmittedbank_transfer_submittedThe user submitted their bank transfer details. No payment is confirmed yet

This is the only member today. It is an enum rather than a bare string so you can switch on it exhaustively and so a second asynchronous flow can be added without changing the callback's shape.

SdkError

class SdkError {
  final SdkErrorType type;   // Error category
  final String message;      // Human-readable description
  final String? field;       // Field name (e.g. "orderId") causing validationError
}

SdkErrorType

ValueDescription
validationErrorInput validation failed
deviceForbiddenRooted or jailbroken device detected
networkErrorNetwork-level error
unknownUncategorized error

field names what produced the error - "orderId" for a malformed order id, and "file.save" for a document download that failed. A file.save validation error is non-terminal: the checkout continues.

ConnectionError

class ConnectionError {
  final ConnectionErrorType type;  // Error category
  final String message;            // Extracted error description or HTTP status phrase
  final int? errorCode;            // Internal WebKit/Android error code
  final int? statusCode;           // HTTP status code, if applicable
  final bool isRecoverable;        // Suggests if safe to offer a "Retry" button
}

ConnectionErrorType

ValueDescription
noInternetDevice is offline
dnsFailureDNS resolution failed
timeoutRequest timed out
sslErrorSSL/TLS certificate error
httpClientErrorHTTP 4xx response
httpServerErrorHTTP 5xx response
webResourceErrorWebView resource loading error
unknownUncategorized connection error

Use isRecoverable to decide whether to show a "Retry" button to the user.

TerminationSource

ValueDescription
userDismissUser dismissed the checkout, including by exiting an SDK error screen
backButtonUser pressed the hardware or software back button
programmaticThe checkout was closed programmatically
connectionErrorA connection error closed the checkout

Callback Routing

One failure produces one callback, and each failure reports its actual semantic.

What happenedCallback
The backend confirmed a payment succeededonPaymentSuccess
The backend confirmed a payment failedonPaymentFailure
The user submitted a bank transferonUserJourneyCompleted
The SDK or the order could not proceedonSdkError
Connectivity failed, or the page did not load in timeonConnectionError
The user left, including from an SDK error screenonPaymentTerminate(TerminationSource.userDismiss)
A document download failedonSdkError (validationError, field file.save)

Notes on the routing:

  • onPaymentFailure means a real decline. A checkout that could not load, a connection failure, or an internal fault all reach onSdkError or onConnectionError instead. A user who abandoned a checkout reaches onPaymentTerminate. In v1.11.x these were reported as payment failures.
  • A single load failure fires one callback. A main-frame load failure delivers onConnectionError alone. It used to deliver onSdkError as well.
  • onSdkError is worth surfacing non-blockingly. The SDK does not currently signal when a reported failure clears, and a transient one can be followed by the checkout rendering normally. Prefer something dismissible over a blocking screen.
  • A load timeout is advisory. ConnectionErrorType.timeout means the page may still be in flight. shouldAutoClose is false, autoCloseOnConnectionError does not apply, and if the page does render the SDK withdraws its own error screen and the checkout continues. Prefer not to tear the checkout down on this one - log it, or show something non-blocking.
  • Your callback always fires first, as soon as the condition is detected, independently of whether the SDK's default dialog is ever dismissed.
  • A failed document download is not an ending. It reaches onSdkError as a validationError on field: 'file.save', and the checkout carries on. See Document Downloads.

The SDK's default error screens

These are a safety net for the case where you do not present your own UX - the user is never left with nothing to press. Which screen appears depends on whether retrying is meaningful.

ConditionScreenControls
The checkout page failed to load (dropped connection, DNS, SSL)Connection error screenRetry (when isRecoverable) and Cancel. Retry reloads the page
The SDK could not reach GlomoPay to fetch your orderError dialogExit
The page did not render inside its load timeoutError dialogExit. Withdrawn automatically if the page renders after all
The SDK or the order could not proceed (onSdkError)Error dialogExit

The last three offer no Retry deliberately: a page that never rendered and a backend the SDK could not reach are both conditions where something is genuinely wrong, and failing visibly gets it reported rather than retried into silence. Exit closes the checkout and reports onPaymentTerminate with TerminationSource.userDismiss.

The SDK does not draw its own error screen when the checkout page reports a failure it handles itself - the page already renders one.

Checkout Status

CheckoutStatus represents the current state of the checkout flow. These are internal to the SDK and are not part of the integration contract - the callbacks above are. Listed for context when reading logs.

StatusDescription
readyReady to start
validatingInput keys and device securely checked
paymentInProgressUser typing card details or authorizing
paymentSuccessfulPayment clear
paymentFailedProcessing declined
paymentCancelledUser bounced
bankTransferSubmittedNew in v2.0.0. User submitted bank transfer details; the checkout ended but no payment is confirmed. Previously reported as paymentSuccessful
errorNew in v2.0.0. The checkout could not proceed and no payment was attempted. Previously reported as paymentFailed

Both new members are appended, so the existing members keep the indices they shipped with. Anything persisting a raw status.index keeps reading what it wrote; only an exhaustive switch over CheckoutStatus needs new arms.

Platform-Specific Behavior

  • iOS: the SDK intercepts downward swipes to dismiss the payment sheet and triggers onPaymentTerminate with TerminationSource.userDismiss.
  • Android: the SDK overrides the standard pop and checks whether the Flow WebView overlay can go back first. The back button is no longer locked while a payment is in progress. Depending on the flow, leaving from inside a bank page may still take two presses, so an accidental press cannot end a payment.

File Uploads

Bank flows that ask for documents open the device picker. As of v2.0.0 the SDK does not filter the picker by file type - whatever the bank's page asks for can be selected, including formats the picker previously greyed out. The bank's page is the authority on what it accepts.

If the user denies camera or storage permission, onUserRefusedDevicePermissions fires.

Document Downloads

New in v2.1.0. Some bank flows offer a form or a signed agreement to download - the Kotak LRS agreements step is the first. Inside a WebView the page cannot put a file on the device by itself - its browser download path is an anchor[download] on a blob URL, which a WebView drops. Before v2.1.0 the control silently did nothing: the tap produced no file and no error.

The SDK now handles those downloads. When the checkout page asks it to save a document:

  1. The SDK fetches the document over its own HTTPS client, with a 30-second timeout.
  2. It opens the platform save dialog with the filename pre-filled - ACTION_CREATE_DOCUMENT on Android, the document picker in export mode on iOS. The user chooses the destination, so where the file lands is their choice, not the SDK's.
  3. The bytes are written to what they picked.

There is nothing to integrate. No callback, no configuration, no permission and no new dependency - file_picker, already in the graph for uploads, provides the dialog on both platforms. The checkout page only offers the download to the SDK once the SDK advertises it, so an older SDK build keeps whatever behaviour it had rather than swallowing the user's tap.

When a download fails

A failed download is reported three ways and never ends the checkout - the payment session is untouched and the user can carry on:

  • The SDK shows a dismissible dialog naming what went wrong, with a single OK. It dims the checkout rather than covering it.
  • onSdkError fires with SdkErrorType.validationError and field set to 'file.save'. Filter on that field to tell a failed download apart from any other validation error.
  • The failure is recorded in the SDK's own diagnostics.
onSdkError: (List<SdkError> errors) {
  for (final error in errors) {
    if (error.field == 'file.save') {
      // A document download failed. The checkout is still alive and the
      // user has already been told - log it, don't tear anything down.
      continue;
    }
    // Everything else.
  }
},

A user who opens the save dialog and cancels it is not a failure: no dialog, no onSdkError, nothing to handle.

What can fail, and what the user is told:

ConditionUser sees
The document could not be fetched - timeout, transport, non-200"Couldn't download the document. Please try again."
The document URL is not https"Couldn't download the document. Please try again."
The document is larger than 100 MB"Downloads above 100 MB are not supported."
The platform refused the save dialog, or the write failed"Couldn't save the document to your device. Please try again."

The 100 MB ceiling is a memory bound, not a policy about document sizes: the save dialog takes the bytes rather than a path, so the whole document is held in memory to hand over, and something larger would take the host app down with it. Oversized responses are refused mid-download and never buffered whole.

The SDK's dialog is suppressed while a connection error screen or a bank page overlay is on screen, so nothing ever paints over the user's only recovery control. onSdkError still fires in those cases.

Mock Mode

The SDK infers mock mode from the publicKey prefix:

PrefixModeEnvironment
live_LiveProduction
test_MockTest/sandbox
mock_MockTest/sandbox

Mock mode keys route to the sandbox backend. No real transactions are created.

  • Connection heuristics allow mock traffic
  • Simulate transactions without real money movement

Troubleshooting

Build fails with "The named parameter 'onUserJourneyCompleted' is required"

Expected on upgrade from v1.11.x. Add the callback. If you do not take bank transfers, an empty body is a complete migration - it simply never fires. See the Migration Guide.

Build fails with "No named parameter with the name 'devMode'"

Also expected on upgrade. Remove the argument from GlomoPayConfig; there is no replacement.

Invalid Order ID format

Ensure orderId starts with order_ and has an appropriate length.

Invalid Subscription ID format

Ensure subscriptionId starts with sub_ and has an appropriate length.

Mutually exclusive identifiers

Provide either orderId or subscriptionId, not both. The SDK fires onSdkError if both are set.

SDK crashes immediately on open

Ensure config.publicKey is correctly set with an expected prefix (live_, mock_, or test_).

File upload buttons do nothing

Run flutter clean and rebuild. Ensure Camera and Storage permissions are declared in the platform config.

Download buttons do nothing

Check the SDK version first: document downloads need v2.1.0 or later. On an older build the checkout page falls back to its browser download path, which cannot write a file from inside a WebView, so the tap produces nothing. Upgrading is the whole fix - there is nothing to wire up.

If you are on v2.1.0 or later and a download still produces nothing, the checkout page may be serving that flow without a download control. Send the order id to the Glomo mobile team.

Downloads report "Couldn't download the document"

The SDK fetched the document and the fetch failed - an expired signed link, a timeout, or a non-200. The document URL must also be https; a plain-http link is refused without a request being made. The failure reaches onSdkError with field: 'file.save', and the message names which of these it was in the SDK's diagnostics.

Device Forbidden Error

Rooted or jailbroken devices trigger onSdkError with SdkErrorType.deviceForbidden on live checkouts. Test on an unmodified device or an emulator.

My app lost its error handlers after upgrading

v2.0.0 no longer installs FlutterError.onError, PlatformDispatcher.instance.onError, or native crash handlers inside your app. If your app was inheriting SDK-installed handlers, install your own. In exchange, the SDK no longer constrains versions for error-tracking packages.

Security

The SDK loads checkout pages in an isolated WebView. No PAN, CVV, or sensitive payment data passes through the Flutter bridge - all sensitive input is handled within the WebView's sandboxed context.

Built-in jailbreak and root detection prevents checkout on compromised devices.

SDK diagnostics are spooled to the application support directory and flushed on the next initialisation, so a checkout interrupted by process death is still reported. The SDK writes only its own diagnostics there, and no payment data.

Document downloads are fetched over https only (the sole exception is localhost, for the mock server). Download URLs are signed links, so they are treated as credentials: neither the URL nor the filename is ever written to diagnostics or analytics - failures are reported by outcome, file extension and byte count.

To report a security vulnerability, email security@glomopay.com. Do not open a public GitHub issue for security reports.

Dependency Notes

path_provider (^2.0.0) is a direct dependency as of v2.0.0. It was already resolved transitively, so no new package enters your dependency graph, but the constraint is now the SDK's own and may affect resolution in a pinned host app.

v2.1.0 adds no dependency. Document downloads use file_picker, which the SDK already depended on for uploads.

Exports

/**
 * All public exports from package:glomopay_sdk/glomopay_sdk.dart
 */

// Widget
GlomoPayCheckout

// Controller
GlomoPayController

// Configuration
GlomoPayConfig

// Payloads
GlomoPayPayload
GlomoPayUserJourneyPayload

// Errors
SdkError
SdkErrorType
ConnectionError
ConnectionErrorType

// Enums
GlomoPayUserJourneyType
TerminationSource
CheckoutStatus