show static method

Future<void> show({
  1. required BuildContext context,
  2. required SmartCheckoutConfig config,
  3. 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];
}