Moneybag
Integration guides

Mobile App Integration

Complete guide for integrating Moneybag payments into mobile applications

Mobile App Integration Guide

This guide walks you through integrating Moneybag payments into your mobile application using Flutter as an example. The same principles apply to native iOS and Android development.


Overview

Mobile app payment integration follows a server-assisted flow for security:

  1. App initiates payment - Collects order details
  2. Backend creates checkout - Calls Moneybag API securely
  3. App opens payment page - Using WebView or browser
  4. Backend verifies payment - Confirms transaction status
  5. App shows result - Updates UI based on payment status

Security Notice

Never store API keys in your mobile app. Always use a backend server to communicate with the Moneybag API.


Architecture

sequenceDiagram
    participant App as Mobile App
    participant Backend as Your Backend
    participant Moneybag as Moneybag API
    participant WebView as Payment Page
    
    App->>Backend: Request payment (order details)
    Backend->>Moneybag: POST /payments/checkout
    Moneybag-->>Backend: Return checkout_url
    Backend-->>App: Send checkout_url
    App->>WebView: Open checkout_url
    WebView->>Moneybag: Process payment
    Moneybag-->>WebView: Payment complete
    WebView-->>App: Redirect with result
    App->>Backend: Verify payment
    Backend->>Moneybag: GET /payments/verify/{id}
    Moneybag-->>Backend: Payment details
    Backend-->>App: Confirmed status

Flutter Implementation

Step 1: Setup Dependencies

Add required packages to pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0
  webview_flutter: ^4.4.0
  url_launcher: ^6.2.0
  flutter_dotenv: ^5.1.0

Step 2: Create Payment Service

Create a service to communicate with your backend:

// lib/services/payment_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;

class PaymentService {
  static const String baseUrl = 'https://your-backend.com/api';
  
  // Create checkout session via your backend
  static Future<CheckoutResponse> createCheckout({
    required double amount,
    required String orderId,
    required CustomerInfo customer,
  }) async {
    try {
      final response = await http.post(
        Uri.parse('$baseUrl/create-checkout'),
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer ${await getAuthToken()}',
        },
        body: jsonEncode({
          'order_id': orderId,
          'amount': amount,
          'currency': 'BDT',
          'customer': customer.toJson(),
        }),
      );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        return CheckoutResponse.fromJson(data);
      } else {
        throw Exception('Failed to create checkout');
      }
    } catch (e) {
      throw Exception('Network error: $e');
    }
  }

  // Verify payment status via your backend
  static Future<PaymentStatus> verifyPayment(String transactionId) async {
    try {
      final response = await http.get(
        Uri.parse('$baseUrl/verify-payment/$transactionId'),
        headers: {
          'Authorization': 'Bearer ${await getAuthToken()}',
        },
      );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        return PaymentStatus.fromJson(data);
      } else {
        throw Exception('Failed to verify payment');
      }
    } catch (e) {
      throw Exception('Verification error: $e');
    }
  }

  static Future<String> getAuthToken() async {
    // Implement your auth token retrieval
    // This could be from secure storage, login session, etc.
    return 'user_auth_token';
  }
}

// Data models
class CheckoutResponse {
  final String checkoutUrl;
  final String transactionId;
  final DateTime expiresAt;

  CheckoutResponse({
    required this.checkoutUrl,
    required this.transactionId,
    required this.expiresAt,
  });

  factory CheckoutResponse.fromJson(Map<String, dynamic> json) {
    return CheckoutResponse(
      checkoutUrl: json['checkout_url'],
      transactionId: json['transaction_id'],
      expiresAt: DateTime.parse(json['expires_at']),
    );
  }
}

class CustomerInfo {
  final String name;
  final String email;
  final String phone;
  final String address;
  final String city;
  final String postcode;
  final String country;

  CustomerInfo({
    required this.name,
    required this.email,
    required this.phone,
    required this.address,
    required this.city,
    required this.postcode,
    required this.country,
  });

  Map<String, dynamic> toJson() {
    return {
      'name': name,
      'email': email,
      'phone': phone,
      'address': address,
      'city': city,
      'postcode': postcode,
      'country': country,
    };
  }
}

class PaymentStatus {
  final String transactionId;
  final String status;
  final double amount;
  final String paymentMethod;
  final DateTime? paidAt;

  PaymentStatus({
    required this.transactionId,
    required this.status,
    required this.amount,
    this.paymentMethod = '',
    this.paidAt,
  });

  factory PaymentStatus.fromJson(Map<String, dynamic> json) {
    return PaymentStatus(
      transactionId: json['transaction_id'],
      status: json['status'],
      amount: json['amount'].toDouble(),
      paymentMethod: json['payment_method'] ?? '',
      paidAt: json['paid_at'] != null 
        ? DateTime.parse(json['paid_at']) 
        : null,
    );
  }

  bool get isSuccess => status == 'SUCCESS';
  bool get isFailed => status == 'FAILED';
  bool get isPending => status == 'PENDING';
}

Step 3: Create Payment WebView

Create a WebView widget to handle the payment page:

// lib/screens/payment_webview.dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class PaymentWebView extends StatefulWidget {
  final String checkoutUrl;
  final String transactionId;
  final Function(String) onPaymentComplete;

  const PaymentWebView({
    Key? key,
    required this.checkoutUrl,
    required this.transactionId,
    required this.onPaymentComplete,
  }) : super(key: key);

  @override
  State<PaymentWebView> createState() => _PaymentWebViewState();
}

class _PaymentWebViewState extends State<PaymentWebView> {
  late final WebViewController controller;
  bool isLoading = true;

  @override
  void initState() {
    super.initState();
    
    controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setNavigationDelegate(
        NavigationDelegate(
          onProgress: (int progress) {
            setState(() {
              isLoading = progress < 100;
            });
          },
          onPageStarted: (String url) {
            debugPrint('Page started loading: $url');
          },
          onPageFinished: (String url) {
            debugPrint('Page finished loading: $url');
            _checkForRedirect(url);
          },
          onNavigationRequest: (NavigationRequest request) {
            // Handle deep links back to your app
            if (request.url.startsWith('yourapp://')) {
              _handleDeepLink(request.url);
              return NavigationDecision.prevent;
            }
            
            // Check for success/fail/cancel URLs
            if (_isCallbackUrl(request.url)) {
              _handleCallback(request.url);
              return NavigationDecision.prevent;
            }
            
            return NavigationDecision.navigate;
          },
        ),
      )
      ..loadRequest(Uri.parse(widget.checkoutUrl));
  }

  bool _isCallbackUrl(String url) {
    // Check if URL matches your callback URLs
    return url.contains('/payment/success') ||
           url.contains('/payment/fail') ||
           url.contains('/payment/cancel');
  }

  void _handleCallback(String url) {
    if (url.contains('/payment/success')) {
      // Extract transaction ID from URL if present
      final uri = Uri.parse(url);
      final transactionId = uri.queryParameters['transaction_id'] ?? 
                           widget.transactionId;
      widget.onPaymentComplete(transactionId);
      Navigator.pop(context, 'success');
    } else if (url.contains('/payment/fail')) {
      Navigator.pop(context, 'failed');
    } else if (url.contains('/payment/cancel')) {
      Navigator.pop(context, 'cancelled');
    }
  }

  void _handleDeepLink(String url) {
    // Handle deep links back to your app
    final uri = Uri.parse(url);
    final status = uri.queryParameters['status'];
    
    if (status == 'success') {
      widget.onPaymentComplete(widget.transactionId);
      Navigator.pop(context, 'success');
    } else {
      Navigator.pop(context, status);
    }
  }

  void _checkForRedirect(String url) {
    // Additional check for redirect URLs
    if (_isCallbackUrl(url)) {
      _handleCallback(url);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Complete Payment'),
        leading: IconButton(
          icon: const Icon(Icons.close),
          onPressed: () {
            showDialog(
              context: context,
              builder: (context) => AlertDialog(
                title: const Text('Cancel Payment?'),
                content: const Text(
                  'Are you sure you want to cancel this payment?'
                ),
                actions: [
                  TextButton(
                    onPressed: () => Navigator.pop(context),
                    child: const Text('Continue Payment'),
                  ),
                  TextButton(
                    onPressed: () {
                      Navigator.pop(context);
                      Navigator.pop(context, 'cancelled');
                    },
                    child: const Text('Cancel'),
                  ),
                ],
              ),
            );
          },
        ),
      ),
      body: Stack(
        children: [
          WebViewWidget(controller: controller),
          if (isLoading)
            const Center(
              child: CircularProgressIndicator(),
            ),
        ],
      ),
    );
  }
}

Step 4: Implement Checkout Flow

Create the main checkout flow in your app:

// lib/screens/checkout_screen.dart
import 'package:flutter/material.dart';
import 'payment_webview.dart';
import '../services/payment_service.dart';

class CheckoutScreen extends StatefulWidget {
  final double amount;
  final String orderId;

  const CheckoutScreen({
    Key? key,
    required this.amount,
    required this.orderId,
  }) : super(key: key);

  @override
  State<CheckoutScreen> createState() => _CheckoutScreenState();
}

class _CheckoutScreenState extends State<CheckoutScreen> {
  final _formKey = GlobalKey<FormState>();
  bool _isProcessing = false;
  
  // Form controllers
  final _nameController = TextEditingController();
  final _emailController = TextEditingController();
  final _phoneController = TextEditingController();
  final _addressController = TextEditingController();
  final _cityController = TextEditingController();
  final _postcodeController = TextEditingController();

  @override
  void dispose() {
    _nameController.dispose();
    _emailController.dispose();
    _phoneController.dispose();
    _addressController.dispose();
    _cityController.dispose();
    _postcodeController.dispose();
    super.dispose();
  }

  Future<void> _initiatePayment() async {
    if (!_formKey.currentState!.validate()) return;

    setState(() {
      _isProcessing = true;
    });

    try {
      // Create customer info
      final customer = CustomerInfo(
        name: _nameController.text,
        email: _emailController.text,
        phone: _phoneController.text,
        address: _addressController.text,
        city: _cityController.text,
        postcode: _postcodeController.text,
        country: 'Bangladesh',
      );

      // Create checkout session
      final checkout = await PaymentService.createCheckout(
        amount: widget.amount,
        orderId: widget.orderId,
        customer: customer,
      );

      if (!mounted) return;

      // Open payment WebView
      final result = await Navigator.push<String>(
        context,
        MaterialPageRoute(
          builder: (context) => PaymentWebView(
            checkoutUrl: checkout.checkoutUrl,
            transactionId: checkout.transactionId,
            onPaymentComplete: (transactionId) async {
              // Verify payment after completion
              await _verifyPayment(transactionId);
            },
          ),
        ),
      );

      // Handle result
      if (result == 'success') {
        _showSuccessDialog();
      } else if (result == 'failed') {
        _showErrorDialog('Payment failed. Please try again.');
      } else if (result == 'cancelled') {
        _showInfoDialog('Payment was cancelled.');
      }
    } catch (e) {
      _showErrorDialog('Error: ${e.toString()}');
    } finally {
      setState(() {
        _isProcessing = false;
      });
    }
  }

  Future<void> _verifyPayment(String transactionId) async {
    try {
      final status = await PaymentService.verifyPayment(transactionId);
      
      if (status.isSuccess) {
        // Payment verified successfully
        debugPrint('Payment verified: ${status.amount} ${status.paymentMethod}');
      } else if (status.isFailed) {
        throw Exception('Payment verification failed');
      }
    } catch (e) {
      debugPrint('Verification error: $e');
      // Handle verification error
    }
  }

  void _showSuccessDialog() {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => AlertDialog(
        title: const Row(
          children: [
            Icon(Icons.check_circle, color: Colors.green),
            SizedBox(width: 10),
            Text('Payment Successful'),
          ],
        ),
        content: Text(
          'Your payment of ৳${widget.amount.toStringAsFixed(2)} has been processed successfully.'
        ),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              Navigator.pop(context, true); // Return to previous screen
            },
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }

  void _showErrorDialog(String message) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Row(
          children: [
            Icon(Icons.error, color: Colors.red),
            SizedBox(width: 10),
            Text('Payment Error'),
          ],
        ),
        content: Text(message),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }

  void _showInfoDialog(String message) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Information'),
        content: Text(message),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Checkout'),
      ),
      body: Form(
        key: _formKey,
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            // Order Summary
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      'Order Summary',
                      style: TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 10),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        const Text('Order ID:'),
                        Text(widget.orderId),
                      ],
                    ),
                    const SizedBox(height: 5),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        const Text('Amount:'),
                        Text(
                          '৳${widget.amount.toStringAsFixed(2)}',
                          style: const TextStyle(
                            fontSize: 18,
                            fontWeight: FontWeight.bold,
                            color: Colors.green,
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 20),

            // Customer Information
            const Text(
              'Customer Information',
              style: TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 10),

            TextFormField(
              controller: _nameController,
              decoration: const InputDecoration(
                labelText: 'Full Name',
                border: OutlineInputBorder(),
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter your name';
                }
                return null;
              },
            ),
            const SizedBox(height: 10),

            TextFormField(
              controller: _emailController,
              keyboardType: TextInputType.emailAddress,
              decoration: const InputDecoration(
                labelText: 'Email',
                border: OutlineInputBorder(),
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter your email';
                }
                if (!value.contains('@')) {
                  return 'Please enter a valid email';
                }
                return null;
              },
            ),
            const SizedBox(height: 10),

            TextFormField(
              controller: _phoneController,
              keyboardType: TextInputType.phone,
              decoration: const InputDecoration(
                labelText: 'Phone Number',
                border: OutlineInputBorder(),
                prefixText: '+880 ',
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter your phone number';
                }
                return null;
              },
            ),
            const SizedBox(height: 10),

            TextFormField(
              controller: _addressController,
              decoration: const InputDecoration(
                labelText: 'Address',
                border: OutlineInputBorder(),
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter your address';
                }
                return null;
              },
            ),
            const SizedBox(height: 10),

            TextFormField(
              controller: _cityController,
              decoration: const InputDecoration(
                labelText: 'City',
                border: OutlineInputBorder(),
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter your city';
                }
                return null;
              },
            ),
            const SizedBox(height: 10),

            TextFormField(
              controller: _postcodeController,
              keyboardType: TextInputType.number,
              decoration: const InputDecoration(
                labelText: 'Postcode',
                border: OutlineInputBorder(),
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter your postcode';
                }
                return null;
              },
            ),
            const SizedBox(height: 30),

            // Pay Button
            ElevatedButton(
              onPressed: _isProcessing ? null : _initiatePayment,
              style: ElevatedButton.styleFrom(
                padding: const EdgeInsets.symmetric(vertical: 16),
                backgroundColor: Colors.green,
              ),
              child: _isProcessing
                  ? const CircularProgressIndicator(
                      valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
                    )
                  : Text(
                      'Pay ৳${widget.amount.toStringAsFixed(2)}',
                      style: const TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
            ),
          ],
        ),
      ),
    );
  }
}

Step 5: Backend Implementation

Your backend server should handle the API calls securely:

// backend/payment.js (Node.js/Express example)
const express = require('express');
const axios = require('axios');
const router = express.Router();

const MONEYBAG_API_KEY = process.env.MONEYBAG_API_KEY;
const MONEYBAG_BASE_URL = 'https://sandbox.api.moneybag.com.bd/api/v2';

// Create checkout session
router.post('/create-checkout', async (req, res) => {
  try {
    const { order_id, amount, currency, customer } = req.body;
    
    const response = await axios.post(
      `${MONEYBAG_BASE_URL}/payments/checkout`,
      {
        order_id,
        order_amount: amount,
        currency: currency || 'BDT',
        order_description: 'Mobile app purchase',
        success_url: 'yourapp://payment/success',
        cancel_url: 'yourapp://payment/cancel',
        fail_url: 'yourapp://payment/fail',
        customer: {
          name: customer.name,
          email: customer.email,
          phone: customer.phone,
          address: customer.address,
          city: customer.city,
          postcode: customer.postcode,
          country: customer.country
        }
      },
      {
        headers: {
          'X-Merchant-API-Key': MONEYBAG_API_KEY,
          'Content-Type': 'application/json'
        }
      }
    );
    
    res.json({
      checkout_url: response.data.data.checkout_url,
      transaction_id: response.data.data.session_id,
      expires_at: response.data.data.expires_at
    });
  } catch (error) {
    console.error('Checkout error:', error);
    res.status(500).json({ error: 'Failed to create checkout' });
  }
});

// Verify payment
router.get('/verify-payment/:transactionId', async (req, res) => {
  try {
    const { transactionId } = req.params;
    
    const response = await axios.get(
      `${MONEYBAG_BASE_URL}/payments/verify/${transactionId}`,
      {
        headers: {
          'X-Merchant-API-Key': MONEYBAG_API_KEY
        }
      }
    );
    
    const data = response.data.data;
    
    res.json({
      transaction_id: data.transaction_id,
      status: data.status,
      amount: data.amount,
      payment_method: data.payment_method,
      paid_at: data.paid_at
    });
  } catch (error) {
    console.error('Verification error:', error);
    res.status(500).json({ error: 'Failed to verify payment' });
  }
});

module.exports = router;

Android Setup

Add deep link configuration to android/app/src/main/AndroidManifest.xml:

<activity
    android:name=".MainActivity"
    android:launchMode="singleTop">
    
    <!-- Deep link configuration -->
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        
        <data
            android:scheme="yourapp"
            android:host="payment" />
    </intent-filter>
</activity>

iOS Setup

Add URL scheme to ios/Runner/Info.plist:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>yourapp</string>
        </array>
        <key>CFBundleURLName</key>
        <string>com.yourcompany.yourapp</string>
    </dict>
</array>

Alternative: External Browser

For simpler implementation, you can use the device's browser:

import 'package:url_launcher/url_launcher.dart';

Future<void> _openPaymentInBrowser(String checkoutUrl) async {
  final Uri url = Uri.parse(checkoutUrl);
  
  if (await canLaunchUrl(url)) {
    await launchUrl(
      url,
      mode: LaunchMode.externalApplication,
    );
    
    // Start polling for payment status
    _startPollingForPaymentStatus();
  } else {
    throw Exception('Could not launch payment URL');
  }
}

void _startPollingForPaymentStatus() {
  Timer.periodic(const Duration(seconds: 5), (timer) async {
    try {
      final status = await PaymentService.verifyPayment(transactionId);
      
      if (status.isSuccess || status.isFailed) {
        timer.cancel();
        // Handle payment result
        if (status.isSuccess) {
          _showSuccessDialog();
        } else {
          _showErrorDialog('Payment failed');
        }
      }
    } catch (e) {
      // Continue polling
    }
  });
}

Security Best Practices

1. Secure API Communication

// Use HTTPS only
class SecureApiClient {
  static const String baseUrl = 'https://your-backend.com/api';
  
  static Future<Map<String, dynamic>> secureRequest(
    String endpoint,
    Map<String, dynamic> data,
  ) async {
    // Add request signing
    final signature = await _generateSignature(data);
    
    final response = await http.post(
      Uri.parse('$baseUrl/$endpoint'),
      headers: {
        'Content-Type': 'application/json',
        'X-Signature': signature,
        'X-Timestamp': DateTime.now().millisecondsSinceEpoch.toString(),
      },
      body: jsonEncode(data),
    );
    
    return jsonDecode(response.body);
  }
  
  static Future<String> _generateSignature(Map<String, dynamic> data) async {
    // Implement HMAC-SHA256 signing
    // Use a shared secret between app and backend
    return 'signature';
  }
}

2. Certificate Pinning

// Implement certificate pinning for additional security
import 'dart:io';

class SecureHttpClient {
  static HttpClient createHttpClient() {
    final client = HttpClient();
    
    client.badCertificateCallback = (cert, host, port) {
      // Verify certificate fingerprint
      final fingerprint = _getCertificateFingerprint(cert);
      return _trustedFingerprints.contains(fingerprint);
    };
    
    return client;
  }
  
  static final _trustedFingerprints = [
    'SHA256:XXXXXX', // Your backend certificate fingerprint
  ];
}

3. Secure Storage

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class SecureStorage {
  static const _storage = FlutterSecureStorage();
  
  static Future<void> saveAuthToken(String token) async {
    await _storage.write(key: 'auth_token', value: token);
  }
  
  static Future<String?> getAuthToken() async {
    return await _storage.read(key: 'auth_token');
  }
  
  static Future<void> clearAuthData() async {
    await _storage.deleteAll();
  }
}

Testing

Test Cards for Mobile

Use these test cards in sandbox:

class TestData {
  static const testCards = {
    'success': '4111111111111111',
    'declined': '4000000000000002',
    '3d_secure': '4000000000003220',
  };
  
  static const testMobileBanking = {
    'bkash': '01700000001',
    'nagad': '01800000001',
    'rocket': '01900000001',
  };
}

Debug Mode

Add debug logging for development:

class PaymentDebugger {
  static bool debugMode = kDebugMode;
  
  static void log(String message, [dynamic data]) {
    if (debugMode) {
      print('[PAYMENT] $message');
      if (data != null) {
        print('[PAYMENT DATA] ${jsonEncode(data)}');
      }
    }
  }
  
  static void logError(String message, dynamic error) {
    if (debugMode) {
      print('[PAYMENT ERROR] $message');
      print('[ERROR DETAILS] $error');
    }
  }
}

Common Issues & Solutions

WebView Not Loading

// Ensure JavaScript is enabled
controller.setJavaScriptMode(JavaScriptMode.unrestricted);

// Add user agent for better compatibility
controller.setUserAgent('MoneybagMobileApp/1.0 Flutter');
// Handle both custom scheme and universal links
if (Platform.isIOS) {
  // iOS specific handling
  if (url.startsWith('yourapp://') || 
      url.startsWith('https://yourapp.com/payment')) {
    _handleDeepLink(url);
  }
} else {
  // Android handling
  if (url.startsWith('yourapp://')) {
    _handleDeepLink(url);
  }
}

Payment Status Not Updating

// Implement retry logic with exponential backoff
Future<PaymentStatus> verifyWithRetry(
  String transactionId, {
  int maxRetries = 3,
}) async {
  int retryCount = 0;
  
  while (retryCount < maxRetries) {
    try {
      return await PaymentService.verifyPayment(transactionId);
    } catch (e) {
      retryCount++;
      if (retryCount >= maxRetries) rethrow;
      
      // Exponential backoff
      await Future.delayed(Duration(seconds: pow(2, retryCount).toInt()));
    }
  }
  
  throw Exception('Max retries exceeded');
}

Performance Optimization

1. Preload WebView

class WebViewPreloader {
  static WebViewController? _preloadedController;
  
  static void preload() {
    _preloadedController = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..loadRequest(Uri.parse('about:blank'));
  }
  
  static WebViewController getController() {
    return _preloadedController ?? WebViewController();
  }
}

2. Cache Customer Data

class CustomerDataCache {
  static Future<CustomerInfo?> getCachedCustomer() async {
    final prefs = await SharedPreferences.getInstance();
    final data = prefs.getString('customer_data');
    
    if (data != null) {
      return CustomerInfo.fromJson(jsonDecode(data));
    }
    return null;
  }
  
  static Future<void> cacheCustomer(CustomerInfo customer) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('customer_data', jsonEncode(customer.toJson()));
  }
}

Native Platform Examples

iOS (Swift)

// PaymentService.swift
import Foundation

class PaymentService {
    static let shared = PaymentService()
    private let baseURL = "https://your-backend.com/api"
    
    func createCheckout(amount: Double, orderId: String, customer: CustomerInfo, 
                       completion: @escaping (Result<CheckoutResponse, Error>) -> Void) {
        
        let url = URL(string: "\(baseURL)/create-checkout")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let body = [
            "order_id": orderId,
            "amount": amount,
            "customer": customer.toDictionary()
        ]
        
        request.httpBody = try? JSONSerialization.data(withJSONObject: body)
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            // Handle response
        }.resume()
    }
}

Android (Kotlin)

// PaymentService.kt
package com.yourapp.payment

import retrofit2.http.*

interface PaymentApi {
    @POST("create-checkout")
    suspend fun createCheckout(
        @Body request: CheckoutRequest
    ): CheckoutResponse
    
    @GET("verify-payment/{transactionId}")
    suspend fun verifyPayment(
        @Path("transactionId") transactionId: String
    ): PaymentStatus
}

class PaymentService(private val api: PaymentApi) {
    suspend fun initiatePayment(
        amount: Double,
        orderId: String,
        customer: CustomerInfo
    ): CheckoutResponse {
        return api.createCheckout(
            CheckoutRequest(
                orderId = orderId,
                amount = amount,
                customer = customer
            )
        )
    }
}

Support

Need help with mobile integration?