Can't Save PayPal for purchase later with the Javascript SDK, unable to pass setup token

radspirit88
Contributor
Contributor

I'm trying to save a user's PayPal for off-session transactions. I'm following the docs here (https://developer.paypal.com/docs/checkout/save-payment-methods/purchase-later/js-sdk/paypal/).

I'm using the docs as closely as possible, though I have to modify the client code to work in node.

I'm getting the token ID from my server, but that doesn't seem to be the format or kind of input required.

 

I'm stuck on step 6. I'm getting smart_api_vault_ectoken_contingency_error

 

 

buttonCorrelationID
: 
"f10601934aa66"
buttonSessionID
: 
"uid_f9dd725eba_mjm6mtk6mjy"
clientID
: 
"AdvBzA3XST3GAEp5MdTk5PqRDjVXZYON0jNyaQfs7P90BQcQqMiIS9HMEjRe3qrehxVArMvE1LnaHVqH"
env
: 
"sandbox"
referer
: 
"www.sandbox.paypal.com"
sdkCorrelationID
: 
"0819775552895"
sessionID
: 
"uid_8c172cf2fe_mjm6mdy6mzu"
timestamp
: 
"1701991169324"
token
: 
null

 

 

Here is my next.js component:

 

 

import React, { useEffect, useState } from 'react';

const PayPalButton = () => {
  const [scriptLoaded, setScriptLoaded] = useState(false);

  useEffect(() => {
    // Function to fetch token and load PayPal script
    const fetchTokenAndLoadScript = async () => {
      try {
        const headers = {
          'Content-Type': 'application/x-www-form-urlencoded',
          Authorization:
            'Basic ' +
            Buffer.from(
              process.env.NEXT_PUBLIC_CLIENT_ID +
                ':' +
                process.env.NEXT_PUBLIC_CLIENT_SECRET
            ).toString('base64'),
        };

        const body = new URLSearchParams();
        body.append('grant_type', 'client_credentials');
        body.append('response_type', 'id_token');

        // Replace with your token fetch request
        const tokenResponse = await fetch(
          'https://api-m.sandbox.paypal.com/v1/oauth2/token',
          {
            method: 'POST',
            headers: headers,
            body: body,
          }
        );

        const tokenData = await tokenResponse.json();
        const userToken = tokenData.id_token; // Adjust according to the actual response structure

        console.log('User token:', userToken);

        // Construct the script URL
        const srcUrl = `https://www.paypal.com/sdk/js?client-id=${process.env.NEXT_PUBLIC_CLIENT_ID}&merchant-id=${process.env.NEXT_PUBLIC_MERCHANT_ID}`;

        console.log('Script URL:', srcUrl);

        // Check if the script is already present
        if (!document.querySelector(`script[src="${srcUrl}"]`)) {
          console.log('Script already loaded once...');
          const script = document.createElement('script');
          script.src=srcUrl;
          script.setAttribute('data-user-id-token', userToken); // Add the 'data-user-id-token' property
          script.addEventListener('load', () => {
            setScriptLoaded(true);
          });
          document.body.appendChild(script);
        }
      } catch (error) {
        console.error(
          'Error fetching token or loading PayPal script:',
          error
        );
      }
    };

    fetchTokenAndLoadScript();
  }, []);

  useEffect(() => {
    if (scriptLoaded) {
      console.log("The script's loaded, let's render the button!");
      window.paypal
        .Buttons({
          createVaultSetupToken: async () => {
            // Call your server API to generate a setup token
            // and return it here as a string
            const response = await fetch(
              'http://localhost:3000/api/v1/paypal/create-setup-token',
              {
                method: 'POST',
                credentials: 'include',
              }
            );
            const result = await response.json();
            console.log('Made it here.');

            console.log(`Token here be: ${result.id}`);

            return result.id;
          },
          onApprove: async ({ vaultSetupToken }) => {
            return fetch(
              'http://localhost:3000/api/v1/paypal/create-payment-token',
              {
                body: JSON.stringify({ vaultSetupToken }),
              }
            );
          },
          onError: (error) => {
            console.log('An error occurred: ', error);
          },
        })
        .render('#paypal-buttons-container');
    }
  }, [scriptLoaded]);

  return <div id="paypal-buttons-container"></div>;
};

export default PayPalButton;

 



And here is my backend in express.js:

 

 

import 'dotenv/config';
import express from 'express';
const { PORT = 3011, ACCESS_TOKEN } = process.env;
const app = express();
app.set('view engine', 'ejs');
app.use(express.static('public'));

app.post('/api/v1/paypal/create-user-token', async (req, res) => {
  console.log('Creating user token...');
  res.status(200).json({ message: 'User token created' });
});

// Create setup token
app.post('/api/v1/paypal/create-setup-token', async (req, res) => {
  console.log(ACCESS_TOKEN);
  console.log('Creating setup token...');

  try {
    // Use your access token to securely generate a setup token
    // with an empty payment_source
    const vaultResponse = await fetch(
      'https://api-m.sandbox.paypal.com/v3/vault/setup-tokens',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${ACCESS_TOKEN}`,
          'PayPal-Request-Id': Date.now(),
        },
        body: JSON.stringify({
          payment_source: {
            paypal: {},
          },
        }),
      }
    );
    const result = await vaultResponse.json();
    res.json(result);
  } catch (err) {
    console.log('Error creating setup token:', err);
    res.status(500).send(err.message);
  }
});
// Create payment token from a setup token
app.post('/api/v1/paypal/create-payment-token/', async (req, res) => {
  try {
    const paymentTokenResult = await fetch(
      'https://api-m.sandbox.paypal.com/v3/vault/payment-tokens',
      {
        method: 'POST',
        body: {
          payment_source: {
            token: {
              id: req.body.vaultSetupToken,
              type: 'SETUP_TOKEN',
            },
          },
        },
        headers: {
          Authorization: 'Bearer ${ACCESS-TOKEN}',
          'PayPal-Request-Id': Date.now(),
        },
      }
    );
    const paymentMethodToken = paymentTokenResult.id;
    const customerId = paymentTokenResult.customer.id;
    await save(paymentMethodToken, customerId);
    res.json(captureData);
  } catch (err) {
    res.status(500).send(err.message);
  }
});
const save = async function (paymentMethodToken, customerId) {
  // Specify where to save the payment method token
};
app.listen(PORT, () => {
  console.log(`Server listening at http://localhost:${PORT}/`);
});

 

 

Login to Me Too
1 ACCEPTED SOLUTION

Accepted Solutions
Solved

radspirit88
Contributor
Contributor

I got it figured out. The JSON body needs to specify the following (this is not properly documented in the documentation link I shared. It says to use the empty paypal:{} object:

paypal: {
usage_type: "MERCHANT",
experience_context: {
vault_instruction: "ON_PAYER_APPROVAL",
return_url: "https://example.com/return",
cancel_url: "https://example.com/cancel"
}
}

View solution in original post

Login to Me Too
3 REPLIES 3

Kavyar
Moderator
Moderator

Good day @radspirit88 

 

Thank you for posting to the PayPal community.

 

I would suggest to please contact your website developer to cross check your using same LIVE/SANDBOX  REST API Credentials(Client ID & Secret) while performing the below API calls.

 

https://developer.paypal.com/api/rest/authentication/

https://developer.paypal.com/api/rest/postman/ 

 

For additional details on saving payment methods, please refer to the link below:

 

https://developer.paypal.com/docs/checkout/save-payment-methods/

 

https://developer.paypal.com/docs/checkout/save-payment-methods/purchase-later/js-sdk/paypal/

 

To create a setup token for your card, please follow the steps provided in the link below:

 

https://developer.paypal.com/docs/multiparty/checkout/save-payment-methods/purchase-later/payment-to...

 

Please verify eligibility criteria by referring to the following link - https://developer.paypal.com/docs/checkout/save-payment-methods/purchase-later/js-sdk/paypal/#link-c...

 

I would like to suggest that you try again following the steps outlined above.

 

If you are still experiencing issues, please create an MTS ticket via the following URL - https://www.paypal-support.com/s/?language=en_US . Please ensure that you provide detailed information and error details when submitting the ticket.

 

Sincerely,

Kavya

PayPal MTS

 

If this post or any other was helpful, please enrich the community by giving kudos or accepting it as a solution.

Login to Me Too

radspirit88
Contributor
Contributor

Thank you for the reply @Kavyar 

 

I followed all your steps.

 

I ensured that we are using the same client information to create the access token as we are in the app.

 

The app and the account are both eligible and properly set up to save payments.

 

I am able to properly save PayPal when I use the API with CURL directly through my terminal. But it won't work in my application.

 

I see this in the console when I load my page: GET https://b.sbox.stats.paypal.com/v2/counter.cgi?p=uid_4074ae3ab0_mtc6mzy6nty&s=SMART_PAYMENT_BUTTONS net::ERR_NAME_NOT_RESOLVED

 

And then when I click the button, I get a window that pops up and immediately closes. Then I see the contingency error in the console.

 

On my server, the set up token is properly created:

 

Setup token created: {
  id: '9BL40893PE668182L',
  customer: { id: 'JLTwBbEZFr' },
  status: 'CREATED',
  payment_source: { paypal: {} },
  links: [
    {
      href: 'https://api.sandbox.paypal.com/v3/vault/setup-tokens/9BL40893PE668182L',
      rel: 'self',
      method: 'GET',
      encType: 'application/json'
    }
  ]
}

And that ID is making it to the vaultResponse, but the console is showing a 400 status when trying to hit this URL: POST https://www.sandbox.paypal.com/smart/api/vault/9BL40893PE668182L/ectoken 400 (Bad Request)

As you can see, it is using the right token ID in that URL.

I do see that the scope returned for my access token shows vault for credit cards but not paypal? I don't know if it's the same thing:

 

"https://uri.paypal.com/services/checkout/one-click-with-merchant-issued-token https://uri.paypal.com/services/invoicing https://uri.paypal.com/services/vault/payment-tokens/read https://uri.paypal.com/services/disputes/read-buyer https://uri.paypal.com/services/payments/realtimepayment https://uri.paypal.com/services/disputes/update-seller https://uri.paypal.com/services/payments/payment/authcapture openid https://uri.paypal.com/services/disputes/read-seller Braintree:Vault https://uri.paypal.com/services/payments/refund https://api.paypal.com/v1/vault/credit-card https://uri.paypal.com/services/pricing/quote-exchange-rates/read https://uri.paypal.com/services/billing-agreements https://api.paypal.com/v1/payments/.* https://uri.paypal.com/payments/payouts https://uri.paypal.com/services/vault/payment-tokens/readwrite https://api.paypal.com/v1/vault/credit-card/.* https://uri.paypal.com/services/shipping/trackers/readwrite https://uri.paypal.com/services/subscriptions https://uri.paypal.com/services/applications/webhooks"



 

 

Login to Me Too
Solved

radspirit88
Contributor
Contributor

I got it figured out. The JSON body needs to specify the following (this is not properly documented in the documentation link I shared. It says to use the empty paypal:{} object:

paypal: {
usage_type: "MERCHANT",
experience_context: {
vault_instruction: "ON_PAYER_APPROVAL",
return_url: "https://example.com/return",
cancel_url: "https://example.com/cancel"
}
}
Login to Me Too

Haven't Found your Answer?

It happens. Hit the "Login to Ask the community" button to create a question for the PayPal community.