applePay static method

Future<void> applePay({
  1. required PaymentRequest paymentRequest,
  2. required void onApplePayResult(
    1. Map<String, dynamic> onApplePayResult
    ),
  3. void onApplePayError(
    1. Object error
    )?,
})

Triggers a headless Google Pay payment using the PayOrc SDK.

This does not require a widget or button. It builds the SDK-backed Google Pay configuration from checkout customization and launches the native Google Pay selector on Android.

The merchant receives the raw Google Pay result, not final order success.

Implementation

static Future<void> applePay({
  required PaymentRequest paymentRequest,
  required void Function(Map<String, dynamic> onApplePayResult) onApplePayResult,
  void Function(Object error)? onApplePayError,
}) async {
  if (!Platform.isIOS) {
    final error = UnsupportedError('Apple Pay is only supported on iOS.');
    if (onApplePayError != null) {
      onApplePayError(error);
      return;
    }
    throw error;
  }

  final json = await _applePayJsonFromSdk(paymentRequest);
  if (json == null || json.trim().isEmpty) {
    final error = StateError(
      'Apple Pay configuration is unavailable for this payment request.',
    );
    if (onApplePayError != null) {
      onApplePayError(error);
      return;
    }
    throw error;
  }

  final config = PaymentConfiguration.fromJsonString(json);
  final items = paymentItemsFromRequest(paymentRequest);
  final payClient = Pay({PayProvider.apple_pay: config});
  Object? applePayResult;

  try {
    final result = await payClient.showPaymentSelector(
      PayProvider.apple_pay,
      items,
    );
    applePayResult = result;
    if (kDebugMode) {
      debugPrint('Apple Pay raw result: $result');
    }

    await _submitApplePayDiagnostics(
      applePayConfiguration: "Apple pay config: ${json}",
      error: null,
    );

    await _submitApplePayDiagnostics(
      applePayConfiguration: applePayResult,
      error: null,
    );

    // Quick pre-parse validation: ensure the native result contains a
    // token-like value and that `paymentData.data` or the raw token string
    // is not empty. This avoids calling the backend with an empty token.
    try {
      if (result is Map<String, dynamic>) {
        dynamic tokenValue = result['token'];
        if (tokenValue == null && result['paymentMethodData'] is Map) {
          final pm = result['paymentMethodData'] as Map<String, dynamic>;
          if (pm['tokenizationData'] is Map) {
            tokenValue = (pm['tokenizationData'] as Map<String, dynamic>)['token'];
          }
        }

        bool tokenEmpty = false;
        if (tokenValue == null) {
          tokenEmpty = true;
        } else if (tokenValue is String) {
          tokenEmpty = tokenValue.trim().isEmpty;
        } else if (tokenValue is Map<String, dynamic>) {
          // Check common shapes: { 'paymentData': { 'data': '...' } } or
          // { 'data': '...' }.
          final pd = tokenValue['paymentData'];
          if (pd is Map<String, dynamic>) {
            tokenEmpty = (pd['data']?.toString().trim().isEmpty ?? true);
          } else {
            tokenEmpty = (tokenValue['data']?.toString().trim().isEmpty ?? true);
          }
        }

        if (tokenEmpty) {
          final err = StateError('Apple Pay native result contains empty wallet token');
          await _submitApplePayDiagnostics(
            applePayConfiguration: applePayResult,
            error: err,
          );
          if (onApplePayError != null) {
            onApplePayError(err);
            return;
          }
          throw err;
        }
      }
    } catch (_) {
      // Fall through to parsing; WalletPaymentResult has its own validations.
    }

    // Convert raw wallet result to typed model and submit to wallet API,
    // mirroring the embedded / sheet flows.
    try {
      final walletPaymentResult = WalletPaymentResult.fromJson(result);
      // If the native wallet result lacks a usable token payload, fail fast
      // instead of sending an empty `paymentData.data` to the backend.
      if (walletPaymentResult.token.data.trim().isEmpty) {
        final err = StateError('Apple Pay returned empty wallet token data');
        await _submitApplePayDiagnostics(
          applePayConfiguration: applePayResult,
          error: err,
        );
        if (onApplePayError != null) {
          onApplePayError(err);
          return;
        }
        throw err;
      }
      final body = WalletPaymentRequest.toJson(
        request: paymentRequest,
        wallet: walletPaymentResult,
        walletType: 'APPLE_PAY',
      );
      if (kDebugMode) {
        debugPrint('Apple Pay wallet payload prepared: ${jsonEncode(body)}');
      }
      final headers = await buildPayorcSdkSignedHeaders(body);
      final paymentResponse = await PaymentSdkRepo().submitWalletPayment(
        body: body,
        headers: headers,
      );

      // Return merchant response to caller.
      onApplePayResult(Map<String, dynamic>.from(paymentResponse.raw));

      await _submitApplePayDiagnostics(
        applePayConfiguration: "Apple pay wallet payment Request body: ${body}",
        error: null,
      );
    } catch (submitError) {
      await _submitApplePayDiagnostics(
        applePayConfiguration: applePayResult,
        error: submitError,
      );
      if (onApplePayError != null) {
        onApplePayError(submitError);
      } else {
        rethrow;
      }
    }
  } catch (error) {
    await _submitApplePayDiagnostics(
      applePayConfiguration: applePayResult,
      error: error,
    );
    final errorMessage = error.toString();
    if (onApplePayError != null) {
      final errorStr = errorMessage.toLowerCase();
      if (errorStr.contains('canceled') || errorStr.contains('cancelled')) {
        onApplePayError('Transaction cancelled by user');
      } else {
        onApplePayError(error);
      }
    } else {
      rethrow;
    }
  }
}