bemobi_smartcheckout_flutter 0.1.6 copy "bemobi_smartcheckout_flutter: ^0.1.6" to clipboard
bemobi_smartcheckout_flutter: ^0.1.6 copied to clipboard

Flutter SDK for Bemobi Smart Checkout: digital wallets, callbacks, customization, and deep link integration.

Bemobi Logo

Bemobi Smart Checkout Flutter SDK #

SDK Flutter para integração com o Smart Checkout, oferecendo uma solução robusta e flexível para pagamentos digitais, incluindo suporte a carteiras digitais, callbacks, customização visual e integração via deep link.

⚠️ Aviso de retrocompatibilidade

A classe BemobiSDK foi substituída pela nova API unificada SmartCheckout introduzida na versão 0.1.0.

Por enquanto está mantida a retrocompatibilidade para integrações que utilizam a API anterior, mas recomenda-se migrar o quanto antes para a nova implementação.


Sumário #


Recursos #

  • Callbacks para sucesso, erro e fechamento.
  • Compatível com Apple Pay e Google Pay.
  • Personalização visual do bottom sheet.
  • Suporte a deep link para Android e iOS.

Instalação #

  1. Adicione a dependência no seu pubspec.yaml:
dependencies:
  bemobi_smartcheckout_flutter: ^0.1.6
  1. Execute:
flutter pub get

Carteiras digitais #

Configuração das carteiras digitais #

iOS (Apple Pay)

No arquivo ios/Runner/Runner.entitlements:

<key>com.apple.developer.in-app-payments</key>
<array>
  <string>merchant.seu.identificador</string>
</array>

Android (Google Pay)

No arquivo android/app/src/main/AndroidManifest.xml:

<meta-data
  android:name="com.google.android.gms.wallet.api.enabled"
  android:value="true" />

Configuração #

Parâmetros de configuração #

A configuração da sessão de checkout é feita via SmartCheckout.config:

final config = SmartCheckout.config(
  environment: SmartCheckout.environment.production, // or .sandbox, .development
  sessionCode: 'a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d', // optional
  partnerKey: 'abc-1234', // optional ([a-z]{3}-[0-9]{4})
  deepLink: Uri.parse('https://smart-checkout.bemobi.com/callback/abc-1234'), // optional
  initialHeightFraction: 0.8, // optional (0.0 to 1.0)
  style: SmartCheckout.style.rounded, // or .flat (optional)
);

Os ambientes disponíveis são:

  • SmartCheckout.environment.sandbox
  • SmartCheckout.environment.development
  • SmartCheckout.environment.production

Os estilos disponíveis são:

  • SmartCheckout.style.rounded
  • SmartCheckout.style.flat

Callbacks #

Você pode definir handlers para os principais eventos do fluxo de checkout:

  • onSuccess(Map<String, dynamic> result): chamado quando o pagamento é concluído com sucesso.
  • onError(Map<String, dynamic> error): chamado em caso de erro.
  • onClose(): chamado quando o bottom sheet é fechado.

Exemplo:

final callbacks = SmartCheckout.callbacks(
  onSuccess: (result) => debugPrint('Checkout completed successfully: $result'),
  onError: (error) => debugPrint('Checkout error: $error'),
  onClose: () => debugPrint('Checkout closed'),
);

Uso básico #

Para abrir o checkout em um modal bottom sheet:

SmartCheckout.showBottomSheet(
  context: context,
  config: config,
  callbacks: callbacks,
);

Customização visual #

O visual do bottom sheet pode ser customizado via o parâmetro style:

  • SmartCheckout.style.rounded: bordas arredondadas (padrão).
  • SmartCheckout.style.flat: bordas retas.

Você também pode ajustar a altura inicial do bottom sheet com initialHeightFraction (valor entre 0.0 e 1.0).


Ações customizadas #

Você pode registrar um handler para ações customizadas disparadas pelo fluxo do checkout:

SmartCheckout.setCustomActionHandler((payload) {
  debugPrint('Custom action received: $payload');
});

Conversão de string para enums #

Se você precisa converter uma string em algum dos enums utilizados, use os métodos utilitários estáticos:

final environment = SmartCheckout.parseEnvironmentFrom('production'); // SmartCheckout.environment.production
final style = SmartCheckout.parseStyleFrom('rounded'); // SmartCheckout.style.rounded

Se o valor informado for inválido, será lançado um ArgumentError.


Android #

No arquivo android/app/src/main/AndroidManifest.xml, adicione o seguinte intent-filter na sua Activity principal (substitua abc-1234 pelo seu identificador do parceiro):

<intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.BROWSABLE" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:scheme="http" />
    <data android:scheme="https" />
    <data android:host="smart-checkout.bemobi.com" />
    <data android:path="/callback/abc-1234" />
</intent-filter>

Importante: Para o deep link funcionar corretamente com o domínio smart-checkout.bemobi.com, informe ao suporte:

  • Application ID
  • SHA-256 Certificate Fingerprint

iOS #

No Xcode, adicione ao arquivo ios/Runner/Runner.entitlements:

<key>com.apple.developer.associated-domains</key>
<array>
    <string>applinks:smart-checkout.bemobi.com</string>
    <string>webcredentials:smart-checkout.bemobi.com</string>
</array>

Importante: Para o deep link funcionar corretamente com o domínio smart-checkout.bemobi.com, informe ao suporte:

  • App ID (Team ID + Bundle ID)

Utilizando a biblioteca app_links:

class _MyHomePageState extends State<MyHomePage> {
  StreamSubscription<Uri>? _deepLinkListener;

  @override
  void initState() {
    super.initState();
    _deepLinkListener = AppLinks().uriLinkStream.listen((uri) {
      // Example opening the checkout from the deep link
      final config = SmartCheckout.config(
        environment: SmartCheckout.environment.production,
        deepLink: uri,
      );
      SmartCheckout.showBottomSheet(
        context: context,
        config: config,
        callbacks: callbacks,
      );
    });
  }

  @override
  void dispose() {
    _deepLinkListener?.cancel();
    super.dispose();
  }
}

A estrutura do deep link segue o padrão:

https://smart-checkout.bemobi.com/callback/{partnerKey}

Onde {partnerKey} é o identificador do parceiro no formato [a-z]{3}-[0-9]{4}:

https://smart-checkout.bemobi.com/callback/abc-1234

Exemplos completos #

Exibindo o checkout diretamente #

import 'package:bemobi_smartcheckout_flutter/bemobi_smartcheckout_flutter.dart';
import 'package:flutter/widgets.dart';

void openCheckout(BuildContext context) {
  final config = SmartCheckout.config(
    environment: SmartCheckout.environment.production,
    sessionCode: 'a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d',
    partnerKey: 'abc-1234',
    deepLink: Uri.parse('https://smart-checkout.bemobi.com/callback/abc-1234'),
    initialHeightFraction: 0.9,
    style: SmartCheckout.style.rounded,
  );

  final callbacks = SmartCheckout.callbacks(
    onSuccess: (result) => debugPrint('Checkout completed successfully: $result'),
    onError: (error) => debugPrint('Checkout error: $error'),
    onClose: () => debugPrint('Checkout closed'),
  );

  SmartCheckout.setCustomActionHandler((payload) {
    debugPrint('Custom action received: $payload');
  });

  SmartCheckout.showBottomSheet(
    context: context,
    config: config,
    callbacks: callbacks,
  );
}

Chamando o checkout via WebView (bridge) #

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:bemobi_smartcheckout_flutter/bemobi_smartcheckout_flutter.dart';
import 'dart:convert';

class WebViewScreen extends StatefulWidget {
  final String sessionCode; // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  final String environment; // sandbox, development or production

  const WebViewScreen({
    super.key,
    required this.sessionCode,
    required this.environment,
  });

  @override
  State<WebViewScreen> createState() => _WebViewScreenState();
}

class _WebViewScreenState extends State<WebViewScreen> {
  late final WebViewController _controller;

  void _postMessage(Map<String, dynamic> message) {
    final script = 'window.postMessage(${jsonEncode(message)}, "*");';
    _controller.runJavaScript(script);
  }

  String _generateHtml() {
    return '''
      <!DOCTYPE html>
      <html>

      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
        <style>
          :root {
            --white: #ffffff;
            --blue-start: #2e7cf6;
            --blue-end: #1f63e5;
            --blue-shadow: rgba(31, 99, 229, 0.28);
            --gray-very-light: #f8fafc;
            --gray-light: #e5e7eb;
            --gray-divider: #f1f5f9;
            --gray-very-dark: #111827;
            --slate-deep: #0f172a;
            --slate-muted: #64748b;
            --shadow-soft: rgba(0, 0, 0, .04);
            --green-deep: #166534;
            --green-wash: #ecfdf5;
            --green-border: #bbf7d0;
            --red-deep: #7f1d1d;
            --red-wash: #fef2f2;
            --red-border: #fecaca;
            --indigo-deep: #1e3a8a;
            --indigo-wash: #eff6ff;
            --indigo-border: #bfdbfe;
          }

          body {
            display: flex;
            margin: max(24px, env(safe-area-inset-top, 24px)) 24px max(24px, env(safe-area-inset-bottom, 24px));
            gap: 24px;
            font-family: ui-monospace, monospace;
            font-size: 14px;
            flex-direction: column;
          }

          button {
            appearance: none;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            padding: 14px 20px;
            min-width: 220px;
            font-size: 16px;
            font-weight: 700;
            letter-spacing: .2px;
            line-height: 1;
            color: var(--white);
            background: linear-gradient(180deg, var(--blue-start) 0%, var(--blue-end) 100%);
            border: none;
            border-radius: 12px;
            box-shadow: 0 6px 16px var(--blue-shadow);
            user-select: none;
            transition: transform .04s ease, filter .12s ease;
          }

          button:active {
            transform: translateY(1px) scale(0.99);
            filter: brightness(96%);
          }

          pre {
            margin: 6px 0 0;
            padding: 8px;
            background: var(--gray-very-light);
            border: 1px solid var(--gray-light);
            border-radius: 6px;
            overflow: auto;
            font-size: 12px;
            line-height: 1.5;
          }

          #log {
            padding: 12px;
            border-radius: 6px;
            max-height: 60vh;
            overflow: auto;
            background: var(--white);
            color: var(--gray-very-dark);
            border: 1px solid var(--gray-light);
            box-shadow: 0 2px 10px var(--shadow-soft);
          }

          #log #empty {
            padding: 16px;
            text-align: center;
            color: var(--slate-muted);
            background: var(--gray-very-light);
            border: 1px dashed var(--gray-light);
            border-radius: 8px;
            margin: 8px;
          }

          #log .item {
            border-bottom: 1px solid var(--gray-divider);
            padding: 10px 6px;
          }

          #log .item:last-child {
            border-bottom: 0;
          }

          #log .head {
            display: flex;
            gap: 8px;
            align-items: baseline;
            margin-bottom: 6px;
          }

          #log .time {
            color: var(--slate-muted);
            font-size: 12px;
          }

          #log .message {
            white-space: pre-wrap;
            margin-left: 2px;
          }

          .badge {
            font-size: 11px;
            font-weight: 700;
            padding: 2px 8px;
            border-radius: 999px;
            border: 1px solid var(--gray-light);
            background: var(--gray-very-light);
            color: var(--slate-deep);
          }

          .success .badge {
            color: var(--green-deep);
            background: var(--green-wash);
            border-color: var(--green-border);
          }

          .error .badge {
            color: var(--red-deep);
            background: var(--red-wash);
            border-color: var(--red-border);
          }

          .info .badge {
            color: var(--indigo-deep);
            background: var(--indigo-wash);
            border-color: var(--indigo-border);
          }

          .meta {
            display: flex;
            flex-direction: column;
            gap: 4px;
            color: var(--slate-deep);
          }

          .meta .label {
            font-weight: 700;
            text-transform: uppercase;
          }

          .meta .value {
            word-break: break-all;
          }
        </style>
      </head>

      <body>
        <div class="meta">
          <div class="label">Session Code</div>
          <div class="value">${widget.sessionCode}</div>
        </div>

        <div class="meta">
          <div class="label">Environment</div>
          <div class="value">${widget.environment}</div>
        </div>

        <button onclick="openSmartCheckout()">Open Smart Checkout SDK</button>

        <div id="log">
          <div id="empty">
            No events yet. Trigger a checkout to see logs here.
          </div>
        </div>

        <script>
          Object.defineProperty(window, 'smartCheckoutLoaded', {
            get() {
              const bridge = window.startPaymentFlow;
              return !!bridge && typeof bridge.postMessage === 'function';
            },
          });

          const EVENT_LOG_MAP = {
            'checkout:action': { level: 'info', message: 'Custom action' },
            'checkout:success': { level: 'success', message: 'Payment completed successfully' },
            'checkout:error': { level: 'error', message: 'Payment error' },
            'checkout:close': { level: 'info', message: 'Checkout closed' },
          };

          function appendLog({ level, message, payload }) {
            const empty = document.getElementById('empty');
            if (empty) empty.remove();

            const item = document.createElement('div');
            item.className = ['item', level].join(' ');

            const head = document.createElement('div');
            head.className = 'head';

            const badge = document.createElement('span');
            badge.className = 'badge';
            badge.textContent = level.replace(/^\\w/, char => char.toUpperCase());

            const time = document.createElement('span');
            time.className = 'time';
            time.textContent = new Date().toLocaleTimeString();

            const text = document.createElement('div');
            text.className = 'message';
            text.textContent = message;

            head.appendChild(badge);
            head.appendChild(time);
            item.appendChild(head);
            item.appendChild(text);

            if (payload) {
              const pre = document.createElement('pre');
              pre.textContent = JSON.stringify(payload, null, 2);
              item.appendChild(pre);
            }

            const log = document.getElementById('log');
            log.prepend(item);
            log.scrollTop = 0;
          }

          function openSmartCheckout() {
            if (window.smartCheckoutLoaded) {
              const paymentData = {
                sessionCode: "${widget.sessionCode}",
                environment: "${widget.environment}"
              };

              window.startPaymentFlow.postMessage(JSON.stringify(paymentData));
            } else {
              appendLog({ level: 'error', message: 'Smart Checkout bridge is not ready' });
            }
          }

          window.addEventListener('message', (event) => {
            const { type, payload } = event.data;
            const log = EVENT_LOG_MAP[type];
            if (log) appendLog({ ...log, payload });
          });
        </script>
      </body>

      </html>
    ''';
  }

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

    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..addJavaScriptChannel(
        'startPaymentFlow',
        onMessageReceived: (JavaScriptMessage message) {
          final jsonObject = jsonDecode(message.message);
          final sessionCode = jsonObject['sessionCode'];
          final environment = jsonObject['environment'];

          SmartCheckout.setCustomActionHandler((payload) {
            debugPrint('Custom action received: $payload');
            _postMessage({'type': 'checkout:action', 'payload': payload});
          });

          SmartCheckout.showBottomSheet(
            context: context,
            config: SmartCheckout.config(
              sessionCode: sessionCode,
              environment: SmartCheckout.parseEnvironmentFrom(environment),
              initialHeightFraction: 0.9,
            ),
            callbacks: SmartCheckout.callbacks(
              onSuccess: (result) {
                debugPrint('Checkout completed successfully: $result');
                _postMessage({'type': 'checkout:success', 'payload': result});
              },
              onError: (error) {
                debugPrint('Checkout error: $error');
                _postMessage({'type': 'checkout:error', 'payload': error});
              },
              onClose: () {
                debugPrint('Checkout closed');
                _postMessage({'type': 'checkout:close'});
              },
            ),
          );
        },
      )
      ..loadHtmlString(_generateHtml());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('WebView Demo')),
      body: WebViewWidget(controller: _controller),
    );
  }
}

Informações adicionais #

0
likes
130
points
731
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter SDK for Bemobi Smart Checkout: digital wallets, callbacks, customization, and deep link integration.

Homepage

Documentation

API reference

License

MIT (license)

Dependencies

android_navigation_settings, device_info_plus, flutter, flutter_credential_manager, flutter_custom_tabs, pay, unique_identifier, webview_flutter, webview_flutter_android

More

Packages that depend on bemobi_smartcheckout_flutter