stripe / stripe-js

Loading wrapper for Stripe.js
https://stripe.com/docs/js
MIT License
622 stars 153 forks source link

[BUG]: Error when paying with cashapp #539

Closed vacoo closed 7 months ago

vacoo commented 8 months ago

What happened?

When the cashapp payment method is linked to my account, an error occurs on the frontend when trying to pay. Everything works fine for other payment methods. Here's what the subscription creation code looks like:

    public function createPaymentIntentSubscription(StripePaymentIntentDto $dto): PaymentIntent
    {
        $cart = $dto->getCart();

        // Metadata
        $metadata = [
            'stripe_payments_cart_number' => $cart->getNumber(),
            'stripe_payments_user_id' => $cart->getUserId(),
            'cart_id' => $cart->getNumber(),
            'user_id' => $cart->getUserId(),
        ];

        $cartMetadata = $cart->getMetadata();
        if ($cart->isIncludeSubmission()) {
            $metadata['submission_id'] = $cartMetadata->getSubmissionId();
        }

        if (null !== $cartMetadata->getSource()) {
            $metadata['source'] = $cartMetadata->getSource();
        }

        // Phase
        $phaseParams = [
            'currency' => $cart->getCurrency(),
            'items' => $dto->getSubscriptionsItems(),
            'add_invoice_items' => $dto->getOneTimeItems(),
            'proration_behavior' => 'none',
            'metadata' => $metadata,
        ];

        if ($cart->isIncludeDosage()) {
            $phaseParams['items'] = $dto->getSubscriptionsItems();
        }

        if (null !== $cart->getCoupon()->getRefCoupon()) {
            $phaseParams['coupon'] = $cart->getCoupon()->getRefCoupon();
        }

        $paymentMethod = $dto->getStripeCustomer()->object['invoice_settings']['default_payment_method'];
        if ($paymentMethod) {
            $phaseParams['default_payment_method'] = $paymentMethod;
        }

        $subscriptionScheduleParams = [
            'customer' => $dto->getStripeCustomer()->refCustomer,
            'start_date' => time(),
            'metadata' => $metadata,
            'phases' => [$phaseParams],
        ];

        $subscriptionSchedule = $this->stripeClient->subscriptionSchedules->create($subscriptionScheduleParams);
        $subscription = $this->stripeClient->subscriptions->retrieve($subscriptionSchedule->subscription);
        $refLatestInvoice = $subscription->latest_invoice;

        if ($cart->isIncludeShipping()) {
            $shippingRate = $this->stripeClient->shippingRates->create([
                'display_name' => 'Shipping',
                'type' => 'fixed_amount',
                'fixed_amount' => [
                    'amount' => $cart->getTotalShippingAmount(),
                    'currency' => $cart->getCurrency(),
                ],
            ]);

            $latestInvoice = $this->stripeClient->invoices->update($subscription->latest_invoice, [
                'shipping_cost' => [
                    'shipping_rate' => $shippingRate->id,
                ],
            ]);

            $refLatestInvoice = $latestInvoice->id;
        }

        $invoiceParams = [
            'metadata' => $metadata,
            'payment_settings' => [
                'payment_method_types' => ['card', 'link', 'cashapp'],
            ],
        ];

        if ($paymentMethod) {
            $invoiceParams['default_payment_method'] = $paymentMethod;
        }

        $this->stripeClient->invoices->update($refLatestInvoice, $invoiceParams);

        $invoice = $this->stripeClient->invoices->finalizeInvoice($refLatestInvoice, [
            'auto_advance' => false,
        ]);

        return $this->stripeClient->paymentIntents->update($invoice->payment_intent, [
            'metadata' => $metadata,
            'setup_future_usage' => 'off_session',
        ]);
    }

Then an error appears on the frontend when the payment is confirmed: But I don't explicitly specify anywhere that mandate_data should be passed

const confirmPaymentResult = await stripe?.value?.confirmPayment({
        elements: elements.value as StripeElements,
        redirect: 'if_required',
        confirmParams: {
          return_url: location.href,
          payment_method_data: {
            billing_details: {
              name,
              address: {
                line1: address.line1,
                line2: address.line2 ?? undefined,
                city: address.city,
                country: address.country,
                postal_code: address.postal_code,
                state: address.state,
              },
            },
          },
        },
      });

POST https://api.stripe.com/v1/payment_intents/pi_3ObaIYCCk0EwjrzL0kMNZ1VR/confirm

use_stripe_sdk: true
mandate_data[customer_acceptance][type]: online
mandate_data[customer_acceptance][online][infer_from_client]: true
return_url: http://site.localhost/checkout
shipping[name]: fddf
shipping[address][city]: Duluth
shipping[address][line1]: 3244 Commerce Avenue Northwest
shipping[address][line2]: 
shipping[address][state]: GA
shipping[address][country]: US
shipping[address][postal_code]: 30096
shipping[phone]: +12312333333
payment_method: pm_1ObLr6CCk0EwjrzL9hx3Ba13
key: <key>
client_secret: <secret>

Response:

{
  "error": {
    "message": "mandate_data is not allowed because this payment intent already has a reusable Cash App Pay payment method attached to it.",
    "payment_intent": {
      "id": "pi_3ObaIYCCk0EwjrzL0kMNZ1VR",
      "object": "payment_intent",
      "amount": 22000,
      "amount_details": {
        "tip": {
        }
      },
      "automatic_payment_methods": null,
      "canceled_at": null,
      "cancellation_reason": null,
      "capture_method": "automatic",
      "client_secret": "<secret>",
      "confirmation_method": "automatic",
      "created": 1705979114,
      "currency": "usd",
      "description": "Subscription creation",
      "last_payment_error": null,
      "livemode": false,
      "next_action": null,
      "payment_method": "pm_1ObLr6CCk0EwjrzL9hx3Ba13",
      "payment_method_configuration_details": null,
      "payment_method_types": [
        "card",
        "cashapp",
        "link"
      ],
      "processing": null,
      "receipt_email": null,
      "setup_future_usage": "off_session",
      "shipping": {
        "address": {
          "city": "Duluth",
          "country": "US",
          "line1": "3244 Commerce Avenue Northwest",
          "line2": "",
          "postal_code": "30096",
          "state": "GA"
        },
        "carrier": null,
        "name": "fddf",
        "phone": "+12312333333",
        "tracking_number": null
      },
      "source": null,
      "status": "requires_confirmation"
    },
    "request_log_url": "https://dashboard.stripe.com/test/logs/req_0ePCN5QRikkDsV?t=1705979117",
    "type": "invalid_request_error"
  }
}

Environment

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36

Reproduction

No response

brendanm-stripe commented 8 months ago

There's a lot going on here with the subscription creation via Schedule, but at the core the issue seems to be that you're trying to confirm via Stripe.js an invoice-associated payment intent using a payment method you've already collected and set up for future usage. That mandate_data is supplied automatically by Stripe.js via the Payment Element.

Is there a particular reason why you're trying to confirm this from the client? If you already have a reusable Payment Method set up for the customer as ['invoice_settings']['default_payment_method'] or the subscription default_payment_method, you should be able to /pay the invoice or confirm the payment intent server-side to complete the payment.

It's possible that there's a bug to address here, that the "extra" mandate data ought to be allowed, but I'd like to know more about the use case.

vacoo commented 7 months ago

@brendanm-stripe Thank you very much for the clarification. I create a confirmation of the invoice via the server and everything worked

$this->stripeClient->invoices->pay($paymentIntent->invoice);