Stripe.js and hcaptcha where hcaptcha is tied to button

1.1k views Asked by At

Code:

<!DOCTYPE html>

<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>

  

<form id="payment-form" action="." method="post">
    <input type="hidden" name="csrfmiddlewaretoken" value="06dAlJhwvSjVK7u4CZcyAbsa9Ikn9EYsiFGxFpA8z3W2wsr0UMKb8KIxNmYbyFYg">
    <p><label for="email">Email:</label> <input type="email" name="payment_email" value="[email protected]" class="form-control" autocomplete="off" id="email" readonly="readonly" required></p>
<p><label for="phone">International Phone:</label> <input type="tel" name="payment_phone" class="form-control" autocomplete="off" id="phone" placeholder="International Phone" required></p>
<p><label for="cardholder-name">Full Name:</label> <input type="text" name="payment_name" class="form-control" autocomplete="off" id="cardholder-name" placeholder="Full Name" required></p>
<p><label for="line1">Address Line 1:</label> <input type="text" name="payment_address1" class="form-control" autocomplete="off" id="line1" placeholder="Address 1" required></p>
<p><label for="line2">Address Line 2:</label> <input type="text" name="payment_address2" class="form-control" autocomplete="off" id="line2" placeholder="Address 2"></p>
<p><label for="city">City:</label> <input type="text" name="payment_city" class="form-control" autocomplete="off" id="city" placeholder="City" required></p>
<p><label for="state">State or Province:</label> <input type="text" name="payment_state" class="form-control" autocomplete="off" id="state" placeholder="State or Province" required></p>
<p><label for="country">2 Letter Country Code (<a href="https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements" target="_blank" rel="noopener noreferrer nofollow">Find Your Country Code</a>):</label> <input type="text" name="payment_country" class="form-control" autocomplete="off" id="country" maxlength="2" placeholder="Country Code" required></p>


      <script nonce="tKD42vLlpXoOTvs+kP4PUQ==" src="https://hcaptcha.com/1/api.js" async defer></script>

<script nonce="tKD42vLlpXoOTvs+kP4PUQ==">
  function onSubmit(token) {
      if ( window.history.replaceState ) {
        window.history.replaceState( null, null, window.location.href );
      }
        var form = document.getElementById("payment-form");
        document.getElementById("card-button").disabled = true;
        form.submit();
        if (event.error) {
            document.getElementById("card-button").disabled = false;
        }
  }
</script>



            <label>
              Credit or Debit Card:
            </label>
            <div id="card-element" class="form-control stripe_card_padding">
            <!-- A Stripe Element will be inserted here. -->


        </div>
        <!-- Used to display form errors. -->
        <div id="card-errors" class="stripe_card_errors" role="alert" ></div>



        <!-- <div id="stripe-result-handler" class="is-hidden">
          Success! Got token: <span class="result"></span>
        </div> -->


        <button
          id="card-button"
          type="submit"
          class=" form-control payment_button h-captcha"
          data-sitekey="8b425911-fa93-429c-a98c-4c56b93b6662"
          data-callback="onSubmit"
        >
          Submit Payment
        </button>

      </form>



  <script nonce="tKD42vLlpXoOTvs+kP4PUQ==" src="https://js.stripe.com/v3/"></script>
  <script nonce="tKD42vLlpXoOTvs+kP4PUQ==">
  // Create a Stripe client.
    var stripe = Stripe("pk_test_tcuxCLB8Y8NN8H3OPcR6ALKh00UjP2WfYt");

    // Create an instance of Elements.
    var elements = stripe.elements();
    var cardButton = document.getElementById("card-button");
    var cardholderName = document.getElementById("cardholder-name");
//var cardElement = elements.create("card");
    //var cardElement = elements.create("card");
    //cardElement.mount("#card-element");
    var line1 = document.getElementById("line1");
    var line2 = document.getElementById("line2");
    var city = document.getElementById("city");
    var country = document.getElementById("country");

    var email = document.getElementById("email");
    var phone = document.getElementById("phone");
    var state = document.getElementById("state");


var style = {
  base: {
    color: 'black',
    iconColor: 'black',
    fontSize: '15px',
    fontFamily: '"Roboto", sans-serif',
    fontSmoothing: 'antialiased',
    '::placeholder': {
      color: 'black',
    },
  },
  invalid: {
    color: '',
    ':focus': {
      color: '',
    },
  },
};




var cardElement = elements.create('card', {style: style});
cardElement.mount('#card-element');



    // Add an instance of the card Element into the `card-element` <div>.
    // Handle real-time validation errors from the card Element.
    cardElement.addEventListener("change", function (event) {
      var displayError = document.getElementById("card-errors");
      if (event.error) {
        displayError.textContent = event.error.message;
      } else {
        displayError.textContent = "";
      }
    });
    // Handle form submission.
    //.value.trim() || null is needed to change the form input to required/optional for data collection. Was originally .value only.
    var form = document.getElementById("payment-form");
    form.addEventListener("submit", function (event) {
      event.preventDefault();
      document.getElementById("card-button").disabled = true;
      var billingInfo = {
      billing_details: {
        name: cardholderName.value.trim() || null,
        address: {
          line1: line1.value.trim() || null,
          line2: line2.value.trim() || null,
          city: city.value.trim() || null,
          state: state.value.trim() || null,
          country: country.value.trim() || null,
        },
        email: email.value,
        phone: phone.value.trim() || null,
      }
    };
      stripe.createPaymentMethod('card', cardElement, billingInfo).then(function (result) {
        if (result.error) {
          // Inform the user if there was an error.
          var errorElement = document.getElementById("card-errors");
          errorElement.textContent = result.error.message;
          document.getElementById("card-button").disabled = false;
        } else {
          // Send the token to your server.
          //setTimeout(stripeSourceHandler(result.paymentMethod.id), 3000);
          stripeSourceHandler(result.paymentMethod.id);
        }
      });
    });


    // Submit the form with the source ID.
    function stripeSourceHandler(payment_method_id) {
      var form = document.getElementById("payment-form");
      var hiddenInput = document.createElement("input");
      hiddenInput.setAttribute("type", "hidden");
      hiddenInput.setAttribute("name", "PaymentMethod");
      hiddenInput.setAttribute("value", payment_method_id);
      form.appendChild(hiddenInput);
      // Insert the source ID into the form so it gets submitted to the server
      // Submit the form
      form.submit();
    }


  </script>

https://jsfiddle.net/g5nkcL1u/

I am trying to do this with hcaptcha:

https://docs.hcaptcha.com/invisible#automatically-bind-the-challenge-to-a-button

And I am trying to combine it with stripe.js.

I think the problem has to do with data-callback for the button. Since there are 2 separate form.submit(); lines in my code, it does not know which one to run.

My question is, how do I combine my stripe javascript with my hcaptcha javascript to get them to work together?

Errors:

'No such PaymentMethod' -> If I use data-callback="stripeSourceHandler"

'Unrecognized request URL (POST: /v1/payment_methods//attach)' -> if I use data-callback="onSubmit"

I don't believe this has anything to do with my server code. If I delete all hcaptcha code on my client side everything works server side. If I use the regular hcaptcha checkbox widget instead of tying to a button, everything works since there is no data-callback. But I want to tie hcaptcha to a button because it is better UX compared to the checkbox widget.

I use the onSubmit function in my other form webpages and hcaptcha works fine since stripe is not involved in those other form webpages.

Other ideas:

onSubmit does not call the part of the code that creates the Stripe PaymentMethod. Have the creation happen in its own function like callStripe() and then call that PaymentMethod create function after the captcha part.

Call hcaptcha.render() to show the captcha and after that create the PaymentMethod and submit the form

1

There are 1 answers

6
Nolan H On

What actually triggers the captcha flow? The onSubmit doesn't appear to have anything explicitly connected to that. Otherwise you ought to be able to chain the sequence of 3 blocks together:

  1. Allow hcaptcha to bind and call onSubmit (or some other function), remove the form.submit() call
  2. Split your form event handler callback into a standalone function and call that after the captcha
  3. stripeSourceHandler should already be called as a callback

Note that you'll also want to make sure you verify the hcaptca result server-side.

Separately, do you not have a problem with recursion? Your form submit listener (form.addEventListener("submit", ...)) ends up calling stripeSourceHandler which itself calls form.submit().