メインコンテンツへスキップ

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
  • サポート 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

要件

  • 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)

インストール

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" />

クイックスタート

1. Import the Package

import 'package:omise_flutter/omise_flutter.dart';

2. Initialize the Client

class Paymentサービス {
final Omise omise;

Paymentサービス()
: 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;
}
}

// 使用方法
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());
}
}

構成

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

インターネットバンキング

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;
}

// 使用方法
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,
);
}

// 使用方法
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,
);
}

// 使用方法
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 _cardNumberコントローラー = TextEditingコントローラー();
final _expiryコントローラー = TextEditingコントローラー();
final _cvvコントローラー = TextEditingコントローラー();
final _nameコントローラー = TextEditingコントローラー();
bool _isLoading = false;

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

@override
void dispose() {
_cardNumberコントローラー.dispose();
_expiryコントローラー.dispose();
_cvvコントローラー.dispose();
_nameコントローラー.dispose();
super.dispose();
}

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

setState(() => _isLoading = true);

try {
// Parse expiry date
final expiry = _expiryコントローラー.text.split('/');
final month = int.parse(expiry[0].trim());
final year = 2000 + int.parse(expiry[1].trim());

final token = await omise.createToken(
name: _nameコントローラー.text,
number: _cardNumberコントローラー.text.replaceAll(' ', ''),
expirationMonth: month,
expirationYear: year,
securityCode: _cvvコントローラー.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: _cardNumberコントローラー,
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: _expiryコントローラー,
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: _cvvコントローラー,
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: _nameコントローラー,
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: 'インターネットバンキング',
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();
}
}

// 使用方法 in TextField
class CardNumberField extends StatelessWidget {
final TextEditingコントローラー 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,
}

// 使用方法
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,
});
}

// 使用方法
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 WebViewコントローラー _controller;
bool _isLoading = true;

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

_controller = WebViewコントローラー()
..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()),
],
),
);
}
}

// 使用方法
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 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 エラーハンドリング

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');
}

ベストプラクティス

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() {
_cardNumberコントローラー.dispose();
_expiryコントローラー.dispose();
_cvvコントローラー.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'),
),
],
),
);
}

テスト

Unit テスト

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 テスト

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 テスト

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);
});
}

トラブルシューティング

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,
);

関連リソース

次のステップ

  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

サポート

Need help with the Flutter SDK?