intram_sdk_flutter 2.1.0 copy "intram_sdk_flutter: ^2.1.0" to clipboard
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();
  }
}
1
likes
120
points
164
downloads

Publisher

unverified uploader

Weekly Downloads

We are accelerating the digitalization of businesses in Africa through digital solutions to sell, receive payments, issue payments and ensure better management.

Homepage
Repository (GitHub)

Documentation

API reference

License

MIT (license)

Dependencies

flutter, webview_flutter, webview_flutter_android, webview_flutter_wkwebview

More

Packages that depend on intram_sdk_flutter