XPay Element for Flutter is an embedded payment system that allows you to collect payments directly from users within your Flutter applications.
It is highly customizable and enables you to align the payment UI seamlessly with your app’s theme and user experience.
Features
Seamless Payment Integration
Integrate payments directly inside your Flutter app without redirecting users to external pages or apps.
Custom Styling
Fully customize the payment UI including:
- Labels
- Placeholders
- Input styles
- Error states
- Focus states
Embedded Authentication
Handle OTP and authentication flows directly within the app for a smooth checkout experience.
Event Handling
Built-in real-time events:
onReadyonBinDiscount
Multi-Payment Support
Supports multiple payment methods:
- Card
- JazzCash
- EasyPaisa
- Google Pay
- Raast RTP
- Raast DQR
- Baadmay (BNPL)
Getting Started
To incorporate the XPay embedded payment system into your Flutter application, start by adding the following dependency to your pubspec.yaml file:
E2E Integration Sample Repo
XStakCommerce/XPay-Element-Flutter-Demo
GitHub repository
https://github.com/XStakCommerce/XPay-Element-Flutter-Demo
// for production
dependencies:
xpay_element_flutter: 7.0.1
// for stagging
dependencies:
xpay_element_flutter_stage: 7.0.3Note (staging ≥v7.0.0 / production ≥v7.0.0): The
hmacKeyparameter has been removed fromXPayElementControllerand is no longer required. If you are upgrading from an earlier version, simply remove thehmacKeyargument from controller initialization.
Platform Configuration (Required for Raast DQR)
iOS Setup
Path: ios/Runner/Info.plist
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need access to save QR codes to your photo library</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to save QR codes to your photo library</string>⚠ Important Notes
- Missing permissions will crash the app when saving QR codes
- If denied, SDK will show a permission error message
Android Setup
Path: android/app/src/main/AndroidManifest.xml
<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"android:maxSdkVersion="28"/>Android 10+ does not require this permission (uses MediaStore)
Usage
To use the XPay SDK in your app, follow these steps:
Import the Package
// for production
import 'package:xpay_element_flutter/xpay_element_flutter.dart';
// for stagging
import 'package:xpay_element_flutter_stage/xpay_element_flutter_stage.dart';Initialize the Element Controller
Create an instance of XPayElementController with necessary credentials available on your XPay dashboard:
enableBackButtonOnIframe (bool, optional — defaults to false)
Pass true to allow the device back button to close the authentication screen that appears after payment submission. Disabled by default to prevent users from accidentally exiting mid-payment. Recommended to keep disabled.
final XPayElementController controller = XPayElementController(
publicKey: "your_account_public_key",
hmacKey: "your_account_hmac_key",
accountId: "your_account_id",
enableBackButtonOnIframe: false, // allow back button on auth screen
);Payment Methods Integration in Your App
1. Dynamic Payment Methods
Render all available payment methods in a single unified UI.
The SDK automatically displays only the payment methods enabled for your merchant account.
To use this approach, simply embed the XPayElementWidget and set showAllPaymentMethods: true. The SDK will automatically handle and render all available payment methods.
You want a single checkout screen with automatic rendering of all available payment methods, faster integration, minimal setup, and SDK-managed payment routing.
Embed the SDK Payment Widget:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title:const Text('XPay Payment Example')),
body: XPayElementWidget(controller: controller, showAllPaymentMethods: true),
);
}2. Individual Payment Method Integration
Use this approach when you want full control over each payment method’s UI and flow.
With this method, each payment widget must be initialized with its own XPayElementController instance, allowing independent handling of different payment methods
Embed the Card Payment Widget
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title:const Text('XPay Payment Example')),
body: XPayElementWidget(controller: controller),
);
}
Embed the JazzCash Payment Widget
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title:const Text('XPay Payment Example')),
body: XPayJazzCashElementWidget(controller: controller),
);
}
Embed the EasyPaisa Payment Widget
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title:const Text('XPay Payment Example')),
body: XPayEasyPaisaElementWidget(controller: controller),
);
}
Embed the Google Pay Payment Widget
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title:const Text('XPay Payment Example')),
body: XPayGooglePayElementWidget(controller: controller),
);
}
Embed the Raast RTP Payment Widget
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title:const Text('XPay Payment Example')),
body: XPayRaastRTPElementWidget(controller: controller),
);
}
Embed the Raast DQR Payment Widget
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title:const Text('XPay Payment Example')),
body: XPayRaastDQRElementWidget(controller: controller),
);
}
Embed the Baadmay Payment Widget
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title:const Text('XPay Baadmay Payment Example')),
body: XPayBaadmayElementWidget(controller: controller),
);
}
Cardholder Name Field (Card Payments)
You can enable the cardholder name input field in XPayElementWidget by setting showNameField: true.
This works for both:
- Individual Card integration
- Dynamic mode (
showAllPaymentMethods: true)
Since the same widget (XPayElementWidget) is used in both cases, the behavior remains consistent.
When enabled:
- The name field is displayed along with card inputs
- The field is required for form validation
- The entered value is automatically used during
confirmPayment - No need to pass
customerNamein theconfirmPaymentcall
Custom Styling
Customize the SDK's appearance to seamlessly integrate with your app's style and theme. The XPay SDK allows you to modify elements' style and appearance, making it adaptable to various design requirements.
Initializing and Applying Custom Styles
Initialize XPayElementCustomStyle and pass custom style configurations to its constructor. Below is an example of how you can tailor the appearance of your payment elements:
These are payment element widget-specific input configurations used to customize labels and placeholders for different payment methods. You can define them in a single inputConfiguration object or separately for each payment method as needed.
For Credit/Debit Card (XPayElementWidget), pass the following inputConfiguration:
"inputConfiguration": {
// Style for Cardholder Name field (used when SDK renders name input field)
"cardholderName": {
"label": "Name on Card",
"placeholder": "Enter cardholder name"
},
"cardNumber": {
"label": "Card Number",
"placeholder": "Enter card number"
},
"expiry": {"label": "Expiry Date", "placeholder": "MM/YY"},
"cvc": {"label": "CVC", "placeholder": "Enter cvc"}
}
For JazzCash Wallet (XPayJazzCashElementWidget), pass the following inputConfiguration:
"inputConfiguration": {
"phoneNumber": {
"label": "Phone Number",
"placeholder": "0312-3456789"
},
"cnicNumber": {"label": "CNIC Number", "placeholder": "Enter cnic number"}
},
For EasyPaisa Wallet (XPayEasyPaisaElementWidget), pass the following inputConfiguration:
"inputConfiguration": {
"phoneNumber": {
"label": "Phone Number",
"placeholder": "0312-3456789"
},
},
For Raast RTP (XPayRaastRTPElementWidget), pass the following inputConfiguration:
"inputConfiguration": {
"accountNumber": {
"label": "Account Number",
"placeholder": "Enter your account number"
},
"raastId": {
"label": "Raast ID",
"placeholder": "Enter your raast id"
},
"bankSelector": {
"label": "Select Bank",
"placeholder": "Choose your bank"
},
"ibanAccount": {
"label": "Account Number / IBAN",
"placeholder": "Enter account number or iban"
}
},
For Raast DQR (XPayRaastDQRElementWidget), pass the following inputConfiguration:
"inputConfiguration": {
"bankSelector": {
"label": "Select Bank",
"placeholder": "Choose your bank"
},
}
You can use a single constructor that includes inputConfiguration along with other style properties to manage input configurations for all payment elements and pass them to the widget. Alternatively, you can define separate constructors for each payment method. Below is a sample example showing how to configure and apply a custom style.
@override
XPayElementCustomStyle elementCustomStyle =
XPayElementCustomStyle.configureCustomStyle({
// Payment element specific input configurations.
// You can define all fields in a single inputConfiguration object
// or split them per payment method as needed.
"inputConfiguration": {
// Style for Cardholder Name field (used when SDK renders name input field)
"cardholderName": {
"label": "Name on Card",
"placeholder": "Enter cardholder name"
},
"cardNumber": {
"label": "Card Number",
"placeholder": "Enter card number"
},
"expiry": {
"label": "Expiry Date",
"placeholder": "MM/YY"
},
"cvc": {
"label": "CVC",
"placeholder": "Enter cvc"
},
"phoneNumber": {
"label": "Phone Number",
"placeholder": "Enter phone number"
},
"cnicNumber": {
"label": "CNIC Number",
"placeholder": "Enter last 6 digits of cnic"
},
"accountNumber": {
"label": "Account Number",
"placeholder": "Enter your account number"
},
"raastId": {
"label": "Raast ID",
"placeholder": "Enter your raast id"
},
"bankSelector": {
"label": "Select Bank",
"placeholder": "Choose your bank"
},
"ibanAccount": {
"label": "Account Number / IBAN",
"placeholder": "Enter account number or iban"
}
},
"inputStyle": {
"height": 40,
"textColor": 0xff000000,
"textSize": 16,
"fontWeight": 400,
"borderColor": 0xffe6e6e6,
"borderRadius": 5,
"borderWidth": 1
},
"inputLabelStyle": {
"textSize": 16,
"fontWeight": 400,
"textColor": 0xff000000
},
"onFocusInputStyle": {
"textColor": 0xff000000,
"textSize": 16,
"fontWeight": 400,
"borderColor": 0xffc8dbf9,
"borderWidth": 1
},
"invalidStyle": {
"borderColor": 0xffff0000,
"borderWidth": 1,
"textColor": 0xffff0000,
"textSize": 14,
"fontWeight": 400
},
"errorMessageStyle": {
"textColor": 0xffff0000,
"textSize": 14,
"fontWeight": 400
}
});Applying the Custom Styles
After configuring your styles, apply them to the XPayElementWidget to enhance the user interface.
1. Dynamic Payment Methods
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Custom Styled XPay Element')),
body: XPayElementWidget(style: elementCustomStyle, controller: controller, showAllPaymentMethods: true)
);
}
2. Individual Payment Method Integration
For Credit/Debit Card (XPayElementWidget)
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Custom Styled XPay Element')),
body: XPayElementWidget(style: elementCustomStyle, controller: controller)
);
}
For JazzCash Wallet (XPayJazzCashElementWidget)
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Custom Styled XPay Element')),
body: XPayJazzCashElementWidget(style: elementCustomStyle, controller: controller)
);
}
For EasyPaisa Wallet (XPayEasyPaisaElementWidget)
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Custom Styled XPay Element')),
body: XPayEasyPaisaElementWidget(style: elementCustomStyle, controller: controller)
);
}
For Raast RTP (XPayRaastRTPElementWidget)
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Custom Styled XPay Element')),
body: XPayRaastRTPElementWidget(style: elementCustomStyle, controller: controller)
);
}
For Raast DQR (XPayRaastDQRElementWidget)
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Custom Styled XPay Element')),
body: XPayRaastDQRElementWidget(style: elementCustomStyle, controller: controller)
);
}
The above styling properties are all optional; you can define only those you require, ensuring flexibility and customization according to your specific design needs.
Element Events
Handle element-specific events to enhance the user experience:
onReady Event
The onReady event is triggered whenever the payment form validity changes. It indicates whether the current payment form is ready for submission.
When all required fields are valid, it returns true, otherwise false.
You can use this event to enable or disable your payment button in real time.
Supported Across All Payment Methods
The onReady event is available for all XPay payment widgets:
- Card (
XPayElementWidget) - JazzCash (
XPayJazzCashElementWidget) - EasyPaisa (
XPayEasyPaisaElementWidget) - Google Pay (
XPayGooglePayElementWidget) - Raast RTP (
XPayRaastRTPElementWidget) - Raast DQR (
XPayRaastDQRElementWidget) - Baadmay (BNPL) (
XPayBaadmayElementWidget) - Dynamic Mode (XPayElementWidget) with
showAllPaymentMethodsenabled
Example Usage
For Credit/Debit Card (XPayElementWidget)
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Custom Styled XPay Element')),
body: XPayElementWidget(
controller: controller,
onReady: (bool isReady) {
// Update your state to enable payment submission
},
)
);
}
onBinDiscount
The onBinDiscount event is triggered when the SDK receives BIN data (first 6 or 8 digits of the card) during card input.
This event helps merchants apply BIN-based discounts or handle card-level configurations dynamically.
Event Behavior
- The event is triggered as soon as BIN data is detected.
- It is triggered regardless of whether a matching BIN configuration exists or not.
- If the card field is cleared after a BIN event was fired, the event is triggered again to support reset handling.
- If a matching merchant BIN configuration exists → The SDK returns configured BIN details with
binDiscount: true(if enabled) orfalse(if disabled) - If no merchant BIN configuration exists → The SDK returns generic BIN details with
binDiscount: false
Just for Credit/Debit Card (XPayElementWidget)
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Custom Styled XPay Element')),
body: XPayElementWidget(
controller: controller,
onBinDiscount: (dynamic binDiscountData) {
// Process and apply BIN-based discounts
},
)
);
}
BIN Discount Calculation (Optional)
To receive discount calculation data alongside standard BIN details in the event, pass apply_bin_discount: true when creating the payment intent from your backend. Take the pi_client_secret from the intent response and provide it to the SDK — either at construction or after creation.
The SDK will automatically include it in the BIN config call, and the event payload will contain the full discount calculations data.
Pass pi_client_secret at construction:
final XPayElementController controller = XPayElementController(
publicKey: "your_public_key",
hmacKey: "your_hmac_key",
accountId: "your_account_id",
clientSecret: "pi_client_secret_from_intent_api",
);Or set it after creation (async flow):
controller.setClientSecret("pi_client_secret_from_intent_api");Note: This is optional. Without
pi_client_secret, the event still fires with standard BIN details and thebinDiscountflag — discount calculation data will not be included.
Confirming Payment
To proceed with the payment confirmation when all form fields are valid and the onReady event has returned true, you should perform a few necessary steps. First, ensure you have initiated a server-side API call to create a payment intent. This create intent API is responsible for generating the clientSecret and encryptionKey, which are critical for securing the payment confirmation.
Server-Side Payment Intent Creation
Before invoking the payment confirmation on the client side, your backend should call the create intent API to obtain:
clientSecret: A secret key used to initiate the payment process securely. encryptionKey: Used to encrypt sensitive information before it is transmitted.
Confirming the Payment
Once you have the necessary keys from your backend, use the XPayElementController initialized earlier to call the confirmPayment method. Here’s how you can implement this in your Flutter app:
void confirmPayment() async {
try {
// Assuming 'controller' is your instance of XPayElementController
var paymentResponse = await controller.confirmPayment(
customerName: "Customer Name",
clientSecret: "client_secret_from_intent_api",
encryptionKeys: "encryption_keys_from_intent_api"
);
if (paymentResponse.error) {
// Handle payment failure
print("Payment failed: ${paymentResponse.message}");
} else {
// Handle payment success
print("Payment successful: ${paymentResponse.message}");
}
} catch (e) {
// Handle exceptions
print("Payment Error: $e");
}
}
Note for Confirm Payment
If you have integrated payment methods individually in your app, call the confirmPayment method using the relevant controller based on the customer’s selected payment method.
If you are using dynamic mode (showAllPaymentMethods: true), simply call confirmPayment using the single XPayElementWidget controller, as the SDK automatically handles and routes the payment based on the selected payment method.
Note:
customerNameis optional for all payment methods except Card.- For Card payments,
customerNameis required ifshowNameFieldis not enabled. - If
showNameField: trueis enabled, you do not need to passcustomerNameinconfirmPayment.
Confirm Payment Response
The response from confirmPayment contains the following fields:
error: A boolean indicating whether the payment failed. Iftrue, the payment was unsuccessful.message: A string containing the server response message with details about the payment result or error.status: A string representing the final payment status
Clean Method
The clear method is used to reset the payment form. This is especially useful if you have a button designed to clear the form or reset the checkout process.
// Assuming 'controller' is your instance of XPayElementController
controller.clear();Get Form Ready Status Using Controller
The isReady method is used to check the form field status. If all fields are filled with valid data, it returns true, allowing the payment button to be enabled. Otherwise, it returns false.
// Assuming 'controller' is your instance of XPayElementController
controller.isReady();