show static method
Future<void>
show({
- required BuildContext context,
- required SmartCheckoutConfig config,
- required SmartCheckoutCallbacks callbacks,
Implementation
static Future<void> show({
required BuildContext context,
required SmartCheckoutConfig config,
required SmartCheckoutCallbacks callbacks,
}) async {
await _closeActiveSheet(context, suppressEvent: true);
final (url, navigationMode) =
await (
_getUrl(config),
AndroidNavigationSettings().getNavigationMode(),
).wait;
debugPrint('🚀 Starting BemobiCheckout...');
debugPrint('🔗 URL: $url');
final keyboardResizeNotifier = ValueNotifier(true);
final heightNotifier = ValueNotifier(
ScreenMetrics.current.height * config.initialHeightFraction,
);
debugPrint('📡 Configuring JavaScript channel...');
late final WebViewController controller;
void handleMessage(JavaScriptMessage message) async {
debugPrint('📥 Received message from WebView: ${message.message}');
try {
try {
final Map<String, dynamic> data = jsonDecode(message.message);
debugPrint('🎯 Event received as JSON: ${data['method']}');
switch (data['method']) {
case 'isReadyToPay':
debugPrint('📏 Entered isReadyToPay');
final Map<String, dynamic> request =
data['request'] as Map<String, dynamic>;
final String callback =
data['callback']?.toString() ?? 'isReadyToPayResult';
try {
final wallets = await wallet.checkAvailability(
request,
config.environment,
);
debugPrint('📏 wallets: $wallets');
await controller.runJavaScript('''
if (typeof window.$callback === 'function') {
window.$callback(${jsonEncode(wallets)});
}
''');
} catch (e) {
debugPrint('❌ Error checking wallet availability: $e');
await controller.runJavaScript('''
if (typeof window.$callback === 'function') {
window.$callback(${jsonEncode({'applePay': false, 'googlePay': false})});
}
''');
}
break;
case 'customAction':
debugPrint('📱 Custom action received: ${data['payload']}');
final payload = data['payload'];
final String callback =
data['callback']?.toString() ?? 'customActionResult';
try {
// Calls the registered handler if it exists
if (action.handler != null) {
action.handler!(payload);
}
final result = {'status': 'success', 'data': payload};
await controller.runJavaScript('''
if (typeof window.$callback === 'function') {
window.$callback(${jsonEncode(result)});
}
''');
} catch (e) {
debugPrint('❌ Error in custom action: $e');
await controller.runJavaScript('''
if (typeof window.$callback === 'function') {
window.$callback(${jsonEncode({'error': e.toString()})});
}
''');
}
break;
case 'openExternalLink':
final map = Map<String, dynamic>.from(data['data']);
final link = map['url'] as String;
final useCustomTabs = map['useCustomTabs'] as bool;
debugPrint(
'🔗 Opening external link: $link (${useCustomTabs ? 'CustomTabs' : 'Browser'})',
);
await launchUrl(
Uri.parse(link),
customTabsOptions:
useCustomTabs ? const CustomTabsOptions() : null,
safariVCOptions:
useCustomTabs ? const SafariViewControllerOptions() : null,
);
break;
case 'toggleKeyboardResize':
final enabled = data['enabled'] as bool;
debugPrint(
'⌨️ Toggling keyboard resize: ${enabled ? 'enabled' : 'disabled'}',
);
keyboardResizeNotifier.value = enabled;
break;
case 'resizeBottomSheet':
debugPrint('📏 Resizing bottom sheet with size: ${data['size']}');
final String size = data['size'].toString();
if (size.endsWith('px')) {
final pixels = double.parse(size.replaceAll('px', ''));
debugPrint('↕️ Setting height to $pixels pixels');
heightNotifier.value = pixels;
} else {
final percentage =
size.endsWith('%')
? double.parse(size.replaceAll('%', '')) / 100
: double.parse(size);
debugPrint(
'↕️ Setting height to $percentage% of screen height',
);
heightNotifier.value =
ScreenMetrics.current.height * percentage;
}
break;
case 'hideBottomSheet':
debugPrint('🔽 Closing bottom sheet');
_closeActiveSheet(context);
break;
case 'checkoutSuccess':
debugPrint('✅ Checkout successful');
callbacks.onSuccess?.call(data['data'] as Map<String, dynamic>);
_closeActiveSheet(
context,
delay: const Duration(milliseconds: 500),
);
break;
case 'checkoutError':
debugPrint('❌ Checkout error');
callbacks.onError?.call(_normalizeError(data['error']));
_closeActiveSheet(
context,
delay: const Duration(milliseconds: 500),
);
break;
case 'loadPaymentData':
debugPrint('📏 Entered loadPaymentData ${data['request']}');
final Map<String, dynamic> request =
data['request'] as Map<String, dynamic>;
final googlePayProvider = GooglePayProvider();
final result = await googlePayProvider.loadPaymentData(
request,
config.environment,
);
final String callbackPaymentData =
data['callback']?.toString() ?? 'loadPaymentDataResult';
try {
await controller.runJavaScript('''
if (typeof window.$callbackPaymentData === 'function') {
window.$callbackPaymentData(${jsonEncode(result)});
}
''');
} catch (e) {
debugPrint('❌ Error loading payment data: $e');
await controller.runJavaScript('''
if (typeof window.$callbackPaymentData === 'function') {
window.$callbackPaymentData(${jsonEncode({'error': e.toString()})});
}
''');
}
break;
case 'startApplePay':
debugPrint('📏 Entered startApplePay ${data['request']}');
final Map<String, dynamic> request =
data['request'] as Map<String, dynamic>;
final applePayProvider = ApplePayProvider();
final result = await applePayProvider.loadPaymentData(
request,
config.environment,
);
final String callbackPaymentData =
data['callback']?.toString() ?? 'loadPaymentDataResult';
try {
await controller.runJavaScript('''
if (typeof window.$callbackPaymentData === 'function') {
window.$callbackPaymentData(${jsonEncode(result)});
}
''');
} catch (e) {
debugPrint('❌ Error loading payment data: $e');
await controller.runJavaScript('''
if (typeof window.$callbackPaymentData === 'function') {
window.$callbackPaymentData(${jsonEncode({'error': e.toString()})});
}
''');
}
break;
default:
debugPrint('⚠️ Unknown method in JSON: ${data['method']}');
break;
}
} catch (jsonError) {
debugPrint('❌ Error decoding JSON: $jsonError');
debugPrint('📝 Received as direct method: ${message.message}');
switch (message.message) {
case 'hideBottomSheet':
debugPrint('🔽 Closing bottom sheet from direct call');
_closeActiveSheet(context);
break;
default:
debugPrint('⚠️ Unknown direct method: ${message.message}');
break;
}
}
} catch (e) {
debugPrint('❌ Error processing message: $e');
debugPrint('❌ Original message: ${message.message}');
callbacks.onError?.call(_normalizeError(e));
}
}
final loadingPage = Completer<void>();
final passkeyListener = PasskeyWebListener();
void pageLoaded() {
if (!loadingPage.isCompleted) loadingPage.complete();
}
controller =
WebViewController()
..setOnConsoleMessage((message) {
final level = message.level.toString().toUpperCase();
debugPrint('[$level]: ${message.message}');
})
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.white)
..enableZoom(false)
..addJavaScriptChannel(
'BemobiNativeSDKBridge',
onMessageReceived: handleMessage,
)
..setNavigationDelegate(
NavigationDelegate(
onProgress: (progress) {
if (progress > 0) pageLoaded();
},
onPageFinished: (String url) async {
debugPrint('🌐 Page finished loading: $url');
debugPrint('💉 Injecting JavaScript bridge...');
// First, let's check if the channel already exists
final String checkResult =
await controller.runJavaScriptReturningResult('''
(function() {
if (typeof BemobiNativeSDKBridge !== 'undefined') {
return 'exists';
}
return 'not_exists';
})();
''')
as String;
debugPrint('🔍 Bridge check result: $checkResult');
await controller.runJavaScript('''
(function() {
try {
if (typeof BemobiNativeSDKBridge === 'undefined') {
console.log('Creating BemobiNativeSDKBridge...');
window.BemobiNativeSDKBridge = {};
window.BemobiNativeSDKBridge.isReadyToPay = function(request) {
console.log('Native: Checking wallet availability' + request);
window.BemobiNativeSDKBridge.postMessage(JSON.stringify({
method: 'isReadyToPay',
request: request
}));
};
window.BemobiNativeSDKBridge.hideBottomSheet = function() {
console.log('Native: Calling hideBottomSheet');
window.BemobiNativeSDKBridge.postMessage('hideBottomSheet');
return true;
};
window.BemobiNativeSDKBridge.resizeBottomSheet = function(size) {
console.log('Native: Calling resizeBottomSheet with size: ' + size);
window.BemobiNativeSDKBridge.postMessage(JSON.stringify({
method: 'resizeBottomSheet',
size: size
}));
return true;
};
// Installation verification
console.log('Bridge methods installed:');
console.log('- hideBottomSheet:', typeof window.BemobiNativeSDKBridge.hideBottomSheet);
console.log('- resizeBottomSheet:', typeof window.BemobiNativeSDKBridge.resizeBottomSheet);
console.log('- postMessage:', typeof window.BemobiNativeSDKBridge.postMessage);
// Immediate test
console.log('Testing bridge...');
window.BemobiNativeSDKBridge.postMessage('test_initialization');
}
// Exposes the bridge globally for debugging
window.testBridge = function() {
console.log('Manual bridge test...');
window.BemobiNativeSDKBridge.hideBottomSheet();
};
window.BemobiNativeSDKBridge.loadPaymentData = function(request) {
console.log('Native: Starting Google Pay with request:', request);
window.BemobiNativeSDKBridge.postMessage(JSON.stringify({
method: 'loadPaymentData',
request: request
}));
};
window.BemobiNativeSDKBridge.customAction = function(payload) {
console.log('Native: Executing custom action with payload:', payload);
window.BemobiNativeSDKBridge.postMessage(JSON.stringify({
method: 'customAction',
payload: payload
}));
};
window.BemobiNativeSDKBridge.toggleKeyboardResize = function(enabled) {
console.log('Native: Calling toggleKeyboardResize with ' + (enabled ? 'enabled' : 'disabled'));
window.BemobiNativeSDKBridge.postMessage(JSON.stringify({
method: 'toggleKeyboardResize',
enabled
}));
};
return 'Bridge initialized successfully';
} catch (e) {
console.error('Error in bridge initialization:', e);
return 'Error: ' + e.message;
}
})();
''');
// Final verification
await controller.runJavaScript('''
console.log('Final bridge verification:');
console.log(window.BemobiNativeSDKBridge);
// Registers an observer for debugging
window.addEventListener('message', function(event) {
console.log('Message received:', event.data);
});
''');
passkeyListener.injectScript(controller);
debugPrint('✅ JavaScript bridge injection completed');
},
onWebResourceError: (WebResourceError error) {
pageLoaded();
debugPrint('❌ Web resource error:');
debugPrint(' Description: ${error.description}');
debugPrint(' Error Type: ${error.errorType}');
debugPrint(' Failed URL: ${error.url}');
},
),
);
if (controller.platform is AndroidWebViewController) {
final AndroidWebViewCookieManager cookieManager =
AndroidWebViewCookieManager(
const PlatformWebViewCookieManagerCreationParams(),
);
await cookieManager.setAcceptThirdPartyCookies(
controller.platform as AndroidWebViewController,
true,
);
}
await Future.wait([
passkeyListener.registerChannel(controller),
controller.loadRequest(url),
loadingPage.future,
]);
debugPrint('📱 Showing bottom sheet...');
_activeSheets[context] = showModalBottomSheet(
context: context,
isScrollControlled: true,
isDismissible: true,
useSafeArea: false,
enableDrag: false,
backgroundColor: Colors.transparent,
builder: (context) {
return _buildContentSheet(
context: context,
config: config,
controller: controller,
keyboardResizeNotifier: keyboardResizeNotifier,
heightNotifier: heightNotifier,
navigationMode: navigationMode,
);
},
).whenComplete(() {
debugPrint(
'🏁 Bottom sheet ${_suppressCloseEvent ? 'close event suppressed' : 'closed'}',
);
keyboardResizeNotifier.dispose();
heightNotifier.dispose();
if (!_suppressCloseEvent) callbacks.onClose?.call();
_activeSheets.remove(context);
});
if (Settings.waitSheetToClose) return _activeSheets[context];
}