stripe / stripe-react-native

React Native library for Stripe.
https://stripe.dev/stripe-react-native
MIT License
1.26k stars 263 forks source link

Different behavior for BLIK payment on Android #1571

Open remonh87 opened 10 months ago

remonh87 commented 10 months ago

Describe the bug When confirming BLIK operation with passing a 6-digit code Android returns error "The payment has been canceled" but I see that payment appears in Stripe dashboard as "Succeeded". iOS with the same implementation return "Payment successfully completed" and I also see that payment appears in Stripe dashboard as "Succeeded".

For more context: https://github.com/flutter-stripe/flutter_stripe/issues/1497

To Reproduce Servercode:

app.post(
  '/create-payment-intent',
  async (
    req: express.Request,
    res: express.Response
  ): Promise<express.Response<any>> => {

    const payment_method_type: string = req.body['payment_method_type'];
    const blik_code: string | undefined = req.body['blik_code'];
    const bank: string | undefined = req.body['bank'];

    const { secret_key } = getKeys();

    const stripe = new Stripe(secret_key as string, {
      apiVersion: '2023-08-16',
      typescript: true,
    });

    if (payment_method_type == 'blik' && blik_code == undefined) {
      return res.send({
        error: 'blik_code is undefined',
      });
    } else if (payment_method_type == 'p24' && bank == undefined) {
      return res.send({
        error: 'bank is undefined',
      });
    }

    var payment_method_data_type: Stripe.PaymentIntentCreateParams.PaymentMethodData.Type;
    if (payment_method_type == 'blik') {
      payment_method_data_type = 'blik';
    } else if (payment_method_type == 'p24') {
      payment_method_data_type = 'p24';
    } else {
      return res.send({
        error: 'Pass payment_method_type blik or p24',
      });
    }

    var bank_type: Stripe.PaymentIntentCreateParams.PaymentMethodData.P24.Bank | undefined;
    if (bank == 'alior_bank') {
      bank_type = 'alior_bank';
    } else if (bank == 'bank_millennium') {
      bank_type = 'bank_millennium';
    } else if (bank == 'ing') {
      bank_type = 'ing';
    }

    // Create a PaymentIntent with the order amount and currency.
    const params: Stripe.PaymentIntentCreateParams = {
      confirm: payment_method_type == 'blik' ? true : undefined,
      amount: 1099,
      currency: "pln",
      payment_method_options: {
        p24: payment_method_type == 'p24' ? {} : undefined,
        blik: payment_method_type == 'blik' ? {
          code: blik_code,
        } : undefined,

      },
      payment_method_data: {
        type: payment_method_data_type,
        blik: payment_method_type == 'blik' ? {} : undefined,
        p24: payment_method_type == 'p24' ? {
          bank: bank_type,
        } : undefined,
        billing_details: payment_method_type == 'p24' ? {
          email: 'emailuzytkownika@gmail.com',
        } : undefined,
      },
      payment_method_types: [
        payment_method_type,
      ],
    };

    console.log(`create-payment-intent params ${params}`)

    try {
      const paymentIntent: Stripe.PaymentIntent =
        await stripe.paymentIntents.create(params);
      // Send publishable key and PaymentIntent client_secret to client.
      console.log(`create-payment-intent client_secret ${paymentIntent.client_secret}`)
      return res.send({
        clientSecret: paymentIntent.client_secret,
      });
    } catch (error: any) {
      console.log(`create-payment-intent error ${error}`)
      return res.send({
        error: error.raw.message,
      });
    }
  }
);

Create a payment intent and confirm it using this sdk.

Expected behavior PAyment should be successful on android like on iOS.

Additional context I found that isNextActionSuccessState(result.nextActionType) in retrievePaymentIntent function in PaymentLauncherFragment.kt in reactnativestripesdk package returns false and thats why it returns error with the message "The payment has been canceled"

private fun retrievePaymentIntent(clientSecret: String, stripeAccountId: String?) {
    stripe.retrievePaymentIntent(clientSecret, stripeAccountId, expand = listOf("payment_method"), object : ApiResultCallback<PaymentIntent> {
      override fun onError(e: Exception) {
        promise.resolve(createError(ConfirmPaymentErrorType.Failed.toString(), e))
        removeFragment(context)
      }

      override fun onSuccess(result: PaymentIntent) {
        when (result.status) {
          StripeIntent.Status.Succeeded,
          StripeIntent.Status.Processing,
          StripeIntent.Status.RequiresConfirmation,
          StripeIntent.Status.RequiresCapture -> {
            promise.resolve(createResult("paymentIntent", mapFromPaymentIntentResult(result)))
          }
          StripeIntent.Status.RequiresAction -> {
            // nextActionType is BlikAuthorize and this method will return false
            if (isNextActionSuccessState(result.nextActionType)) {
              promise.resolve(createResult("paymentIntent", mapFromPaymentIntentResult(result)))
            } else {

              (result.lastPaymentError)?.let {
                promise.resolve(createError(ConfirmPaymentErrorType.Canceled.toString(), it))
              } ?: run {
                // Android will go here
                promise.resolve(createError(ConfirmPaymentErrorType.Canceled.toString(), "The payment has been canceled"))
              }
            }
          }
          StripeIntent.Status.RequiresPaymentMethod -> {
            promise.resolve(createError(ConfirmPaymentErrorType.Failed.toString(), result.lastPaymentError))
          }
          StripeIntent.Status.Canceled -> {
            promise.resolve(createError(ConfirmPaymentErrorType.Canceled.toString(), result.lastPaymentError))
          }
          else -> {
            promise.resolve(createError(ConfirmPaymentErrorType.Unknown.toString(), "unhandled error: ${result.status}"))
          }
        }
        removeFragment(context)
      }
    })
  }

On iOS StripeSdk.swift has different implementation for this confirmPayment

func onCompleteConfirmPayment(status: STPPaymentHandlerActionStatus, paymentIntent: STPPaymentIntent?, error: NSError?) {
        self.confirmPaymentClientSecret = nil
        switch (status) {
        case .failed:
            confirmPaymentResolver?(Errors.createError(ErrorType.Failed, error))
            break
        case .canceled:
            let statusCode: String
            if (paymentIntent?.status == STPPaymentIntentStatus.requiresPaymentMethod) {
                statusCode = ErrorType.Failed
            } else {
                statusCode = ErrorType.Canceled
            }
            if let lastPaymentError = paymentIntent?.lastPaymentError {
                confirmPaymentResolver?(Errors.createError(statusCode, lastPaymentError))
            } else {
                confirmPaymentResolver?(Errors.createError(statusCode, "The payment has been canceled"))
            }
            break
        case .succeeded:
            // iOS will go here
            if let paymentIntent = paymentIntent {
                let intent = Mappers.mapFromPaymentIntent(paymentIntent: paymentIntent)
                confirmPaymentResolver?(Mappers.createResult("paymentIntent", intent))
            }
            break
        @unknown default:
            confirmPaymentResolver?(Errors.createError(ErrorType.Unknown, "Cannot complete the payment"))
            break
        }
    }
wojciechzahradnikdeviniti commented 10 months ago

any updates?

wojciechzahradnikdeviniti commented 9 months ago

hi, any updates?

wojciechzahradnikdeviniti commented 8 months ago

hi, any updates?

krzysiek-mc commented 6 months ago

Same problem here...