Web-payments api tokenize(paymentMethod) is throwing "One or more of the arguments needed are missing or invalid." error

When a card is entered and the handlePaymentSubmission() function is called, the subsequent call to tokenize(paymentMethod) and paymentMethod.tokenize() is throwing an argument error. PaymentMethod object appears valid.

Is there an expected argument for the paymentMethod.tokenize() method?

What is causing this error?

The stack is:

square.js:2 One or more of the arguments needed are missing or invalid.   (anonymous) @ square.js:2
handlePaymentMethodSubmission @ (index):1538
async function (async)  handlePaymentMethodSubmission @ (index):1519
(anonymous) @ (index):1545 
 r @ square.js:2

:wave: What example are you running into this issue? Here is a reference to the web-payments-quickstart . :slightly_smiling_face:

I am using the code base from GIT related to web-payments. As I detailed above, I traced it through to the error being thrown when paymentMethod.tokenize() is called from the tokenize() function called from the handlepaymentMethodSubmission() call. This is triggered when the button to encode the card is clicke.

Is there an argument required for the method paymentMethod.tokenize() ? That seems to be where the error is coming from. Displaying the object ‘paymentMethod’ does not show me the methods that it uses, only the properties. Hence I can’t see if the thrown error is a direct result of the tokenize() method or whether it is something that it is calling within the object method.

And here is the source code from your GIT repository. I commented out the createPayment() function since that is done in the backend in my environment (at least for now).

    <script
      type="text/javascript"
      src="{$web_payments_url}"
    ></script>
    <script>
      const appId = '{$application_id}';
      const locationId = '{$location_id}';

      async function initializeCard(payments) {
        const card = await payments.card();
        await card.attach('#card-container');

        return card;
      }

      // verificationToken can be undefined, as it does not apply to all payment methods.
/* Not used
      async function createPayment(token, verificationToken) {
        const bodyParameters = {
          locationId,
          sourceId: token,
        };

        if (verificationToken !== undefined) {
          bodyParameters.verificationToken = verificationToken;
        }

        const body = JSON.stringify(bodyParameters);

        const paymentResponse = await fetch('/payment', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body,
        });

        if (paymentResponse.ok) {
          return paymentResponse.json();
        }

        const errorBody = await paymentResponse.text();
        throw new Error(errorBody);
      }
*/
	async function loadPaymentForm() {
//	document.addEventListener('DOMContentLoaded', async function () {
	  if (!window.Square) {
		throw new Error('Square.js failed to load properly');
	  }

	  const payments = window.Square.payments(appId, locationId);
	  let card;
	  try {
		card = await initializeCard(payments);
	  } catch (e) {
		console.error('Initializing Card failed', e);
		return;
	  }
	
	  // Step 5.2: create card payment
//	});
	}
	
      async function tokenize(paymentMethod) {
console.log("calling paymentMethod.tokenize() paymentMethod:"+JSON.stringify(paymentMethod,null,4) );
        const tokenResult = await paymentMethod.tokenize();
console.log("tokenResult:"+JSON.stringify(tokenResult,null,4) );
        if (tokenResult.status === 'OK') {
          return tokenResult.token;
        } else {
          throw new Error('Tokenization errors: '+JSON.stringify(tokenResult.errors));
        }
      }

      // status is either SUCCESS or FAILURE;
      function displayPaymentResults(status) {
        const statusContainer = document.getElementById(
          'payment-status-container'
        );
        if (status === 'SUCCESS') {
          statusContainer.classList.remove('is-failure');
          statusContainer.classList.add('is-success');
        } else {
          statusContainer.classList.remove('is-success');
          statusContainer.classList.add('is-failure');
        }

        statusContainer.style.visibility = 'visible';
      }

      async function verifyBuyer(payments, token) {
        const verificationResults = await payments.verifyBuyer(
          token,
          verificationDetails	// Global below
        );
        return verificationResults.token;
      }

      document.addEventListener('DOMContentLoaded', async function () {
        if (!window.Square) {
          throw new Error('Square.js failed to load properly');
        }

        let payments;
        try {
          payments = window.Square.payments(appId, locationId);
console.log('Event listener loaded: window.Square.payments('+appId+', '+locationId+')');
        } catch {
          const statusContainer = document.getElementById(
            'payment-status-container'
          );
          statusContainer.className = 'missing-credentials';
          statusContainer.style.visibility = 'visible';
          return;
        }

        let card;
        try {
          card = await initializeCard(payments);
//console.log("initializeCard, card="+JSON.stringify(card,null,4) );
        } catch (e) {
          console.error('Initializing Card failed', e);
          return;
        }

        async function handlePaymentMethodSubmission(
          event,
          paymentMethod,
          shouldVerify = false
        ) {
          event.preventDefault();

          try {
            // disable the submit button as we await tokenization and make a payment request.
            cardButton.disabled = true;
            const token = await tokenize(paymentMethod);
            let verificationToken;

            if (shouldVerify) {
              verificationToken = await verifyBuyer(payments, token);
            }

            const paymentResults = await createPayment(
              token,
              verificationToken
            );
console.log("paymentResults:"+JSON.stringify(paymentResults,null,4) );

            displayPaymentResults('SUCCESS');
            console.debug('Payment Success', paymentResults);
          } catch (e) {
console.log("Caught error:"+JSON.stringify(e,null,4) );
            cardButton.disabled = false;
            displayPaymentResults('FAILURE');
            console.error(e.message);
          }
        }

        const cardButton = document.getElementById('card-button');
        cardButton.addEventListener('click', async function (event) {
          // SCA only needs to be run for Card Payments. All other payment methods can be set to false.
          handlePaymentMethodSubmission(event, card, true);
        });
      });
    </script>

I took what you provided and am getting a very different error. I get a verificationDetails is not defined error. If you try the following without commenting anything out does it work?

<!DOCTYPE html>
<html>
  <head>
    <link href="/app.css" rel="stylesheet" />
    <script
    type="text/javascript"
    src="https://sandbox.web.squarecdn.com/v1/square.js"
  ></script>
  <script>
  const appId = 'REPLACE_ME';
  const locationId = 'REPLACE_ME';

      async function initializeCard(payments) {
        const card = await payments.card();
        await card.attach('#card-container');

        return card;
      }

      async function initializeCard(payments) {
           const card = await payments.card();
           await card.attach('#card-container');

           return card;
         }

         // verificationToken can be undefined, as it does not apply to all payment methods.
         async function createPayment(token, verificationToken) {
           const bodyParameters = {
             locationId,
             sourceId: token,
           };

           if (verificationToken !== undefined) {
             bodyParameters.verificationToken = verificationToken;
           }

           const body = JSON.stringify(bodyParameters);

           const paymentResponse = await fetch('/payment', {
             method: 'POST',
             headers: {
               'Content-Type': 'application/json',
             },
             body,
           });

           if (paymentResponse.ok) {
             return paymentResponse.json();
           }

           const errorBody = await paymentResponse.text();
           throw new Error(errorBody);
         }

         async function tokenize(paymentMethod) {
           const tokenResult = await paymentMethod.tokenize();
           if (tokenResult.status === 'OK') {
             return tokenResult.token;
           } else {
             throw new Error(
               `Tokenization errors: ${JSON.stringify(tokenResult.errors)}`
             );
           }
         }

         // status is either SUCCESS or FAILURE;
         function displayPaymentResults(status) {
           const statusContainer = document.getElementById(
             'payment-status-container'
           );
           if (status === 'SUCCESS') {
             statusContainer.classList.remove('is-failure');
             statusContainer.classList.add('is-success');
           } else {
             statusContainer.classList.remove('is-success');
             statusContainer.classList.add('is-failure');
           }

           statusContainer.style.visibility = 'visible';
         }

         async function verifyBuyer(payments, token) {
           const verificationDetails = {
             amount: '1.00',
             billingContact: {
               addressLines: ['123 Main Street', 'Apartment 1'],
               familyName: 'Doe',
               givenName: 'John',
               email: '[email protected]',
               country: 'GB',
               phone: '3214563987',
               region: 'LND',
               city: 'London',
             },
             currencyCode: 'GBP',
             intent: 'CHARGE',
           };

           const verificationResults = await payments.verifyBuyer(
             token,
             verificationDetails
           );
           return verificationResults.token;
         }

         document.addEventListener('DOMContentLoaded', async function () {
           if (!window.Square) {
             throw new Error('Square.js failed to load properly');
           }

           let payments;
           try {
             payments = window.Square.payments(appId, locationId);
           } catch {
             const statusContainer = document.getElementById(
               'payment-status-container'
             );
             statusContainer.className = 'missing-credentials';
             statusContainer.style.visibility = 'visible';
             return;
           }

           let card;
           try {
             card = await initializeCard(payments);
           } catch (e) {
             console.error('Initializing Card failed', e);
             return;
           }

           async function handlePaymentMethodSubmission(
             event,
             paymentMethod,
             shouldVerify = false
           ) {
             event.preventDefault();

             try {
               // disable the submit button as we await tokenization and make a payment request.
               cardButton.disabled = true;
               const token = await tokenize(paymentMethod);
               let verificationToken;

               if (shouldVerify) {
                 verificationToken = await verifyBuyer(payments, token);
               }

               const paymentResults = await createPayment(
                 token,
                 verificationToken
               );

               displayPaymentResults('SUCCESS');
               console.debug('Payment Success', paymentResults);
             } catch (e) {
               cardButton.disabled = false;
               displayPaymentResults('FAILURE');
               console.error(e.message);
             }
           }

           const cardButton = document.getElementById('card-button');
           cardButton.addEventListener('click', async function (event) {
             // SCA only needs to be run for Card Payments. All other payment methods can be set to false.
             handlePaymentMethodSubmission(event, card, true);
           });
         });
       </script>
  </head>
  <body>
    <form id="payment-form">
      <div id="card-container"></div>
      <button id="card-button" type="button">Pay $1.00</button>
    </form>
    <div id="payment-status-container"></div>
  </body>
</html>

With a little modification, your code does work. I don’t understand why initializeCard() is declared twice. I did have to add some {literal} tags for the template compiler I’m using related to the error output in

`Tokenization errors: ${JSON.stringify(tokenResult.errors)}`

I will need to look at modifications related NOT calling createPayment() and instead, just modifying hidden form variables for the none and the verification token that can be passed to the server for the actual payment, creating customer, saving card, etc. since none of these typical actions are shown in your example scripts.

Not clear exactly why my code above failed on calling ‘paymentMethod.tokenize()’ and this version doesn’t…

It would sure be helpful if there was an actual specification of the available methods, their arguments and data types versus the snippets of sample code which can only be used in very simplistic environments… Any pointers to real docs?

One last question. How can I force this to see that I’m in a different locale? I’m using UK address in my verificationData, but the payment form seems to be looking at my browser and/or my IP to determine that I’m in US. I.e. I’d like it to display (and accept) ‘Postal code’ versus ‘Zip code (especially in sandbox environment)’. Zip doesn’t allow for letters like Canadian or UK postal codes. Hence the postal/zip isn’t really related to the billing info for the customer. Shouldn’t there be a way to use the billing address to initialize the form so it’s relative to the billing address?

Glad to hear that you got it to work. Have you seen the Technical Reference for the Web Payments SDK? That’s where all the specifications to the methods are.

As for forcing the SDK to think that you are in a different local you can use one of the EU test values. This will allow you to see Postal Code instead of Zip code. Or you can enter a valid UK, or Canadian card but be sure not to try and charge it as it will fail. The form will show Postal Code.

The call to verifyBuyer() is returning a verificationToken even when the verification should be a failure. How does one catch a verification failure so as not to defer a bad verification to the payment process? I.e. my testing is as follows:
1 - enter an EU test card
2 - Enter wrong verification code in the 2nd tier identification dialog
3 - Enter wrong verification code again (last attempt)
4 - verifcationToken is returned as: verf:CBASEKebI_uf4U8UP3aiN-LaOiI

Seems like it should be null if the verification failed… Enclosing the verifyBuyer() call in a try/catch does not generate an error. How/where do I see that the verifyBuyer() succeeded/failed?

:wave: Currently, the guidance is to not gate taking payments based on verify buyer themselves, and instead to include the verification token with the payment. By submitting as many payments as possible to the gateways, we aren’t blocking payments and letting the issuer decide whether they want to authorize given the authentication result. :slightly_smiling_face:

I get that (prefer you drop this mess entirely) but how do I catch the error and what is the return result once you unleash this “rule” on the world? I’d rather develop code that accounts for the specification rather than the current “rule”. Will a try/catch be the method to review the error and take appropriate action?

From what you’re saying is that the backend (Square) is returning a valid verification code even when the verification itself fails. Who makes this stuff up?

Is this correct methodolgy?

               if (shouldVerify) {
				   try {
	                 verificationToken = await verifyBuyer(payments, token);
				   } catch(e) {
					   cardButton.disabled = false;
					   $('#payment-status-container').html('<p>'+e.message+'</p>');
					   displayPaymentResult('FAILURE');
				   }
               }

For the cases when we will return an error in js through a callback, in this case we advise to retry verifyBuyer.
In other cases, when verification token is available we recommend proceeding with the payment and the backend will either decline right away, or proceed to authorization depending on verifyBuyer results.

Where do I set the callback? Why not use a try/catch? Can you provide a sample of how to see the error and how to get to the details? The spec is pretty silent on this.

You expect me to make a payment with a bad verficationToken so I can determine if the token is good or not? What happens when the “intent” is SAVE? Am I supposed to try to save the card with a bad token and then to wait for it to fail?

If the buyer does not verify (for whatever reason) it should throw an error that I can catch that says that the verification failed.

You are using the SqPaymentForm right? If so here is sample code:

paymentForm.verifyBuyer(
           nonce, 
           verificationDetails, 
           function(err, verificationResult) {
           ^^^ callback
            if (err == null) {
              //TODO: Move existing Fetch API call here
            }
      }); 

With the Web Payments SDK verifyBuyer is a promise so depending on your implementation you would have to try/catch around the promise if you’re using async/await or the older way which is .catch() the promiseVerifyBuyer will return a VerifyBuyerError if there’s an error.

No, I’m trying to use the new web-payments so I don’t have to redo this when you stop supporting SqPaymentForm.

Given this info: https://developer.squareup.com/reference/sdks/web/payments/errors/VerifyBuyerError
it would appear that the errors are “thrown” and caught via try/catch. Can you confirm that this is correct and explain why a conscious verifcation failure does not generate the error (especially in the sandbox for testing).

Yes, the errors are “thrown” and caught via try/catch. At this time I don’t have any additional information as to why a verification failure doesn’t always generate an error but I have shared this with the team.