bemobi_smartcheckout_flutter 0.1.6
bemobi_smartcheckout_flutter: ^0.1.6 copied to clipboard
Flutter SDK for Bemobi Smart Checkout: digital wallets, callbacks, customization, and deep link integration.
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
BemobiSDKfoi substituída pela nova API unificadaSmartCheckoutintroduzida na versão0.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
- Instalação
- Carteiras digitais
- Configuração
- Uso básico
- Customização visual
- Ações customizadas
- Conversão de string para enums
- Integração com deep link
- Exemplos completos
- Informações adicionais
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 #
- Adicione a dependência no seu
pubspec.yaml:
dependencies:
bemobi_smartcheckout_flutter: ^0.1.6
- 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.
Integração com deep link #
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)
Exemplo de uso do deep link #
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 #
- Saiba mais sobre o Smart Checkout e soluções de negócio em: https://bemobi.com/pt/smart-checkout-pt/