flutter_jazzcash 1.0.5
flutter_jazzcash: ^1.0.5 copied to clipboard
A Flutter package for JazzCash payment integration supporting both mobile wallet and card payments.
Flutter JazzCash Package - Complete User Guide #
Developer: Talha Javed | GitHub: @talhajaved
Screenshots #
Mobile Wallet Payment | Card Payment | Card Payment |
---|---|---|
![]() |
![]() |
![]() |
A comprehensive guide for integrating JazzCash payments in your Flutter app using both mobile wallet and card payment methods.
Table of Contents #
- Installation
- Setup & Configuration
- Mobile Wallet Payment
- Card Payment
- Transaction Status
- Error Handling
- Production Setup
- Complete Example
- Best Practices
- Troubleshooting
Installation #
Step 1: Add Dependency #
Add the package to your pubspec.yaml
:
dependencies:
flutter:
sdk: flutter
flutter_jazzcash: ^1.0.0
Step 2: Install Package #
flutter pub get
Step 3: Import Package #
import 'package:flutter_jazzcash/flutter_jazzcash.dart';
Setup & Configuration #
Step 1: Get JazzCash Credentials #
Contact JazzCash to obtain:
- Merchant ID (e.g., MC123456)
- Password (provided by JazzCash)
- Integrity Salt (for secure hash generation)
Step 2: Initialize the Service #
class PaymentService {
late JazzCashService jazzCash;
void initializeJazzCash() {
jazzCash = JazzCashService.initialize(
merchantId: 'YOUR_MERCHANT_ID', // Replace with your merchant ID
password: 'YOUR_PASSWORD', // Replace with your password
integritySalt: 'YOUR_INTEGRITY_SALT', // Replace with your integrity salt
isProduction: false, // Set to true for production
);
}
}
⚠️ Security Note: Never hardcode production credentials in your app. Use environment variables or secure storage.
Mobile Wallet Payment #
Mobile wallet payment allows users to pay directly through their JazzCash mobile app using their mobile number and CNIC.
Step 1: Create Payment Request #
Future<void> processMobileWalletPayment({
required double amount,
required String mobileNumber,
required String cnic,
required String description,
}) async {
try {
// Create payment request
final request = JazzCashMobileWalletRequest(
amount: amount, // Amount in PKR (e.g., 100.0)
billReference: _generateBillReference(), // Unique bill reference
cnic: cnic, // Customer's CNIC (13 digits)
description: description, // Payment description
mobileNumber: mobileNumber, // Customer's mobile number (03XXXXXXXXX)
customFields: { // Optional custom fields
'ppmpf_1': 'Custom Field 1',
'ppmpf_2': 'Custom Field 2',
},
);
// Process payment
final response = await jazzCash.processMobileWalletPayment(request);
// Handle response
if (response.isSuccessful) {
_handlePaymentSuccess(response);
} else {
_handlePaymentFailure(response);
}
} catch (e) {
_handlePaymentError(e);
}
}
String _generateBillReference() {
// Generate unique bill reference
return 'BILL${DateTime.now().millisecondsSinceEpoch}';
}
Step 2: Handle Payment Response #
void _handlePaymentSuccess(JazzCashMobileWalletResponse response) {
print('✅ Payment Successful!');
print('Transaction Reference: ${response.txnRefNo}');
print('Auth Code: ${response.authCode}');
print('Amount: PKR ${double.parse(response.amount) / 100}');
print('Status: ${response.statusMessage}');
// Update your UI
showSuccessDialog(
title: 'Payment Successful',
message: 'Your payment of PKR ${double.parse(response.amount) / 100} has been processed successfully.',
transactionRef: response.txnRefNo,
);
// Save transaction to your backend
saveTransactionToBackend(response);
}
void _handlePaymentFailure(JazzCashMobileWalletResponse response) {
print('❌ Payment Failed');
print('Error: ${response.statusMessage}');
print('Transaction Reference: ${response.txnRefNo}');
// Show error to user
showErrorDialog(
title: 'Payment Failed',
message: response.statusMessage,
);
}
Step 3: Input Validation #
bool validateMobileWalletInputs({
required String amount,
required String mobileNumber,
required String cnic,
required String description,
}) {
// Validate amount
final amountValue = double.tryParse(amount);
if (amountValue == null || amountValue <= 0) {
showError('Please enter a valid amount');
return false;
}
if (amountValue < 10) {
showError('Minimum amount is PKR 10');
return false;
}
if (amountValue > 25000) {
showError('Maximum amount is PKR 25,000');
return false;
}
// Validate mobile number
if (!RegExp(r'^03\d{9}$').hasMatch(mobileNumber)) {
showError('Please enter valid mobile number (03XXXXXXXXX)');
return false;
}
// Validate CNIC
if (!RegExp(r'^\d{13}$').hasMatch(cnic)) {
showError('Please enter valid 13-digit CNIC');
return false;
}
// Validate description
if (description.trim().length < 3) {
showError('Description must be at least 3 characters');
return false;
}
return true;
}
Card Payment #
Card payment opens a secure WebView where users can enter their card details and complete payment with 3D Secure authentication.
Step 1: Create Card Payment Request #
Future<void> processCardPayment({
required double amount,
required String description,
required BuildContext context,
}) async {
try {
// Create card payment request
final request = JazzCashCardPaymentRequest(
amount: amount, // Amount in PKR
billReference: _generateBillReference(), // Unique bill reference
description: description, // Payment description
returnUrl: 'https://yourapp.com/payment-return', // Your return URL
customFields: { // Optional custom fields
'ppmpf_1': 'Order ID: 12345',
'ppmpf_2': 'Customer ID: 67890',
},
);
// Open card payment WebView
await jazzCash.openCardPayment(
context: context,
request: request,
onPaymentSuccess: _handleCardPaymentSuccess,
onPaymentFailure: _handleCardPaymentFailure,
onPaymentCancelled: _handleCardPaymentCancellation,
);
} catch (e) {
_handlePaymentError(e);
}
}
Step 2: Handle Card Payment Callbacks #
void _handleCardPaymentSuccess(JazzCashCardPaymentResponse response) {
print('✅ Card Payment Successful!');
print('Transaction Reference: ${response.txnRefNo}');
print('Auth Code: ${response.authCode}');
print('Amount: PKR ${double.parse(response.amount) / 100}');
// Close any loading dialogs
Navigator.of(context).popUntil((route) => route.isFirst);
// Show success message
showSuccessDialog(
title: 'Card Payment Successful',
message: 'Your card payment has been processed successfully.',
transactionRef: response.txnRefNo,
);
// Save to backend
saveTransactionToBackend(response);
}
void _handleCardPaymentFailure(String error) {
print('❌ Card Payment Failed: $error');
// Show error message
showErrorDialog(
title: 'Card Payment Failed',
message: 'Your card payment could not be processed. Please try again.',
details: error,
);
}
void _handleCardPaymentCancellation() {
print('🚫 Card Payment Cancelled');
// Show cancellation message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Payment was cancelled'),
backgroundColor: Colors.orange,
),
);
}
Step 3: Card Payment Validation #
bool validateCardInputs({
required String amount,
required String description,
}) {
// Validate amount
final amountValue = double.tryParse(amount);
if (amountValue == null || amountValue <= 0) {
showError('Please enter a valid amount');
return false;
}
if (amountValue < 10) {
showError('Minimum amount is PKR 10');
return false;
}
// Validate description
if (description.trim().length < 3) {
showError('Description must be at least 3 characters');
return false;
}
return true;
}
Transaction Status #
Check the status of any transaction using its reference number.
Check Transaction Status #
Future<void> checkTransactionStatus(String transactionRef) async {
try {
// Show loading indicator
showLoadingDialog('Checking transaction status...');
// Check status
final status = await jazzCash.checkTransactionStatus(transactionRef);
// Hide loading
hideLoadingDialog();
// Parse response
final responseCode = status['pp_ResponseCode'] ?? '';
final responseMessage = status['pp_ResponseMessage'] ?? '';
// Handle different status codes
_handleTransactionStatus(responseCode, responseMessage, transactionRef);
} catch (e) {
hideLoadingDialog();
showError('Unable to check transaction status: $e');
}
}
void _handleTransactionStatus(String code, String message, String txnRef) {
String statusText;
Color statusColor;
IconData statusIcon;
switch (code) {
case '000':
statusText = 'Transaction Successful';
statusColor = Colors.green;
statusIcon = Icons.check_circle;
break;
case '001':
statusText = 'Transaction Pending';
statusColor = Colors.orange;
statusIcon = Icons.pending;
break;
case '101':
statusText = 'Transaction Failed';
statusColor = Colors.red;
statusIcon = Icons.error;
break;
case '111':
statusText = 'Insufficient Balance';
statusColor = Colors.red;
statusIcon = Icons.account_balance_wallet;
break;
default:
statusText = 'Unknown Status';
statusColor = Colors.grey;
statusIcon = Icons.help;
}
// Show status dialog
showStatusDialog(
status: statusText,
color: statusColor,
icon: statusIcon,
code: code,
message: message,
txnRef: txnRef,
);
}
Error Handling #
Comprehensive Error Handling #
void _handlePaymentError(dynamic error) {
String errorMessage;
String? errorCode;
if (error is JazzCashException) {
errorMessage = error.message;
errorCode = error.code;
// Handle specific JazzCash errors
switch (errorCode) {
case '001':
errorMessage = 'Transaction is pending. Please wait.';
break;
case '101':
errorMessage = 'Transaction failed. Please try again.';
break;
case '111':
errorMessage = 'Insufficient balance in your account.';
break;
case '121':
errorMessage = 'Invalid transaction. Please check your details.';
break;
default:
errorMessage = error.message;
}
} else {
errorMessage = 'An unexpected error occurred: $error';
}
// Log error for debugging
print('Payment Error: $errorMessage (Code: $errorCode)');
// Show user-friendly error
showErrorDialog(
title: 'Payment Error',
message: errorMessage,
code: errorCode,
);
}
// Error dialog helper
void showErrorDialog({
required String title,
required String message,
String? code,
}) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
Icon(Icons.error, color: Colors.red),
SizedBox(width: 8),
Expanded(child: Text(title)),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(message),
if (code != null) ...[
SizedBox(height: 8),
Text('Error Code: $code', style: TextStyle(color: Colors.grey)),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'),
),
],
),
);
}
Production Setup #
Step 1: Update Configuration #
void initializeForProduction() {
jazzCash = JazzCashService.initialize(
merchantId: 'YOUR_PRODUCTION_MERCHANT_ID',
password: 'YOUR_PRODUCTION_PASSWORD',
integritySalt: 'YOUR_PRODUCTION_INTEGRITY_SALT',
isProduction: true, // ✅ Set to true for production
);
}
Step 2: Environment Management #
class AppConfig {
static const bool isProduction = bool.fromEnvironment('PRODUCTION', defaultValue: false);
static String get merchantId => isProduction
? 'PROD_MERCHANT_ID'
: 'SANDBOX_MERCHANT_ID';
static String get password => isProduction
? 'PROD_PASSWORD'
: 'SANDBOX_PASSWORD';
static String get integritySalt => isProduction
? 'PROD_INTEGRITY_SALT'
: 'SANDBOX_INTEGRITY_SALT';
}
// Initialize with environment-based config
jazzCash = JazzCashService.initialize(
merchantId: AppConfig.merchantId,
password: AppConfig.password,
integritySalt: AppConfig.integritySalt,
isProduction: AppConfig.isProduction,
);
Step 3: Build Commands #
# For development (Sandbox)
flutter run
# For production
flutter run --dart-define=PRODUCTION=true
flutter build apk --dart-define=PRODUCTION=true
flutter build ios --dart-define=PRODUCTION=true
Complete Example #
Here's a complete implementation example:
import 'package:flutter/material.dart';
import 'package:flutter_jazzcash/flutter_jazzcash.dart';
class PaymentPage extends StatefulWidget {
@override
_PaymentPageState createState() => _PaymentPageState();
}
class _PaymentPageState extends State<PaymentPage> {
late JazzCashService jazzCash;
final TextEditingController amountController = TextEditingController();
final TextEditingController mobileController = TextEditingController();
final TextEditingController cnicController = TextEditingController();
final TextEditingController descriptionController = TextEditingController();
bool isLoading = false;
@override
void initState() {
super.initState();
// Initialize JazzCash
jazzCash = JazzCashService.initialize(
merchantId: 'YOUR_MERCHANT_ID',
password: 'YOUR_PASSWORD',
integritySalt: 'YOUR_INTEGRITY_SALT',
isProduction: false,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('JazzCash Payment')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
// Amount Field
TextField(
controller: amountController,
decoration: InputDecoration(
labelText: 'Amount (PKR)',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
),
SizedBox(height: 16),
// Description Field
TextField(
controller: descriptionController,
decoration: InputDecoration(
labelText: 'Description',
border: OutlineInputBorder(),
),
),
SizedBox(height: 16),
// Mobile Number Field (for mobile wallet)
TextField(
controller: mobileController,
decoration: InputDecoration(
labelText: 'Mobile Number (for wallet payment)',
hintText: '03XXXXXXXXX',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.phone,
),
SizedBox(height: 16),
// CNIC Field (for mobile wallet)
TextField(
controller: cnicController,
decoration: InputDecoration(
labelText: 'CNIC (for wallet payment)',
hintText: '1234567890123',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
),
SizedBox(height: 32),
// Payment Buttons
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: isLoading ? null : _processMobileWalletPayment,
icon: Icon(Icons.phone_android),
label: Text('Mobile Wallet'),
),
),
SizedBox(width: 16),
Expanded(
child: ElevatedButton.icon(
onPressed: isLoading ? null : _processCardPayment,
icon: Icon(Icons.credit_card),
label: Text('Card Payment'),
),
),
],
),
// Loading Indicator
if (isLoading)
Padding(
padding: EdgeInsets.only(top: 20),
child: CircularProgressIndicator(),
),
],
),
),
);
}
Future<void> _processMobileWalletPayment() async {
if (!_validateMobileWalletInputs()) return;
setState(() => isLoading = true);
try {
final request = JazzCashMobileWalletRequest(
amount: double.parse(amountController.text),
billReference: 'BILL${DateTime.now().millisecondsSinceEpoch}',
cnic: cnicController.text.trim(),
description: descriptionController.text.trim(),
mobileNumber: mobileController.text.trim(),
);
final response = await jazzCash.processMobileWalletPayment(request);
if (response.isSuccessful) {
_showSuccessDialog('Mobile Wallet Payment Successful', response.txnRefNo);
} else {
_showErrorDialog('Payment Failed', response.statusMessage);
}
} catch (e) {
_showErrorDialog('Error', e.toString());
} finally {
setState(() => isLoading = false);
}
}
Future<void> _processCardPayment() async {
if (!_validateCardInputs()) return;
final request = JazzCashCardPaymentRequest(
amount: double.parse(amountController.text),
billReference: 'CARD${DateTime.now().millisecondsSinceEpoch}',
description: descriptionController.text.trim(),
returnUrl: 'https://yourapp.com/payment-return',
);
await jazzCash.openCardPayment(
context: context,
request: request,
onPaymentSuccess: (response) {
_showSuccessDialog('Card Payment Successful', response.txnRefNo);
},
onPaymentFailure: (error) {
_showErrorDialog('Card Payment Failed', error);
},
onPaymentCancelled: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Payment cancelled')),
);
},
);
}
bool _validateMobileWalletInputs() {
if (amountController.text.isEmpty ||
mobileController.text.isEmpty ||
cnicController.text.isEmpty ||
descriptionController.text.isEmpty) {
_showErrorDialog('Validation Error', 'Please fill all fields');
return false;
}
if (!RegExp(r'^03\d{9}$').hasMatch(mobileController.text)) {
_showErrorDialog('Validation Error', 'Invalid mobile number format');
return false;
}
if (cnicController.text.length != 13) {
_showErrorDialog('Validation Error', 'CNIC must be 13 digits');
return false;
}
return true;
}
bool _validateCardInputs() {
if (amountController.text.isEmpty || descriptionController.text.isEmpty) {
_showErrorDialog('Validation Error', 'Please fill amount and description');
return false;
}
return true;
}
void _showSuccessDialog(String title, String txnRef) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
Icon(Icons.check_circle, color: Colors.green),
SizedBox(width: 8),
Text(title),
],
),
content: Text('Transaction Reference: $txnRef'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'),
),
],
),
);
}
void _showErrorDialog(String title, String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
Icon(Icons.error, color: Colors.red),
SizedBox(width: 8),
Text(title),
],
),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'),
),
],
),
);
}
@override
void dispose() {
amountController.dispose();
mobileController.dispose();
cnicController.dispose();
descriptionController.dispose();
super.dispose();
}
}
Best Practices #
1. Security #
- ✅ Never hardcode production credentials
- ✅ Use environment variables for configuration
- ✅ Validate all inputs before processing
- ✅ Always validate payment responses on your backend
- ✅ Use HTTPS for all return URLs
2. User Experience #
- ✅ Show loading indicators during payment processing
- ✅ Provide clear error messages
- ✅ Allow users to retry failed payments
- ✅ Save payment history for user reference
- ✅ Implement proper navigation handling
3. Error Handling #
- ✅ Handle network timeouts gracefully
- ✅ Provide fallback options for failed payments
- ✅ Log errors for debugging (without sensitive data)
- ✅ Show user-friendly error messages
- ✅ Implement retry mechanisms
4. Testing #
- ✅ Test with sandbox credentials thoroughly
- ✅ Test various error scenarios
- ✅ Validate on different devices and network conditions
- ✅ Test payment cancellation flows
- ✅ Verify transaction status checking
Troubleshooting #
Common Issues and Solutions #
1. Hash Validation Failed
Problem: "Response hash validation failed" Solution:
- Verify integrity salt is correct
- Check field ordering in hash generation
- Ensure all required fields are present
2. WebView Not Loading
Problem: Card payment WebView doesn't load Solution:
- Check internet connectivity
- Verify return URL is accessible
- Ensure WebView permissions are granted
3. Payment Timeout
Problem: Payment takes too long or times out Solution:
- Increase timeout duration
- Check network stability
- Implement retry mechanism
4. Invalid Credentials
Problem: "Invalid merchant credentials" Solution:
- Verify merchant ID, password, and integrity salt
- Check if using correct environment (sandbox vs production)
- Contact JazzCash support if needed
5. Mobile Number Validation
Problem: Mobile number not accepted Solution:
- Ensure format is 03XXXXXXXXX (11 digits)
- Remove any spaces or special characters
- Verify number is registered with JazzCash
Debug Mode #
Enable debug logging for troubleshooting:
void enableDebugMode() {
// Add debug prints in your payment methods
print('JazzCash Debug: Processing payment...');
print('Amount: ${request.amount}');
print('Bill Reference: ${request.billReference}');
print('Environment: ${jazzCash.isProduction ? 'Production' : 'Sandbox'}');
}
Support #
For additional help:
- Package Issues: Create an issue on the GitHub repository
- JazzCash Integration: Contact JazzCash merchant support
- Production Setup: Reach out to JazzCash technical team
- Documentation: Refer to JazzCash official API documentation
🎉 You're now ready to integrate JazzCash payments in your Flutter app!
This guide covers everything you need to implement both mobile wallet and card payments successfully. Remember to test thoroughly in sandbox mode before going live with production credentials.