intram_sdk_flutter 2.1.0
intram_sdk_flutter: ^2.1.0 copied to clipboard
We are accelerating the digitalization of businesses in Africa through digital solutions to sell, receive payments, issue payments and ensure better management.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intram_sdk_flutter/intram_sdk_flutter_webview.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
),
);
runApp(const IntramExampleApp());
}
class IntramExampleApp extends StatelessWidget {
const IntramExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Intram SDK Example',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF29b3a6)),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
cardTheme: CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
home: const IntramExamplePage(),
);
}
}
class IntramExamplePage extends StatefulWidget {
const IntramExamplePage({super.key});
@override
State<IntramExamplePage> createState() => _IntramExamplePageState();
}
class _IntramExamplePageState extends State<IntramExamplePage> {
final TextEditingController _amountController = TextEditingController(text: '1000');
final TextEditingController _publicKeyController = TextEditingController();
final TextEditingController _storeNameController = TextEditingController(text: 'Ma Boutique');
final TextEditingController _logoUrlController = TextEditingController(text: 'https://intram.org/images/logo-1.png');
bool _sandbox = true;
String _selectedColor = '#29b3a6';
String? _lastResult;
bool _isLoading = false;
final List<Map<String, String>> _colors = [
{'name': 'Teal', 'value': '#29b3a6'},
{'name': 'Blue', 'value': '#2196F3'},
{'name': 'Green', 'value': '#4CAF50'},
{'name': 'Orange', 'value': '#FF9800'},
{'name': 'Red', 'value': '#F44336'},
{'name': 'Purple', 'value': '#9C27B0'},
];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: AppBar(
title: const Text('Intram SDK Example'),
centerTitle: true,
elevation: 0,
backgroundColor: const Color(0xFF29b3a6),
foregroundColor: Colors.white,
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildInfoBanner(),
const SizedBox(height: 16),
_buildConfigSection(),
const SizedBox(height: 16),
_buildPayButton(),
const SizedBox(height: 16),
if (_lastResult != null) _buildResultSection(),
],
),
),
),
);
}
Widget _buildInfoBanner() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.blue.shade400, Colors.blue.shade600]),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.blue.withValues(alpha: 0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.info_outline, color: Colors.white, size: 24),
),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Numéros de test Intram',
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white, fontSize: 16),
),
),
],
),
const Divider(color: Colors.white54, height: 24),
_buildTestNumber('MTN Mobile Money', '61000000'),
const SizedBox(height: 8),
_buildTestNumber('Moov Money', '94000000'),
const Divider(color: Colors.white54, height: 24),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _sandbox
? Colors.orange.withValues(alpha: 0.3)
: Colors.red.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: _sandbox ? Colors.orange : Colors.red, width: 2),
),
child: Row(
children: [
Icon(_sandbox ? Icons.science : Icons.warning, color: Colors.white),
const SizedBox(width: 12),
Expanded(
child: Text(
_sandbox
? 'Mode Sandbox (Test) - Aucun vrai paiement'
: 'Mode Production - Vrais paiements !',
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
),
),
],
),
),
],
),
);
}
Widget _buildTestNumber(String operator, String number) {
return Row(
children: [
const Text('🇧🇯', style: TextStyle(fontSize: 20)),
const SizedBox(width: 8),
Expanded(child: Text(operator, style: const TextStyle(color: Colors.white, fontSize: 14))),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(20),
),
child: Text(
number,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontFamily: 'monospace'),
),
),
],
);
}
Widget _buildConfigSection() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.settings, color: Colors.grey.shade700),
const SizedBox(width: 8),
const Text('Configuration', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 16),
TextField(
controller: _publicKeyController,
decoration: const InputDecoration(
labelText: 'Clé publique Intram *',
hintText: '6a695a2def2ba8e68c773a260f95c0a0...',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.key),
helperText: 'Récupérez-la sur app.intram.org',
helperMaxLines: 2,
),
maxLines: 2,
),
const SizedBox(height: 16),
TextField(
controller: _storeNameController,
decoration: const InputDecoration(
labelText: 'Nom du magasin',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.store),
),
),
const SizedBox(height: 16),
TextField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Montant (XOF)',
hintText: '1000',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.attach_money),
suffixText: 'XOF',
),
),
const SizedBox(height: 16),
TextField(
controller: _logoUrlController,
decoration: const InputDecoration(
labelText: 'URL du logo',
hintText: 'https://example.com/logo.png',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.image),
),
),
const SizedBox(height: 16),
const Text('Couleur du thème', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: _colors.map((color) {
final isSelected = _selectedColor == color['value'];
return InkWell(
onTap: () => setState(() => _selectedColor = color['value']!),
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Color(int.parse(color['value']!.replaceAll('#', 'FF'), radix: 16)),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: isSelected ? Colors.black : Colors.transparent, width: 3),
boxShadow: isSelected
? [BoxShadow(color: Colors.black.withValues(alpha: 0.3), blurRadius: 8, offset: const Offset(0, 2))]
: null,
),
child: isSelected ? const Icon(Icons.check, color: Colors.white) : null,
),
);
}).toList(),
),
const SizedBox(height: 16),
Container(
decoration: BoxDecoration(
color: _sandbox
? Colors.orange.withValues(alpha: 0.1)
: Colors.red.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: _sandbox ? Colors.orange : Colors.red),
),
child: SwitchListTile(
title: const Text('Mode Sandbox (Test)', style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(
_sandbox ? 'Utilisez les numéros de test ci-dessus' : 'ATTENTION: Vrais paiements !',
style: TextStyle(color: _sandbox ? Colors.orange.shade700 : Colors.red),
),
value: _sandbox,
activeColor: Colors.orange,
onChanged: (value) {
setState(() => _sandbox = value);
if (!value) _showProductionWarning();
},
),
),
],
),
),
);
}
Widget _buildPayButton() {
return ElevatedButton(
onPressed: _isLoading ? null : _makePayment,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF29b3a6),
foregroundColor: Colors.white,
padding: const EdgeInsets.all(16),
elevation: 2,
),
child: _isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2, valueColor: AlwaysStoppedAnimation<Color>(Colors.white)),
)
: const Text('Lancer le paiement', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
);
}
Widget _buildResultSection() {
final isSuccess = _lastResult!.contains('RÉUSSI');
final isCancelled = _lastResult!.contains('ANNULÉ');
return Card(
color: isSuccess
? Colors.green.shade50
: isCancelled
? Colors.orange.shade50
: Colors.red.shade50,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
isSuccess ? Icons.check_circle : isCancelled ? Icons.warning : Icons.error,
color: isSuccess ? Colors.green : isCancelled ? Colors.orange : Colors.red,
),
const SizedBox(width: 8),
const Expanded(
child: Text('Résultat', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
IconButton(icon: const Icon(Icons.close), onPressed: () => setState(() => _lastResult = null)),
],
),
const Divider(),
Text(_lastResult!, style: const TextStyle(fontSize: 14)),
],
),
),
);
}
Future<void> _makePayment() async {
if (_publicKeyController.text.isEmpty) {
_showError('Veuillez entrer votre clé publique Intram');
return;
}
final amount = int.tryParse(_amountController.text);
if (amount == null || amount <= 0) {
_showError('Montant invalide');
return;
}
setState(() => _isLoading = true);
final intramSdk = IntramSdkPayment(
_publicKeyController.text,
'', '', '', false, {},
);
final result = await intramSdk.makePayment(
context,
amount,
_sandbox,
_storeNameController.text,
_selectedColor,
_logoUrlController.text,
);
setState(() => _isLoading = false);
_handleResult(result);
}
void _handleResult(Map<String, dynamic> result) {
String message;
if (result['success'] == true) {
message = 'PAIEMENT RÉUSSI !\n\n'
'Transaction ID: ${result['transaction_id'] ?? 'N/A'}\n'
'Timestamp: ${result['timestamp'] ?? 'N/A'}\n'
'Mode: ${_sandbox ? 'Sandbox (Test)' : 'Production'}\n\n'
'Données: ${result['data']?.toString() ?? 'N/A'}';
} else if (result['cancelled'] == true) {
message = 'PAIEMENT ANNULÉ\n\nL\'utilisateur a fermé la fenêtre sans effectuer le paiement.';
} else {
message = 'PAIEMENT ÉCHOUÉ\n\n'
'Erreur: ${result['error'] ?? 'Erreur inconnue'}\n\n'
'Vérifiez:\n'
'- Votre clé publique est valide\n'
'- Vous êtes connecté à Internet\n'
'- Le mode Sandbox est activé pour les tests';
}
setState(() => _lastResult = message);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
result['success'] == true
? 'Paiement réussi !'
: result['cancelled'] == true
? 'Paiement annulé'
: 'Paiement échoué',
),
backgroundColor: result['success'] == true
? Colors.green
: result['cancelled'] == true
? Colors.orange
: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
void _showError(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.red, duration: const Duration(seconds: 3)),
);
}
void _showProductionWarning() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Row(children: [Icon(Icons.warning, color: Colors.red), SizedBox(width: 8), Text('Mode Production')]),
content: const Text(
'Attention ! Vous allez passer en mode PRODUCTION.\n\n'
'Les paiements effectués seront RÉELS.\n\n'
'Êtes-vous sûr de vouloir continuer ?',
),
actions: [
TextButton(
onPressed: () {
setState(() => _sandbox = true);
Navigator.of(context).pop();
},
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red, foregroundColor: Colors.white),
child: const Text('Confirmer'),
),
],
),
);
}
@override
void dispose() {
_amountController.dispose();
_publicKeyController.dispose();
_storeNameController.dispose();
_logoUrlController.dispose();
super.dispose();
}
}