Skip to main content

Flutter SDK

The Omise Flutter SDK enables you to build secure payment integrations for both iOS and Android platforms with a single codebase. Built with Dart, it provides a unified API for tokenizing cards, creating payment sources, and handling all major payment methods in Southeast Asia.

Overviewโ€‹

The Flutter SDK enables you to:

  • Write once, deploy everywhere - Single codebase for iOS and Android
  • Tokenize credit cards securely without sensitive data touching your servers
  • Create payment sources for alternative payment methods
  • Implement native UI with Flutter widgets
  • Support async operations with Dart's Future and Stream APIs
  • Handle errors gracefully with comprehensive exception handling

Key Featuresโ€‹

  • Cross-platform support (iOS 12+, Android 5.0+)
  • Null-safe Dart API
  • Future-based async operations
  • Stream support for real-time updates
  • Built-in input validation
  • Type-safe request/response models
  • Platform-specific features handled automatically
  • Hot reload support for rapid development

Requirementsโ€‹

  • Flutter 2.0 or later
  • Dart 2.12 or later (null safety)
  • iOS 12.0+ or Android API Level 21+
  • Xcode 13.0+ (for iOS)
  • Android Studio Arctic Fox+ (for Android)

Installationโ€‹

Add to pubspec.yamlโ€‹

dependencies:
omise_flutter: ^4.0.0

Install Packageโ€‹

flutter pub add omise_flutter

Or manually:

flutter pub get

Platform Setupโ€‹

iOS Setupโ€‹

Add to ios/Podfile:

platform :ios, '12.0'

Android Setupโ€‹

Update android/app/build.gradle:

android {
defaultConfig {
minSdkVersion 21
}
}

Add internet permission to android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />

Quick Startโ€‹

1. Import the Packageโ€‹

import 'package:omise_flutter/omise_flutter.dart';

2. Initialize the Clientโ€‹

class PaymentService {
final Omise omise;

PaymentService()
: omise = Omise(
publicKey: 'pkey_test_5xyzyx5xyzyx5xyzyx5',
);
}

3. Create a Tokenโ€‹

Future<Token> createToken({
required String name,
required String number,
required int expirationMonth,
required int expirationYear,
required String securityCode,
}) async {
try {
final token = await omise.createToken(
name: name,
number: number,
expirationMonth: expirationMonth,
expirationYear: expirationYear,
securityCode: securityCode,
);

print('Token created: ${token.id}');
return token;

} catch (error) {
print('Error creating token: $error');
rethrow;
}
}

// Usage
void processPayment() async {
try {
final token = await createToken(
name: 'John Doe',
number: '4242424242424242',
expirationMonth: 12,
expirationYear: 2025,
securityCode: '123',
);

// Send token to your server
await sendTokenToServer(token.id);

} catch (error) {
showError(error.toString());
}
}

Configurationโ€‹

Client Configurationโ€‹

// Basic configuration
final omise = Omise(
publicKey: 'pkey_test_5xyzyx5xyzyx5xyzyx5',
);

// Advanced configuration
final omise = Omise(
publicKey: 'pkey_test_5xyzyx5xyzyx5xyzyx5',
apiVersion: '2019-05-29',
timeout: Duration(seconds: 60),
debugMode: kDebugMode,
);

Environment-based Configurationโ€‹

class Config {
static String get publicKey {
const environment = String.fromEnvironment(
'ENVIRONMENT',
defaultValue: 'development',
);

switch (environment) {
case 'production':
return 'pkey_5xyzyx5xyzyx5xyzyx5';
case 'staging':
return 'pkey_test_staging_key';
default:
return 'pkey_test_5xyzyx5xyzyx5xyzyx5';
}
}
}

final omise = Omise(publicKey: Config.publicKey);

Creating Tokensโ€‹

Basic Card Tokenizationโ€‹

Future<Token> tokenizeCard({
required String name,
required String number,
required int expirationMonth,
required int expirationYear,
required String securityCode,
}) async {
return await omise.createToken(
name: name,
number: number,
expirationMonth: expirationMonth,
expirationYear: expirationYear,
securityCode: securityCode,
);
}

With Billing Addressโ€‹

Future<Token> tokenizeCardWithAddress() async {
final address = Address(
street1: '123 Wireless Road',
street2: 'Lumpini',
city: 'Pathum Wan',
state: 'Bangkok',
postalCode: '10330',
country: 'TH',
);

return await omise.createToken(
name: 'John Doe',
number: '4242424242424242',
expirationMonth: 12,
expirationYear: 2025,
securityCode: '123',
billingAddress: address,
);
}

Validation Before Tokenizationโ€‹

import 'package:omise_flutter/omise_flutter.dart';

Future<Token> validateAndTokenize({
required String number,
required int expirationMonth,
required int expirationYear,
required String securityCode,
required String name,
}) async {
// Validate card number
if (!CardValidator.isValidNumber(number)) {
throw ValidationException('Invalid card number');
}

// Validate expiry
if (!CardValidator.isValidExpiry(expirationMonth, expirationYear)) {
throw ValidationException('Invalid or expired card');
}

// Validate CVV
final brand = CardValidator.detectBrand(number);
if (!CardValidator.isValidCVV(securityCode, brand)) {
throw ValidationException('Invalid security code');
}

// Validate name
if (name.trim().isEmpty) {
throw ValidationException('Cardholder name is required');
}

return await omise.createToken(
name: name,
number: number,
expirationMonth: expirationMonth,
expirationYear: expirationYear,
securityCode: securityCode,
);
}

class ValidationException implements Exception {
final String message;
ValidationException(this.message);

@override
String toString() => message;
}

Creating Payment Sourcesโ€‹

Internet Bankingโ€‹

Future<Source> createInternetBankingSource({
required int amount,
required InternetBanking bank,
}) async {
final source = await omise.createSource(
amount: amount,
currency: 'thb',
type: SourceType.internetBanking,
internetBanking: bank,
);

// Redirect user to authorize payment
if (source.authorizeUri != null) {
await openAuthorizeUrl(source.authorizeUri!);
}

return source;
}

// Usage
final source = await createInternetBankingSource(
amount: 100000, // 1,000.00 THB
bank: InternetBanking.bay,
);

Mobile Bankingโ€‹

Future<Source> createMobileBankingSource({
required int amount,
required MobileBanking bank,
}) async {
return await omise.createSource(
amount: amount,
currency: 'thb',
type: SourceType.mobileBanking,
mobileBanking: bank,
);
}

// Usage
final source = await createMobileBankingSource(
amount: 50000, // 500.00 THB
bank: MobileBanking.scb,
);

PromptPayโ€‹

Future<Source> createPromptPaySource({
required int amount,
}) async {
final source = await omise.createSource(
amount: amount,
currency: 'thb',
type: SourceType.promptPay,
);

// Display QR code to user
if (source.scanQRCodeUrl != null) {
displayQRCode(source.scanQRCodeUrl!);
}

return source;
}

TrueMoney Walletโ€‹

Future<Source> createTrueMoneySource({
required int amount,
required String phoneNumber,
}) async {
return await omise.createSource(
amount: amount,
currency: 'thb',
type: SourceType.trueMoney,
phoneNumber: phoneNumber,
);
}

// Usage
final source = await createTrueMoneySource(
amount: 100000,
phoneNumber: '0812345678',
);

Alipayโ€‹

Future<Source> createAlipaySource({
required int amount,
}) async {
return await omise.createSource(
amount: amount,
currency: 'thb',
type: SourceType.alipay,
);
}

UI Componentsโ€‹

Credit Card Form Widgetโ€‹

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

class CreditCardFormWidget extends StatefulWidget {
final Function(Token) onTokenCreated;
final Function(String) onError;

const CreditCardFormWidget({
Key? key,
required this.onTokenCreated,
required this.onError,
}) : super(key: key);

@override
State<CreditCardFormWidget> createState() => _CreditCardFormWidgetState();
}

class _CreditCardFormWidgetState extends State<CreditCardFormWidget> {
final _formKey = GlobalKey<FormState>();
final _cardNumberController = TextEditingController();
final _expiryController = TextEditingController();
final _cvvController = TextEditingController();
final _nameController = TextEditingController();
bool _isLoading = false;

final omise = Omise(publicKey: 'pkey_test_5xyzyx5xyzyx5xyzyx5');

@override
void dispose() {
_cardNumberController.dispose();
_expiryController.dispose();
_cvvController.dispose();
_nameController.dispose();
super.dispose();
}

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

setState(() => _isLoading = true);

try {
// Parse expiry date
final expiry = _expiryController.text.split('/');
final month = int.parse(expiry[0].trim());
final year = 2000 + int.parse(expiry[1].trim());

final token = await omise.createToken(
name: _nameController.text,
number: _cardNumberController.text.replaceAll(' ', ''),
expirationMonth: month,
expirationYear: year,
securityCode: _cvvController.text,
);

widget.onTokenCreated(token);

} catch (error) {
widget.onError(error.toString());
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}

@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _cardNumberController,
decoration: const InputDecoration(
labelText: 'Card Number',
hintText: '4242 4242 4242 4242',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Card number is required';
}
final cleaned = value.replaceAll(' ', '');
if (!CardValidator.isValidNumber(cleaned)) {
return 'Invalid card number';
}
return null;
},
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextFormField(
controller: _expiryController,
decoration: const InputDecoration(
labelText: 'Expiry',
hintText: 'MM/YY',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Expiry is required';
}
// Add expiry validation
return null;
},
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _cvvController,
decoration: const InputDecoration(
labelText: 'CVV',
hintText: '123',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'CVV is required';
}
if (value.length < 3) {
return 'Invalid CVV';
}
return null;
},
),
),
],
),
const SizedBox(height: 16),
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Cardholder Name',
hintText: 'John Doe',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Name is required';
}
return null;
},
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _submitForm,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(16),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Pay Now'),
),
],
),
);
}
}

Payment Method Selectorโ€‹

class PaymentMethodSelector extends StatelessWidget {
final Function(PaymentMethod) onMethodSelected;

const PaymentMethodSelector({
Key? key,
required this.onMethodSelected,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return ListView(
children: [
PaymentMethodTile(
icon: Icons.credit_card,
title: 'Credit/Debit Card',
subtitle: 'Visa, Mastercard, JCB',
onTap: () => onMethodSelected(PaymentMethod.card),
),
PaymentMethodTile(
icon: Icons.account_balance,
title: 'Internet Banking',
subtitle: 'Bangkok Bank, SCB, Kasikorn',
onTap: () => onMethodSelected(PaymentMethod.internetBanking),
),
PaymentMethodTile(
icon: Icons.qr_code,
title: 'PromptPay',
subtitle: 'Scan QR code to pay',
onTap: () => onMethodSelected(PaymentMethod.promptPay),
),
PaymentMethodTile(
icon: Icons.wallet,
title: 'TrueMoney Wallet',
subtitle: 'Pay with TrueMoney',
onTap: () => onMethodSelected(PaymentMethod.trueMoney),
),
],
);
}
}

class PaymentMethodTile extends StatelessWidget {
final IconData icon;
final String title;
final String subtitle;
final VoidCallback onTap;

const PaymentMethodTile({
Key? key,
required this.icon,
required this.title,
required this.subtitle,
required this.onTap,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(icon, size: 32),
title: Text(title),
subtitle: Text(subtitle),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
);
}
}

enum PaymentMethod {
card,
internetBanking,
promptPay,
trueMoney,
}

QR Code Display Widgetโ€‹

import 'package:qr_flutter/qr_flutter.dart';

class QRCodeWidget extends StatelessWidget {
final String qrCodeUrl;
final String amount;

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

@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Scan to Pay',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'Amount: $amount THB',
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 24),
QrImageView(
data: qrCodeUrl,
version: QrVersions.auto,
size: 250.0,
),
const SizedBox(height: 24),
const Text(
'Open your mobile banking app\nand scan this QR code',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
],
);
}
}

Input Validationโ€‹

Card Number Validationโ€‹

import 'package:omise_flutter/omise_flutter.dart';

class CardNumberValidator {
static bool validate(String number) {
return CardValidator.isValidNumber(number);
}

static CardBrand detectBrand(String number) {
return CardValidator.detectBrand(number);
}

static String format(String number) {
// Remove any existing spaces
final cleaned = number.replaceAll(' ', '');

// Add space every 4 digits
final buffer = StringBuffer();
for (var i = 0; i < cleaned.length; i++) {
if (i > 0 && i % 4 == 0) {
buffer.write(' ');
}
buffer.write(cleaned[i]);
}

return buffer.toString();
}
}

// Usage in TextField
class CardNumberField extends StatelessWidget {
final TextEditingController controller;

const CardNumberField({Key? key, required this.controller}) : super(key: key);

@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
decoration: const InputDecoration(
labelText: 'Card Number',
hintText: '4242 4242 4242 4242',
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(16),
CardNumberInputFormatter(),
],
validator: (value) {
if (value == null || value.isEmpty) {
return 'Card number is required';
}
if (!CardNumberValidator.validate(value.replaceAll(' ', ''))) {
return 'Invalid card number';
}
return null;
},
);
}
}

class CardNumberInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final text = newValue.text.replaceAll(' ', '');
final formatted = CardNumberValidator.format(text);

return TextEditingValue(
text: formatted,
selection: TextSelection.collapsed(offset: formatted.length),
);
}
}

Expiry Date Validationโ€‹

class ExpiryValidator {
static bool isValid(int month, int year) {
return CardValidator.isValidExpiry(month, year);
}

static bool isExpired(int month, int year) {
final now = DateTime.now();
final expiry = DateTime(year, month);
return expiry.isBefore(now);
}
}

class ExpiryInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final text = newValue.text.replaceAll('/', '');

if (text.length >= 2) {
return TextEditingValue(
text: '${text.substring(0, 2)}/${text.substring(2)}',
selection: TextSelection.collapsed(
offset: text.length <= 2 ? text.length : text.length + 1,
),
);
}

return newValue;
}
}

CVV Validationโ€‹

class CVVValidator {
static bool isValid(String cvv, CardBrand brand) {
return CardValidator.isValidCVV(cvv, brand);
}

static int expectedLength(CardBrand brand) {
return brand == CardBrand.amex ? 4 : 3;
}
}

State Managementโ€‹

Using Providerโ€‹

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

class PaymentProvider with ChangeNotifier {
final Omise omise;

PaymentState _state = PaymentState.idle;
String? _errorMessage;
Token? _token;
Source? _source;

PaymentProvider(this.omise);

PaymentState get state => _state;
String? get errorMessage => _errorMessage;
Token? get token => _token;
Source? get source => _source;

Future<void> createToken({
required String name,
required String number,
required int expirationMonth,
required int expirationYear,
required String securityCode,
}) async {
_state = PaymentState.loading;
_errorMessage = null;
notifyListeners();

try {
_token = await omise.createToken(
name: name,
number: number,
expirationMonth: expirationMonth,
expirationYear: expirationYear,
securityCode: securityCode,
);

_state = PaymentState.success;
notifyListeners();

} catch (error) {
_state = PaymentState.error;
_errorMessage = error.toString();
notifyListeners();
}
}

Future<void> createSource({
required int amount,
required String currency,
required SourceType type,
}) async {
_state = PaymentState.loading;
_errorMessage = null;
notifyListeners();

try {
_source = await omise.createSource(
amount: amount,
currency: currency,
type: type,
);

_state = PaymentState.success;
notifyListeners();

} catch (error) {
_state = PaymentState.error;
_errorMessage = error.toString();
notifyListeners();
}
}

void reset() {
_state = PaymentState.idle;
_errorMessage = null;
_token = null;
_source = null;
notifyListeners();
}
}

enum PaymentState {
idle,
loading,
success,
error,
}

// Usage
class PaymentScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => PaymentProvider(
Omise(publicKey: 'pkey_test_5xyzyx5xyzyx5xyzyx5'),
),
child: Consumer<PaymentProvider>(
builder: (context, provider, child) {
switch (provider.state) {
case PaymentState.loading:
return const Center(child: CircularProgressIndicator());
case PaymentState.error:
return ErrorWidget(message: provider.errorMessage!);
case PaymentState.success:
return SuccessWidget(token: provider.token!);
default:
return CreditCardFormWidget(
onTokenCreated: (token) {
// Handle token
},
onError: (error) {
// Handle error
},
);
}
},
),
);
}
}

Using Riverpodโ€‹

import 'package:flutter_riverpod/flutter_riverpod.dart';

final omiseProvider = Provider((ref) {
return Omise(publicKey: 'pkey_test_5xyzyx5xyzyx5xyzyx5');
});

final tokenProvider = FutureProvider.family<Token, TokenRequest>((ref, request) async {
final omise = ref.watch(omiseProvider);
return await omise.createToken(
name: request.name,
number: request.number,
expirationMonth: request.expirationMonth,
expirationYear: request.expirationYear,
securityCode: request.securityCode,
);
});

class TokenRequest {
final String name;
final String number;
final int expirationMonth;
final int expirationYear;
final String securityCode;

TokenRequest({
required this.name,
required this.number,
required this.expirationMonth,
required this.expirationYear,
required this.securityCode,
});
}

// Usage
class PaymentScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final tokenAsync = ref.watch(tokenProvider(TokenRequest(
name: 'John Doe',
number: '4242424242424242',
expirationMonth: 12,
expirationYear: 2025,
securityCode: '123',
)));

return tokenAsync.when(
data: (token) => SuccessWidget(token: token),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorWidget(message: error.toString()),
);
}
}

3D Secure Authenticationโ€‹

Handling 3D Secure with WebViewโ€‹

import 'package:webview_flutter/webview_flutter.dart';

class ThreeDSecureWebView extends StatefulWidget {
final String authorizeUrl;
final String returnUrl;
final Function(bool) onComplete;

const ThreeDSecureWebView({
Key? key,
required this.authorizeUrl,
required this.returnUrl,
required this.onComplete,
}) : super(key: key);

@override
State<ThreeDSecureWebView> createState() => _ThreeDSecureWebViewState();
}

class _ThreeDSecureWebViewState extends State<ThreeDSecureWebView> {
late final WebViewController _controller;
bool _isLoading = true;

@override
void initState() {
super.initState();

_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (url) {
setState(() => _isLoading = true);
},
onPageFinished: (url) {
setState(() => _isLoading = false);

// Check if returned from 3DS
if (url.startsWith(widget.returnUrl)) {
widget.onComplete(true);
}
},
onNavigationRequest: (request) {
if (request.url.startsWith(widget.returnUrl)) {
widget.onComplete(true);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
)
..loadRequest(Uri.parse(widget.authorizeUrl));
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Secure Payment'),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => widget.onComplete(false),
),
),
body: Stack(
children: [
WebViewWidget(controller: _controller),
if (_isLoading)
const Center(child: CircularProgressIndicator()),
],
),
);
}
}

// Usage
Future<void> processPaymentWithToken(String tokenId) async {
// Create charge on your server
final charge = await createChargeOnServer(tokenId);

if (charge.authorizeUri != null) {
final success = await Navigator.push<bool>(
context,
MaterialPageRoute(
builder: (_) => ThreeDSecureWebView(
authorizeUrl: charge.authorizeUri!,
returnUrl: 'myapp://payment/callback',
onComplete: (success) {
Navigator.pop(context, success);
},
),
),
);

if (success == true) {
await verifyChargeStatus();
}
}
}

Error Handlingโ€‹

Error Typesโ€‹

Future<void> handlePayment() async {
try {
final token = await omise.createToken(
name: 'John Doe',
number: '4242424242424242',
expirationMonth: 12,
expirationYear: 2025,
securityCode: '123',
);

handleSuccess(token);

} on OmiseException catch (e) {
// API error from Omise
handleOmiseError(e);
} on NetworkException catch (e) {
// Network connectivity error
showNetworkError();
} on ValidationException catch (e) {
// Input validation error
showValidationError(e.message);
} catch (e) {
// Unknown error
showGenericError(e.toString());
}
}

Custom Error Handlingโ€‹

void handleOmiseError(OmiseException error) {
switch (error.code) {
case 'invalid_card':
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Invalid Card'),
content: const Text('Please check your card details'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
break;

case 'insufficient_fund':
showSnackBar('Your card has insufficient funds');
break;

case 'failed_processing':
showSnackBar('Unable to process your payment');
break;

default:
showSnackBar(error.message);
}
}

void showSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}

Retry Logicโ€‹

Future<Token> createTokenWithRetry({
required int maxAttempts,
required Duration delay,
}) async {
int attempts = 0;

while (attempts < maxAttempts) {
try {
return await omise.createToken(
name: 'John Doe',
number: '4242424242424242',
expirationMonth: 12,
expirationYear: 2025,
securityCode: '123',
);
} catch (error) {
attempts++;

// Don't retry on validation errors
if (error is ValidationException) {
rethrow;
}

if (attempts >= maxAttempts) {
rethrow;
}

// Wait before retrying
await Future.delayed(delay * attempts);
}
}

throw Exception('Max retry attempts reached');
}

Best Practicesโ€‹

Securityโ€‹

// โœ… DO: Use environment variables
class Config {
static const publicKey = String.fromEnvironment(
'OMISE_PUBLIC_KEY',
defaultValue: 'pkey_test_5xyzyx5xyzyx5xyzyx5',
);
}

// โŒ DON'T: Hardcode production keys
// const publicKey = 'pkey_5xyzyx5xyzyx5xyzyx5';

// โœ… DO: Use kDebugMode for testing
final omise = Omise(
publicKey: kDebugMode ? Config.testKey : Config.publicKey,
);

// โŒ DON'T: Log sensitive data
// print('Card number: $cardNumber');

// โœ… DO: Use sanitized logging
debugPrint('Token created: ${token.id}');

Performanceโ€‹

// โœ… DO: Use const constructors
const CreditCardFormWidget(
onTokenCreated: handleToken,
onError: handleError,
);

// โœ… DO: Dispose controllers
@override
void dispose() {
_cardNumberController.dispose();
_expiryController.dispose();
_cvvController.dispose();
super.dispose();
}

// โœ… DO: Use keys for expensive widgets
ListView.builder(
itemBuilder: (context, index) {
return PaymentMethodTile(
key: ValueKey(methods[index].id),
method: methods[index],
);
},
);

User Experienceโ€‹

// โœ… DO: Show loading indicators
if (_isLoading)
const CircularProgressIndicator()
else
const Text('Pay Now');

// โœ… DO: Provide feedback
void handleSuccess(Token token) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Payment successful!'),
backgroundColor: Colors.green,
),
);
}

// โœ… DO: Handle errors gracefully
void handleError(String error) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Error'),
content: Text(error),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
}

Testingโ€‹

Unit Testingโ€‹

import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

class MockOmise extends Mock implements Omise {}

void main() {
group('Payment Tests', () {
late MockOmise mockOmise;

setUp(() {
mockOmise = MockOmise();
});

test('createToken returns token on success', () async {
// Arrange
final expectedToken = Token(id: 'tokn_test_123');
when(mockOmise.createToken(
name: anyNamed('name'),
number: anyNamed('number'),
expirationMonth: anyNamed('expirationMonth'),
expirationYear: anyNamed('expirationYear'),
securityCode: anyNamed('securityCode'),
)).thenAnswer((_) async => expectedToken);

// Act
final token = await mockOmise.createToken(
name: 'John Doe',
number: '4242424242424242',
expirationMonth: 12,
expirationYear: 2025,
securityCode: '123',
);

// Assert
expect(token.id, equals('tokn_test_123'));
});

test('createToken throws error on invalid card', () async {
// Arrange
when(mockOmise.createToken(
name: anyNamed('name'),
number: anyNamed('number'),
expirationMonth: anyNamed('expirationMonth'),
expirationYear: anyNamed('expirationYear'),
securityCode: anyNamed('securityCode'),
)).thenThrow(OmiseException('invalid_card', 'Invalid card number'));

// Act & Assert
expect(
() => mockOmise.createToken(
name: 'John Doe',
number: '1234567890123456',
expirationMonth: 12,
expirationYear: 2025,
securityCode: '123',
),
throwsA(isA<OmiseException>()),
);
});
});
}

Widget Testingโ€‹

import 'package:flutter_test/flutter_test.dart';

void main() {
testWidgets('CreditCardForm validates input', (tester) async {
// Build widget
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CreditCardFormWidget(
onTokenCreated: (_) {},
onError: (_) {},
),
),
),
);

// Enter invalid card number
await tester.enterText(
find.byType(TextFormField).first,
'1234',
);

// Tap submit button
await tester.tap(find.text('Pay Now'));
await tester.pump();

// Verify error message is shown
expect(find.text('Invalid card number'), findsOneWidget);
});
}

Integration Testingโ€‹

import 'package:integration_test/integration_test.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

testWidgets('Complete payment flow', (tester) async {
// Launch app
await tester.pumpWidget(MyApp());

// Navigate to payment screen
await tester.tap(find.text('Pay Now'));
await tester.pumpAndSettle();

// Enter card details
await tester.enterText(
find.byKey(const Key('cardNumber')),
'4242424242424242',
);
await tester.enterText(
find.byKey(const Key('expiry')),
'12/25',
);
await tester.enterText(
find.byKey(const Key('cvv')),
'123',
);
await tester.enterText(
find.byKey(const Key('name')),
'John Doe',
);

// Submit
await tester.tap(find.text('Submit'));
await tester.pumpAndSettle();

// Verify success
expect(find.text('Payment Successful'), findsOneWidget);
});
}

Troubleshootingโ€‹

Common Issuesโ€‹

Issue: "MissingPluginException"

# Solution: Rebuild the app
flutter clean
flutter pub get
flutter run

Issue: iOS build fails

# Solution: Update CocoaPods
cd ios
pod install --repo-update
cd ..
flutter run

Issue: Android build fails with "Minimum SDK version"

// Solution: Update android/app/build.gradle
android {
defaultConfig {
minSdkVersion 21 // Changed from lower version
}
}

Issue: Token creation returns null

// Solution: Check your public key and network connection
final omise = Omise(
publicKey: 'pkey_test_5xyzyx5xyzyx5xyzyx5',
debugMode: true, // Enable debug logging
);

Issue: Hot reload not working with forms

// Solution: Use unique keys for StatefulWidgets
class CreditCardForm extends StatefulWidget {
const CreditCardForm({Key? key}) : super(key: key);
// ...
}

Frequently Asked Questionsโ€‹

Can I use this SDK with GetX/Bloc/MobX?

Yes, the SDK is state management agnostic. You can use it with any state management solution by wrapping the API calls in your chosen pattern.

Does the SDK support web and desktop platforms?

Currently, the SDK focuses on mobile platforms (iOS and Android). For web, consider using Omise.js. For desktop, you can use the Dart SDK.

Can I customize the error messages?

Yes, you can catch exceptions and display custom messages to your users:

try {
await createToken();
} on OmiseException catch (e) {
showCustomError(e.code);
}

How do I handle app lifecycle events?

Use WidgetsBindingObserver to handle app lifecycle:

class _PaymentScreenState extends State<PaymentScreen> 
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}

@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
// Check payment status
verifyPaymentStatus();
}
}
}

Can I save card details for future use?

The SDK creates tokens that you send to your server. Use the Customers API on your backend to attach tokens to customers for future charges.

Does the SDK work offline?

No, the SDK requires network connectivity to create tokens and sources. Consider implementing queue mechanisms for offline scenarios.

How do I migrate from native iOS/Android SDKs?

The Flutter SDK provides similar APIs. Replace platform-specific code with Flutter widgets and use the unified Dart API.

Can I use this with Flutter Web?

The SDK is designed for mobile platforms. For web applications, use Omise.js instead.

What's the bundle size impact?

The SDK adds approximately 500KB to your app size (compressed). This may vary based on your target platforms and optimization settings.

How do I enable debug logging?

final omise = Omise(
publicKey: 'pkey_test_...',
debugMode: kDebugMode,
);

Next Stepsโ€‹

  1. Set up your account to get your API keys
  2. Integrate on your server to create charges from tokens
  3. Test your integration with test cards
  4. Go live with production keys

Supportโ€‹

Need help with the Flutter SDK?