stripe / react-stripe-js

React components for Stripe.js and Stripe Elements
https://stripe.com/docs/stripe-js/react
MIT License
1.75k stars 267 forks source link

Express checkout apple pay element won't update to a higher price in onShippingAddressChange #506

Closed grantsingleton closed 3 months ago

grantsingleton commented 4 months ago

What happened?

Issue

I need the shipping address to calculate tax. I listen to onShippingAddressChange, use the address to calculate tax and update the total amount to reflect the increase in price due to tax.

 onShippingAddressChange={async ({ resolve, address }) => {
    const checkoutAddress: Partial<CheckoutBuyerAddress> = {
      city_locality: address?.city,
      state_province: address?.state,
      postal_code: address?.postal_code,
      country_code: address?.country
    };
    const updatedCheckoutData = await updateCheckout({ buyerAddress: checkoutAddress as CheckoutBuyerAddress });
   // ** this line does not affect the amount unless it's less than the previous amount was when setting up Elements.  
   elements.update({ amount: updatedCheckoutData?.paymentAmount, mode: 'payment', currency: 'usd' });
    resolve({
      lineItems: generateLineItems(updatedCheckoutData)
    });
  }}

The amount wont update in the apple pay modal unless the amount is less than the previous amount. The line items do update with the correct tax amount, but the price doesnt change.

In this example the after tax price is $19.02, but it does not reflect. Screenshot 2024-06-06 at 7 47 44 AM

Full Code Snippet

function generateLineItems(checkoutData: GetCheckoutV2Response) {
  return [
    {
      name: 'Shipping',
      amount: checkoutData?.shippingAmount
    },
    {
      name: 'Tax',
      amount: checkoutData?.taxAmount
    }
  ];
}

function ExpressCheckoutForm({ checkoutData, updateCheckout }: ExpressCheckoutFormProps) {
  const stripe = useStripe();
  const elements = useElements();
  const { dispatch } = useGlobalState();

  console.log('checkoutData', checkoutData);

  return (
    <ExpressCheckoutElement
      onShippingAddressChange={async ({ resolve, address }) => {
        const checkoutAddress: Partial<CheckoutBuyerAddress> = {
          city_locality: address?.city,
          state_province: address?.state,
          postal_code: address?.postal_code,
          country_code: address?.country
        };
        const updatedCheckoutData = await updateCheckout({ buyerAddress: checkoutAddress as CheckoutBuyerAddress });
        console.log('updatedCheckoutData', updatedCheckoutData?.paymentAmount);
        elements.update({ amount: updatedCheckoutData?.paymentAmount, mode: 'payment', currency: 'usd' });
        resolve({
          lineItems: generateLineItems(updatedCheckoutData)
        });
      }}
      onConfirm={async event => {
        try {
          const { error: submitError } = await elements.submit();
          if (submitError) {
            throw new Error(submitError.message);
          }

          const { error: confirmError } = await stripe.confirmPayment({
            elements,
            clientSecret: checkoutData.clientSecret,
            confirmParams: {
              return_url: window.location.href,
            }
          });

          if (confirmError) {
            console.log(confirmError);
            throw new Error(confirmError.message);
          }

          return;
        } catch (e: any) {
          console.error(e.message);
          console.error(e);
          dispatch({
            type: ActionType.ADD_MODAL_DATA,
            payload: {
              title: 'Something went wrong',
              message: 'There was an error processing this payment. Please try again or reach out to our support team if the issue continues.',
              showDismiss: true,
              dismissText: 'Ok'
            }
          });
        }
      }}
      onClick={({ resolve }) => {
        resolve({
          emailRequired: true,
          allowedShippingCountries: ['US'],
          shippingAddressRequired: true,
          shippingRates: [{
            id: 'media-mail',
            displayName: 'Standard Shipping',
            amount: checkoutData?.shippingAmount,
            deliveryEstimate: {
              maximum: { unit: 'day', value: 8 },
              minimum: { unit: 'day', value: 2 },
            },
          }],
          lineItems: generateLineItems(checkoutData)
        });
      }}
    />
  );
}

interface ExpressCheckoutProps {
  checkoutData: GetCheckoutResponse;
  stripePromise: Promise<Stripe>;
  updateCheckout: UpdateCheckout;
}
export default function ExpressCheckout({ checkoutData, stripePromise, updateCheckout }: ExpressCheckoutProps) {

  if (!checkoutData?.paymentIntent) return null;

  return (
    <Elements
      stripe={stripePromise}
      options={{
        clientSecret: checkoutData.clientSecret,
        appearance: {
          theme: 'stripe'
        }
      }}
    >
      <ExpressCheckoutForm checkoutData={checkoutData} updateCheckout={updateCheckout} />
    </Elements>
  );
}

Environment

No response

Reproduction

No response

grantsingleton commented 3 months ago

Anyone from stripe? Express checkout is currently not usable if you need to calculate tax from the shipping address.

brendanm-stripe commented 3 months ago

Do you have a reproduction of this in test mode that you can share? This does not align with my expectations and behaviour for updating the Express Checkout Element.

grantsingleton commented 3 months ago

@brendanm-stripe how would you like me to share that? What I shared is the code that im running with the behavior. Do I need to host something and share a link?

To reproduce you just need to update the payment amount in onShippingAddressChange to higher than you started with.

brendanm-stripe commented 3 months ago

Actually I chatted with a teammate who helped point out that it looks like you're using an "intent first" pattern supplying the payment intent client secret up front. Note that our recommended integration is using a "deferred" approach with the intent created at the end just before confirming.

The way you've configured things should work, but requires you to update your payment intent amount server side (perhaps in your updateCheckout()), then client-side you need to retrieve the latest details using elements.fetchUpdates() instead of elements.update(...).

grantsingleton commented 3 months ago

@brendanm-stripe that makes sense. I just confirmed using fetchUpdates() works as expected. Thank you!!