Overview
Bank Pay allows customers to make payments via cash or cheque at any bank branch using a generated payment slip. This payment method is ideal for customers who prefer in-person banking or don’t have access to digital payment methods.
How Bank Pay Works
Customer Selects Bank Pay
Customer chooses the Bank Pay option during checkout.
Payment Slip Generated
The SDK generates a downloadable PDF payment slip with all transaction details.
Customer Visits Bank
Customer takes the payment slip to any supported bank branch.
Payment Made
Customer makes payment via cash or cheque at the bank teller.
Status Update
Once the bank confirms payment, the transaction status is updated automatically.
Payment Slip
The payment slip contains all necessary information for the bank teller:
- Business Name and logo
- Customer Name and contact details
- Transaction ID (unique reference)
- Amount to Pay (including any charges)
- Description of the payment
- Invoice Number (if applicable)
- Barcode/QR Code for quick processing
Implementation
Basic Bank Pay Flow
import 'package:hubtel_merchant_checkout_sdk/hubtel_merchant_checkout_sdk.dart';
// Configure checkout
final purchaseInfo = PurchaseInfo(
amount: 200.00,
description: 'Payment for invoice #INV-2024-001',
customerMsisdn: '0241234567',
);
final config = HubtelCheckoutConfiguration(
merchantApiKey: 'your_api_key',
merchantID: 'your_merchant_id',
callbackUrl: 'https://your-callback-url.com',
);
// Navigate to checkout screen
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CheckoutScreen(
purchaseInfo: purchaseInfo,
configuration: config,
),
),
);
// Bank Pay returns pending status
if (result is CheckoutCompletionStatus &&
result.status == UnifiedCheckoutPaymentStatus.pending) {
print('Payment slip generated. Transaction ID: ${result.transactionId}');
print('Customer should visit bank with payment slip');
}
Generating the PDF Receipt
The SDK includes a receipt screen that allows customers to download the payment slip:
import 'package:hubtel_merchant_checkout_sdk/src/ux/bank_pay/bank_pay_receipt_screen.dart';
import 'package:flutter_html_to_pdf/flutter_html_to_pdf.dart';
import 'package:open_filex/open_filex.dart';
class BankPayReceiptScreen extends StatefulWidget {
final MomoResponse mobileMoneyResponse; // Contains transaction details
final HtmlRequirements businessDetails; // Contains business info
BankPayReceiptScreen({
required this.mobileMoneyResponse,
required this.businessDetails,
});
}
// Generate PDF from HTML
Future<void> generatePdfFromHtml(String htmlContent) async {
Directory appDocDir = await getApplicationDocumentsDirectory();
final targetPath = appDocDir.path;
final targetFileName = "${businessDetails.businessName}_${mobileMoneyResponse.customerName}_${DateTime.now()}";
final generatedPdfFile = await FlutterHtmlToPdf.convertFromHtmlContent(
htmlContent,
targetPath,
targetFileName
);
// Open the generated PDF
await OpenFilex.open(generatedPdfFile.path);
}
Bank Pay Status Screen
After generating the payment slip, customers see a status screen with instructions:
import 'package:hubtel_merchant_checkout_sdk/src/ux/bank_pay/bank_pay_status_screen.dart';
class BankPayStatusScreen extends StatelessWidget {
const BankPayStatusScreen();
@override
Widget build(BuildContext context) {
return AppPage(
title: 'Confirm Process',
body: Center(
child: Column(
children: [
// Success icon
SvgPicture.asset(CheckoutDrawables.transactionSuccessful),
// Instructions
Text('Pay at any bank branch on the Ghana.Gov payment platform'),
// Status check info
Text(
'Once complete you may check the status of this pay-in-slip '
'in your payment history or dial *718*108#'
),
],
),
),
);
}
}
UI Components
The SDK provides a pre-built expansion tile for Bank Pay:
import 'package:hubtel_merchant_checkout_sdk/src/custom_components/bank_pay_tile.dart';
class BankPayExpansionTile extends StatefulWidget {
final ExpansionTileController controller;
final void Function(bool)? onExpansionChanged;
final bool isSelected;
BankPayExpansionTile({
required this.controller,
required this.onExpansionChanged,
required this.isSelected,
});
}
// Usage in checkout
BankPayExpansionTile(
controller: bankPayExpansionController,
onExpansionChanged: (expanded) {
if (expanded) {
// Handle Bank Pay selection
}
},
isSelected: selectedPaymentMethod == 'bankpay',
)
Payment Status Tracking
Bank Pay payments start with a pending status:
The payment remains pending until the bank confirms receipt of payment. This can take anywhere from a few minutes to a few hours depending on the bank.
Checking Payment Status
Customers can check their payment status using:
- Your Application: The checkout returns with
pending status after receipt download
- Payment History: Check within their payment history
- USSD Code: Dial
*718*108# for status updates
- Backend Verification: Use your callback URL to receive payment confirmations
The checkout returns with UnifiedCheckoutPaymentStatus.pending for Bank Pay after the receipt is downloaded:
// Handle Bank Pay completion
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CheckoutScreen(
purchaseInfo: purchaseInfo,
configuration: config,
),
),
);
if (result is CheckoutCompletionStatus) {
switch (result.status) {
case UnifiedCheckoutPaymentStatus.pending:
// Bank Pay receipt downloaded
// Payment pending bank confirmation
print('Payment slip downloaded: ${result.transactionId}');
// Check your backend for payment confirmation
break;
case UnifiedCheckoutPaymentStatus.userCancelledPayment:
// User closed checkout without downloading receipt
print('Payment cancelled');
break;
default:
break;
}
}
WebView Display
The payment slip is displayed in a WebView before PDF generation:
import 'package:webview_flutter/webview_flutter.dart';
late final WebViewController controller;
@override
void initState() {
super.initState();
controller = WebViewController()
..loadHtmlString(
HTMLStrings.generateString(businessDetails, mobileMoneyResponse)
);
}
// Display WebView
WebViewWidget(controller: controller)
HTML Template Requirements
The HtmlRequirements class specifies what business information is needed:
class HtmlRequirements {
final String businessName;
final String businessLogoUrl;
final String transactionId;
final double amount;
final String customerName;
final String customerMsisdn;
final String description;
// Additional fields as needed
}
Configuration
Enable Bank Pay
Bank Pay availability is determined by the channel configuration:
// The SDK checks available payment channels
final channels = await checkoutViewModel.fetchChannels();
// Bank Pay is available if included in channels list
if (channels.channels?.contains('bankpay') == true) {
// Show Bank Pay option
}
Best Practices
PDF Download: Ensure customers download the PDF before leaving the checkout screen. Without it, they cannot make the payment at the bank.
Expiry Time: Payment slips may have an expiry period. Communicate this clearly to customers.
User Experience Recommendations
- Clear Instructions: Show step-by-step instructions for using the payment slip
- Download Confirmation: Confirm the PDF was successfully downloaded
- Contact Information: Provide support contact in case of issues
- Status Notifications: Send updates when payment is confirmed
Implementation Tips
// 1. Show loading state while generating PDF
showLoadingDialog(context: context, text: "Downloading");
// 2. Handle PDF generation errors
try {
final pdfFile = await FlutterHtmlToPdf.convertFromHtmlContent(
htmlContent,
targetPath,
targetFileName
);
if (pdfFile.path.isNotEmpty) {
// Success - open the file
await OpenFilex.open(pdfFile.path);
}
} catch (e) {
// Show error dialog
showErrorDialog(
context: context,
message: 'Failed to generate payment slip. Please try again.',
);
}
// 3. Navigate to status screen after successful download
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const BankPayStatusScreen()
),
);
Transaction Response
Bank Pay uses the same MomoResponse structure as mobile money:
class MomoResponse {
String? transactionId; // Unique transaction reference
double? amount; // Payment amount
double? charges; // Any applicable charges
String? description; // Payment description
String? customerName; // Customer's name
String? customerMsisdn; // Customer's phone number
String? invoiceNumber; // Invoice reference
String? clientReference; // Your internal reference
}
Dependencies
Bank Pay requires these packages:
dependencies:
flutter_html_to_pdf: ^0.7.0 # PDF generation
open_filex: ^4.3.2 # Open generated PDF
path_provider: ^2.0.0 # File system access
webview_flutter: ^4.0.0 # Display payment slip
Payment Confirmation Flow
Customer Makes Payment
Customer presents payment slip at bank and makes payment.
Bank Processes Payment
Bank teller processes the payment using the reference number.
Payment Gateway Notified
Bank notifies Hubtel’s payment gateway of the successful payment.
Webhook Triggered
Your webhook endpoint receives a payment confirmation notification.
Status Updated
Transaction status changes from pending to paid.
Testing
In sandbox/test mode, you can simulate bank payment confirmations without actually visiting a bank.
// Test mode - simulate bank payment
final testResult = await checkoutViewModel.simulateBankPayment(
transactionId: 'test-transaction-id',
);
if (testResult.status == PaymentStatus.paid) {
print('Test bank payment successful');
}
Error Handling
// Handle Bank Pay errors
try {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CheckoutScreen(
purchaseInfo: purchaseInfo,
configuration: config,
),
),
);
if (result is CheckoutCompletionStatus &&
result.status == UnifiedCheckoutPaymentStatus.pending) {
// PDF receipt was generated
print('Payment slip downloaded');
} else if (result is CheckoutCompletionStatus &&
result.status == UnifiedCheckoutPaymentStatus.userCancelledPayment) {
// User cancelled without downloading
print('Payment cancelled');
}
} catch (e) {
// Handle errors
showErrorDialog(
context: context,
message: 'An error occurred. Please try again.',
);
}
}
Cancellation
Customers can cancel Bank Pay at the status screen:
// Cancel button on status screen
TextButton(
onPressed: () {
Navigator.pop(context); // Return to checkout
},
child: Text('Cancel'),
)
// On cancellation, transaction remains pending
// Customer can still use the payment slip if already downloaded