Search code examples
node.jspaypal

PayPal immediately redirects to my site when clicking "Continue to review order"


I am implementing PayPal on my site and everything is working correctly, I receive all the webhooks it will add tokens to the user in CHECKOUT.ORDER.APPROVED

however, my problem is, instead of waiting for the payment to actually go through(it does eventually) the user is instantly redirected to my site

I don't know if I am missing a status call on PayPal webhook side?

the page I am talking about is here for reference

enter image description here

here is my controller code

exports.createPayment = async (req, res) => {
  const { userId, amountCNY, selectedTokens } = req.body;

  // Convert amount from RMB to USD based on the current exchange rate
  const amountInUsd = convertRmbToUsd(amountCNY);

  // Concatenate userId and selectedTokens to use as custom_id
  const customId = `${userId}:${selectedTokens}`;

  const request = new paypal.orders.OrdersCreateRequest();
  request.prefer('return=representation');
  request.requestBody({
    intent: 'CAPTURE',
    purchase_units: [
      {
        amount: {
          currency_code: 'USD',
          value: amountInUsd.toFixed(2).toString(),
        },
        custom_id: customId,
      },
    ],
    application_context: {
      return_url: 'example.com',
      cancel_url: 'example.com',
    },
  });

  try {
    const response = await client().execute(request);
    if (response.statusCode === 201) {
      const approvalUrl = response.result.links.find((link) => link.rel === 'approve').href;
      res.json({ id: response.result.id, approvalUrl: approvalUrl });
    }
  } catch (error) {
    res.status(500).send('Error creating PayPal payment');
  }
};

exports.executePayment = async (req, res) => {
  const { orderId } = req.body;

  const request = new paypal.orders.OrdersCaptureRequest(orderId);
  request.requestBody({});

  try {
    const capture = await client().execute(request);
    res.status(200).json(capture.result);
  } catch (error) {
    res.status(500).send('Error capturing PayPal payment');
  }
};

exports.handleWebhook = async (req, res) => {
  const event = req.body;

  if (event.event_type === 'CHECKOUT.ORDER.APPROVED') {
    const orderId = event.resource.id;

    try {
      const request = new paypal.orders.OrdersCaptureRequest(orderId);
      request.requestBody({});
      const capture = await client().execute(request);

      const purchaseUnit = capture.result.purchase_units[0];
      const customId = purchaseUnit.custom_id;
      const [userId, selectedTokens] = customId.split(':');

      const newBalance = await addTokensByUserId(userId, parseInt(selectedTokens, 10));
      res.status(200).send('Success! Tokens added.');
    } catch (error) {
      res.status(500).send({ error: `Error processing payment: ${error.message}` });
    }
  } else if (event.event_type === 'PAYMENT.CAPTURE.COMPLETED') {
    res.status(200).send('Payment capture completed.');
  } else {
    res.status(200).send('Event received, but not handled.');
  }
};


Solution

  • Approved means a payer signed in and approved the checkout at PayPal. It does not mean any payment was captured; the order is not complete at that stage.

    Webhooks are not necessary to process normal v2/checkout/orders payments . If you implement them, do so later; your Orders v2 integration should work without them first.

    There are two ways for a payer to approve a PayPal order, redirecting them away from your site over to PayPal and back or using the JS SDK. The JS SDK is a much better payer experience for many reasons and you should change to that, see https://developer.paypal.com/docs/checkout/standard/integrate/ for JS samples paired with an orders backend.

    If using the old redirect-away-from-site pattern, the button you have circled will (by default) say "Continue to Review Order", and you are expected to show your own confirmation page to the user. After they confirm the order by clicking some action on that final page of yours, you are expected to do a final orders 'capture' API call to commit the transaction. Based on the response of that capture API call you are expected to show a clear message of success or failure to the payer.

    With the JS SDK, the circled final button will (by default) say "Pay Now". With that default setting no review step is expected, and you can proceed with capturing the order immediately with the API call for that. Based on the response you are expected to show the message of success or failure to the payer.

    It is possible to override the default wording of the last action at PayPal, using the API parameter user_action . This only changes the wording, it does not change anything PayPal does. The behavior of what your integration does after approval must change to correspond if you are changing from the default for whichever approval method.

    If for some reason you are not familiar with the better no-redirect experience the JS SDK gives, see the frontend code demo at https://developer.paypal.com/demo/checkout/#/pattern/server