stripe-samples / saving-card-without-payment

How to build a form to save a credit card without taking a payment.
https://stripe.com/docs/payments/save-and-reuse
MIT License
127 stars 68 forks source link

The forms are extremely ugly and unnecessarily complex #10

Closed britisharmy closed 4 years ago

britisharmy commented 4 years ago

I want to collect card information from customers. I have my form created in bootstrap already setup. The problem is, before you figure out how to connect the token, the form fields to post, you shall have looked at tonnes of code to follow up, install a myriad of tools and good luck with getting it to work.

You should have a simple,example where its just the traditional html, simple javascript setup and not to limit your examples to the SPA madness. believe me, the rest of the world is fine without SPA and frankly, the examples are a big waste of time and its not what customers expect. From cards to bank integration, the nightmare is the same across the board.

adreyfus-stripe commented 4 years ago

@britisharmy

Hi there! Most of our samples don't require SPAs (in fact this sample that you filed an issue for just uses HTML / JS for a very simple frontend).

Many of our samples require setting up a server because collecting payments involves handling very sensitive information like pricing and customer information that you don't want to be easily manipulated from the client. If you're looking for the most simple and straightforward way to process payments I'd recommend looking at Checkout, which is a prebuilt payment form you can redirect your customer to. There's even a client-only implementation.

Docs: https://stripe.com/docs/payments/checkout/client Sample: https://github.com/stripe-samples/checkout-one-time-payments/tree/master/client-only/client/html

Hope that helps!

britisharmy commented 4 years ago

Wow, so what you are basically saying is that,i either accept the one line of a form or just piss off? Right?

My users aren't very sophisticated and there is 3D to take care off later so it would really help if you considered having vertical and well spaced forms that you have imposed on people.

I am scratching my head on how you arrived at the decision that one line of mount a form without considering people who are used to simple interfaces would say.

Anyway, thanks for taking time to point me to obvious documents i have looked at before, oh those are the cows coming home, that's how long i have been here reading the official docs.

cjavilla-stripe commented 4 years ago

@britisharmy Hey there. Can you share what you mean by accepting the one line of form? Curious which bit of the sample you're referring to. If you're interested, I'd be happy to jump on a zoom call to talk through how to use this.

britisharmy commented 4 years ago

Can this be styled to occupy a field of its own. Currently, card number,expiry and postal code are inside what looks like one line of input. My question is therefore, can this be done so that card number,expiry and postal code occupy there own input field?

ol

I also would like to add card holder name and leave out postal code.

Is this possible, maybe i am looking in the wrong places but i haven't seen that addressed in stripe elements docs.

Please don't offer me alternatives you may have that prevents me from having what i really want, this is all i wanna know.

Also important

Its said in this video https://www.youtube.com/watch?v=95qSebQrm5E&t=31s that setUpIntent is depreceated and this code hasn't been updated https://github.com/stripe-samples/developer-office-hours/tree/master/2019-08-28-save-and-reuse-cards

In this code

stripe.handleCardSetup(
          setupIntent.client_secret, cardElement, {
            payment_method_data: {
              billing_details: {name: cardholderName.value}
 }

The error

Uncaught TypeError: Cannot read property 'client_secret' of undefined
    at HTMLButtonElement.<anonymous> ((index):60)
cjavilla-stripe commented 4 years ago

Hey britisharmy. Yes. You can use separate input controls for each of the number, expiry, cvc, and postal code.

While recommended to help reduce fraud, the postal code is not strictly required so you could leave out that input if you'd like.

You can collect the card holder name in a typical text input and pass it's value in the billing details as shown.

I made that video and added the deprecation warning. SetupIntent's aren't deprecated, but the method name handleCardSetup is deprecated and it was replaced by confirmCardSetup [1]. In the interest of not just sending you links, I've included an example call below.

Note that the value for cardElement can either be a card element which is the one line input, or it can be a cardNumber element which would be just the element mounted for collecting only the card number (without the expiry/cvv/postal).

Here is a relatively simple example for how to mount separate elements for that input: https://jsfiddle.net/ywain/o2n3js2r/

The idea is that you have separate HTML div's for each of the input controls with unique ID's

<label>
  <span>Card number</span>
  <div id="card-number-element"></div>
</label>
<label>
  <span>Expiry date</span>
  <div id="card-expiry-element"></div>
</label>

Then you create an element for each of those types: cardNumber, cardExpiry, cardCvc and mount to that html element.

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

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

Then when you call confirmCardSetup, you pass in the cardNumberElement like this:

stripe
  .confirmCardSetup(setupIntent.client_secret, {
    payment_method: {
      card: cardNumberElement,
      billing_details: {
        name: cardholderName.value,
      },
    },
  })
  .then(function(result) {
    // Handle result.error or result.setupIntent
  });

Hope that helps. Can you confirm which backend language you're using? (We need to re-record that video anyways to use the updated client side methods, and I might be able to do it in your language of choice).

[1] https://stripe.com/docs/js/setup_intents/confirm_card_setup

britisharmy commented 4 years ago

I am using php to see how the stripe SCA works but we use spring boot as a backend. Thanks for taking time to explain.

In the code i posted, i changed nothing and i am getting that error. I got the code from https://github.com/stripe-samples/developer-office-hours/tree/master/2019-08-28-save-and-reuse-cards

As such, i can't post anything to the backend.

cjavilla-stripe commented 4 years ago

Thanks for reporting that error. It seem the JSON returned from that /setup_intents API call is returning the JSON and some html for an error related to some unrelated deprecation warnings. I'm going to update and push a new version now to https://github.com/stripe-samples/developer-office-hours/tree/master/2019-08-28-save-and-reuse-cards

Sorry for the churn. Can you try pulling and testing with that new code, @britisharmy

britisharmy commented 4 years ago

I have pulled and it has worked. I would make this request though. Since SCA has emerged as an industry standard, almost everyone processing cards via stripe MUST look at this repo to know how to handle the three steps of SCA authentication since its not obvious without prior experience. Keeping this repo up to date shall save people a lot of time.

cjavilla-stripe commented 4 years ago

@britisharmy great point and I'll make an effor to improve there. Let us know if you have any further questions or concerns.

britisharmy commented 4 years ago

I have another issue. The code you gave for setting individual fields works but now i need to pass card details when the button is clicked.

This is my code

 //Pass the SetupIntent’s client secret to the client
      var setupIntent;
      debug('Fetching setup intent');
      fetch('/stripe_create_intent').then(function(r) {
        return r.json();
      }).then(function(response) {
        console.log(response);
        setupIntent = response;
        debug('Fetched setup intent: ' + response.id);
      });

    var stripe = Stripe('');
var elements = stripe.elements();

var style = {
  base: {
    iconColor: '#666EE8',
    color: '#31325F',
    lineHeight: '40px',
    fontWeight: 300,
    fontFamily: 'Helvetica Neue',
    fontSize: '15px',

    '::placeholder': {
      color: '#CFD7E0',
    },
  },
};

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

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

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

//Submit the card details to Stripe from the client
      var cardholderName = document.getElementById('cardholder-name');
      var cardButton = document.getElementById('card-button');
      // rely on setup intent fetched above instead.
      // var clientSecret = cardButton.dataset.secret;

      cardButton.addEventListener('click', function(ev) {
        ev.preventDefault();
        debug('handling card setup...');
        // Note: stripe.handleCardSetup was deprecated. Instead, use confirmCardSetup
        // https://stripe.com/docs/js/setup_intents/confirm_card_setup
        //
        // Old code from the video for reference:
        // stripe.handleCardSetup(
        //   setupIntent.client_secret, cardElement, {
        //     payment_method_data: {
        //       billing_details: {name: cardholderName.value}
        //     }
        //   }
        // ).then(function(result) {

        // new code that uses confirmCardSetup:
        stripe.confirmCardSetup(
          setupIntent.client_secret, {
            payment_method: {
              card: cardElement,
              billing_details: {
                name: cardholderName.value,
              },
            },
          }
        ).then(function(result) {
          console.log(result);
          if (result.error) {
            debug(result.error.message);
            // Display error.message in your UI.
          } else {
            debug('Setup succeeded');
            debug('Setup Intent Payment Method: ' + result.setupIntent.payment_method);
            // The setup has succeeded. Display a success message.
            createCustomer(result.setupIntent.payment_method);
          }
        });
      });

      function createCustomer(paymentMethod) {
        debug('Creating customer...');
        fetch('/create_customer', {
          method: 'POST',
          body: JSON.stringify({
            payment_method: paymentMethod
          })
        }).then(function(r) {
          return r.json();
        }).then(function(response) {
          console.log(response);
          if(response.error) {
            debug(response.error.message);
          } else {
            debug('Created customer: ' + response.id);
          }
        });
      }

      function debug(message) {
        var debugMessage = document.getElementById('card-message');
        console.log('DEBUG: ', message);
       // debugMessage.innerText += "\n" + message;
      }

function setOutcome(result) {
  var successElement = document.querySelector('.success');
  var errorElement = document.querySelector('.error');
  successElement.classList.remove('visible');
  errorElement.classList.remove('visible');

  if (result.token) {
    // In this example, we're simply displaying the token
    successElement.querySelector('.token').textContent = result.token.id;
    successElement.classList.add('visible');

    // In a real integration, you'd submit the form with the token to your backend server
    //var form = document.querySelector('form');
    //form.querySelector('input[name="token"]').setAttribute('value', result.token.id);
    //form.submit();
  } else if (result.error) {
    errorElement.textContent = result.error.message;
    errorElement.classList.add('visible');
  }
}

cardNumberElement.on('change', function(event) {
  setOutcome(event);
});

cardExpiryElement.on('change', function(event) {
  setOutcome(event);
});

cardCvcElement.on('change', function(event) {
  setOutcome(event);
});

This is the html code

    <div class="group">
    <br/>
     <label>
      <span>Names On Card</span>
      <input id="cardholder-name" type="text" class="field" >
      </label>
      <label>
        <span>Card number</span>
        <div id="card-number-element" class="field"></div>
      </label>
      <label>
        <span>Expiry date</span>
        <div id="card-expiry-element" class="field"></div>
      </label>
      <label>
        <span>CVC</span>
        <div id="card-cvc-element" class="field"></div>
      </label>
      <label>
        <span>Postal code</span>
        <input id="postal-code" name="postal_code" class="field"  />
      </label>
      <button class="btn btn-success mb-3" id="card-button" ><i class="fas fa-credit-card"></i> Add Card</button>
    </div>

Since each field is being mounted individually, how then can we submit all form fields as one including the card holder names?

 stripe.confirmCardSetup(
          setupIntent.client_secret, {
            payment_method: {
              card: cardElement,
              billing_details: {
                name: cardholderName.value,
              },
            },
          }
        ).then(....

In the code above, the card details were held in cardElement. How can i pass the other card details since they are now in seperate fields?.

cjavilla-stripe commented 4 years ago

Solid question that I should've addressed when showing the multi element code. It turns out you don't actually need to pass the other elements anywhere and by passing the cardNumber element, Stripe.js knows to find the other mounted elements and include their values when making the client side API call to Stripe to tokenize and confirm setup.

britisharmy commented 4 years ago

Okay great. I also noted you never mounted card name while mounting other fields and so,in my code, its returning empty when i submit.

I get this error

DEBUG: You passed an empty string for 'payment_method_data[billing_details][name]'. We assume empty values are an attempt to unset a parameter; however 'payment_method_data[billing_details][name]' cannot be unset. You should remove 'payment_method_data[billing_details][name]' from your request or supply a non-empty value.

cjavilla-stripe commented 4 years ago

Card name? Is that the cardholder's name? We don't have a Stripe element for that as it's not sensitive, so you'd use a typical input text field for that

<input type="text" id="cardholder">
var cardholderNameInput = document.getElementById('cardholder');
// then use cardholderNameInput.value;
britisharmy commented 4 years ago

Finally, in the php code using slim php framework,

//Attach the PaymentMethod to a Customer after success
$app->post('/create_customer', function(Request $request, Response $response) use ($app)  {
  $params = json_decode($request->getBody());
  try {
    $customer = \Stripe\Customer::create([
      'payment_method' => $params->payment_method,
    ]);
  } catch (Exception $e) {
    return $response->withJson($e->getJsonBody());
  }
  return $response->withJson($customer);
});

In this line,

$params = json_decode($request->getBody());

what does it translate to in plain php?

I checked what the slim php docs say here http://www.slimframework.com/docs/v2/request/body.html and i can't immidiately translate this to what i am doing.

cjavilla-stripe commented 4 years ago

I'm not super familiar with php, but I think this is the translation from slim to vanilla php:

// read the post data from the incoming POST request as a string
$postdata = file_get_contents("php://input");

// decode the string into an associative array
$params = json_decode($postdata);
//.. 
britisharmy commented 4 years ago

I have used codeigniter input class and it worked. The customer created, is not showing up on the stripe test customer tab although in your debug messgae, it displays customer DEBUG: Created customer:

Should the customer created show up in the stripe stripe account testing tab?

Also,in this code

  stripe.confirmCardSetup(
          setupIntent.client_secret, {
            payment_method: {
              card: cardNumberElement,
              billing_details: {
                name: cardholderName.value,
              },
            },
          }
        ).then(function(result) {
          console.log(result);
          if (result.error) {
            debug(result.error.message);
            // Display error.message in your UI.
          } else {
            debug('Setup succeeded');
            debug('Setup Intent Payment Method: ' + result.setupIntent.payment_method);
            // The setup has succeeded. Display a success message.
            createCustomer(result.setupIntent.payment_method);
          }
        });

Can this createCustomer(result.setupIntent.payment_method); be offloaded to a async/await?

The reason for that, is that, i want to get

let pm = result.setupIntent.payment_method let customer_id = await createCustomer...

and post this to my back end as a pair.

cjavilla-stripe commented 4 years ago

Hmm. If that doesn't show the ID of the customer, that might have failed. Can you look in the network tab and see if that API request succeeded?

britisharmy commented 4 years ago

I am getting back the customer id and 200 on the network tab meaning two things. I can only get customer id and 200 code if the customer creation was carried out successfully. Problem is, i can't find the customer created anywhere on my account.

britisharmy commented 4 years ago

Sorted: To appear in the backend, i must supply at least an emal and description. I did and it worked.

cjavilla-stripe commented 4 years ago

okay great.

britisharmy commented 4 years ago

Hello, i have a problem with catching exceptions in php. I have this code in codeigniter php but its mostly ordinary php

<?php 
public function make_payment(){
        $stripe = new \Stripe\StripeClient(
          'sk_test_key'
        );
    //Read from billing table pending bills and pay: where bill is unpaid
    $query = $this->db->query("select * from billing JOIN payments_data ON payments_data.user_id=billing.user_id where billing.billing_status='unpaid' and payments_data.is_default='default'");

    foreach ($query->result() as $row)
    {
            echo $row->payment_method.'<br/>';
            echo $row->customer_id.'<br/>';
            echo $row->amount_billed.'<br/>';
            echo $row->user_id.'<br/>'.'<hr/>';

            //make payment
                try {
                $uid = $row->user_id;
 $intent = $stripe->paymentIntents->create([
      'amount' => $row->amount_billed,
      'currency' => 'usd',
      'payment_method_types' => ['card'],
      'customer' => $row->customer_id,
      'payment_method' => $row->payment_method,
      'off_session' => true,
      'confirm' => true,
    ]);

    /**
    if($intent->status == 'successeded'){
        ..insert values into the database saying payment succeeded
    }
    else{
        ..insert values into database saying payment failed
    }
    */

  } catch (Exception $e) {

      /**
      Two types of exceptions 

      1. Card has no funds or anything not related to 3d lack of authorization
      2. 3D auth required

      In each, i want to log info into the database
    */

         $data = array(
        'last_updated' => time(),
        'billing_status' => 'unpaid',
        'billing_comments' => $e->getMessage(),
        );

        $this->db->where('user_id', $row->user_id);
        $res = $this->db->update('billing', $data);

      echo '<pre>';
      print_r($e->getMessage());
      echo '</pre>';
      echo '<br/>';
      echo '<pre>';
      echo json_encode($e->getJsonBody());
      echo '<br/>';
      print_r($e->getJsonBody()['error']['payment_intent']['id']);
      echo '<br/>';
      print_r($e->getJsonBody()['error']['message']);
      echo '<br/>';
      print_r($e->getJsonBody()['error']['payment_intent']['customer']);
      echo '</pre>';

      $payment_intent_id = $e->getJsonBody()['error']['payment_intent']['id'];
      $payment_link = base_url('info/stripe_3d_authorize/').$payment_intent_id.'/'.$row->payment_method;
      $data = array(
        'user_id' => $row->user_id,
        'payment_link' => $payment_link,
        'last_updated' => time()
        );

    $this->db->insert('3d_payments_links', $data);

  }

    }
    }

When charging has succeeded i want to log that into the database and also log any failure not related to lack of 3D authorization. I am currently using the exception handling. In the try, thats where i am charging and catches the exception. However, there are sevetral exceptions i would like to log differently.

  1. Ordinary lack of funds or anything not related to lack of 3D authorization.
  2. Lack of 3D authorization

In my code 1 and 2 are happening inside the catch block. If the error was lack of funds and not 3D auth, i am going to log an error thus the need to seperate those two possible errors.

Also, i would like to have a sort of if so that i can also log succcess well for instance

if($charge->status == 'succeeded'){
..do something
}
else{
..do something
}

Finally, lets say the customer has gone though the 3D authorisation and finally authorised, but the card do not have funds,i realize the link once authorized cannot be reused again. Can we log that the payment even after 3D auth failed and how can we handle that?. That is very important in my view.

Update

I did a simple if statement with te error i knew about and it worked

public function make_payment(){
      $stripe = new \Stripe\StripeClient(
          'sk_test_...'
        );
    //Read from billing table pending bills and pay: where bill is unpaid
    $query = $this->db->query("select * from billing JOIN payments_data ON payments_data.user_id=billing.user_id where billing.billing_status='unpaid' and payments_data.is_default='default'");

    foreach ($query->result() as $row)
    {
        /**
            echo $row->payment_method.'<br/>';
            echo $row->customer_id.'<br/>';
            echo $row->amount_billed.'<br/>';
            echo $row->user_id.'<br/>'.'<hr/>';
            */

            //make payment
                try {
                $uid = $row->user_id;
 $intent = $stripe->paymentIntents->create([
      'amount' => $row->amount_billed,
      'currency' => 'usd',
      'payment_method_types' => ['card'],
      'customer' => $row->customer_id,
      'payment_method' => $row->payment_method,
      'off_session' => true,
      'confirm' => true,
    ]);

  } catch (Exception $e) {

      if($e->getMessage() == 'Your card was declined. This transaction requires authentication.'){
              $payment_intent_id = $e->getJsonBody()['error']['payment_intent']['id'];
      $payment_link = base_url('info/stripe_3d_authorize/').$payment_intent_id.'/'.$row->payment_method;
      $data = array(
        'user_id' => $row->user_id,
        'payment_link' => $payment_link,
        'last_updated' => time()
        );

    $this->db->insert('3d_payments_links', $data);

    echo $e->getMessage().'3d error'.'<br/>';

      }
      else{

         $data = array(
        'last_updated' => time(),
        'billing_status' => 'unpaid',
        'billing_comments' => $e->getMessage(),
        );

        $this->db->where('user_id', $row->user_id);
        $res = $this->db->update('billing', $data);
        echo $e->getMessage().'Ordinary Error';
      }

        /**
      echo '<pre>';
      print_r($e->getMessage());
      echo '</pre>';
      echo '<br/>';
      echo '<pre>';
      echo json_encode($e->getJsonBody());
      echo '<br/>';
      print_r($e->getJsonBody()['error']['payment_intent']['id']);
      echo '<br/>';
      print_r($e->getJsonBody()['error']['message']);
      echo '<br/>';
      print_r($e->getJsonBody()['error']['payment_intent']['customer']);
      echo '</pre>';
      */
     }
    }
    }

The last question though, in some preapid cards, the balance at times might not be enough to cover the amount required. Do we have a trst scenario to make a 3D card to have insufficient funds?