Using STRIPE Elements PHP API to collect card details and later handle off-session payment

This is a walk through of the PHP & Javascript code required for collecting and storing card details using ready for a later payment while the user is offline/"off-session".

warning WARNING: This code is not 100% working code and is still being worked on. Handle with care.

The code here is using the STRIPE developers test environment and will not collect real funds.

As a pre-requisit for your own coding, you will need to have created a STRIPE account and acquired the public and secret keys from your STRIPE dashboard.
You will also have used composer to install the STRIPE API into your application vendor folders using the command line composer require stripe/stripe-php

The code is broken down into a few steps to set-up a customer on the STRIPE environment and then create a form to collect the credit card details that will be used by a background billing process later; use the STRIPE test cards listed below - or found on https://stripe.com/docs/testing#cards.

Note, because this is a demonstrator, little effort has been put into disabling buttons during processing - when going live you will want to consider patching these kinds of UX issues in order to avoid double-clicking purchase, etc.

To run stand-alone, outside of this explanation frame, click here.

STRIPE fees in the UK are: 1.4% + 20p for UK cards (+1.1% for European Economic Area) / 2.9% + 20p for international cards.

Written: July-2022 PHP: 7+ STRIPE.js: v3

Code:

The code starts off with working out if the login customer already exists on STRIPE, or not. If it does not, then it creates the customer.

stripe-elements-checkout-offsession-code.php :: Create customer
$json = $stripe->customers->create(
    [
        'email' => $email,
        'name'  => $customerNm,
    ]
);
//echo "<LI>417:<PRE>" . print_r($json, true) . "</PRE>";
$customerId = $json->id;

The code here is using a dummy play of a customer id stored in a cookie on your browser to avoid having to code up login screens for the demonsrtation. The code also uses a simple server-side file system storage of STRIPE customer ids to assist in locating the current login users customer id (as the STRIPE search facility seems to not be designed to work after a customer is created [there may be a better way to do this but it is not clear at this point in time]).

Once the customer id is known, the code will present the user a list of any card payment-methods already set up on STRIPE against their customer.

stripe-elements-checkout-offsession-code.php :: List customers payment methods
$json = $stripe->customers->allPaymentMethods($customerId, ["type" => "card"]);
$cardsFound = false;
$disabled = "disabled";
$selectedPMI = "";


if ( strcmp($_SERVER['REQUEST_METHOD'], "GET")==0 ) {

    echo "<form method='post' action='$script' name='paymentform'>";
    if (1) {
        echo "<input type='hidden' name='action' value='payment'>";
        echo "<input type='hidden' name='step' value='paymentIntent'>";
        echo "<input type='hidden' name='customerId' value='$customerId'>";

        if ($json and isset($json->data[0])) {
            echo "<table>";
            foreach ($json->data as $paymentMethod) {
                //echo "442:<PRE>$paymentMethod</PRE>";
                $cardsFound = true;
                $id = $paymentMethod->id;
                $cardbrand = $paymentMethod->card->brand;
                $expy = $paymentMethod->card->exp_year;
                $expm = $paymentMethod->card->exp_month;
                $last4X = $paymentMethod->card->last4;

                $last4 = "**** **** **** $last4X";
                $expires = "Expires " . date("M/y", strtotime("$expy-$expm-01"));

                if (isset($_POST['selected-card-PMI']) and strcmp($_POST['selected-card-PMI'], $id) == 0) {
                    $selected = "checked";
                    $disabled = "";
                    $selectedPMI = $id;
                }
                else $selected = "";


                $cardlogo = "<img src='https://methodfish.com/images/card_$cardbrand.png' class='card'>";
                $note = $this->getCardHelperNote($last4X);
                echo "<tr><td>";
                echo "<input type='radio' $selected name='selected-card-PMI' id='$id' value='$id' onClick='doEnablePayment()'>";
                echo "</td>";
                echo "<td>";
                echo "<label for='$id'>$cardlogo - $last4 $expires $note</label><br>";
                echo "</td></tr>";
            }
            echo "</table>";
        }

        if (!$cardsFound) echo "<p>no cards found</p>";

        echo "<HR><button id='new' onClick='doDisableButtons(); window.location.href=\"$script?action=add\"'>Add New Card</button>";

        if ($cardsFound) echo "<button id='payment' $disabled type='submit' onClick='paymentform.submit(); doDisableButtons(); '>Make Payment...</button>"; // This button will submit the form
    }
    echo "</form>";

The user can then add another card via the doAddCard() function that performs front-end processing, or choose to make a payment from one of the existing ones using the doMakePayment() function, that performs back-end processing.

stripe-elements-checkout-offsession-code.php :: user action branch
if (isset($_GET['action'])) {
    switch ($_GET['action']) {
        case "add":
            $this->doAddCard($stripe, $customerId, $stripe_publickey);
            break;
    }
}

if (isset($_POST['action'])) {
    switch ($_POST['action']) {
        case "payment":
            $this->doMakePayment($stripe, $customerId, $selectedPMI, $stripe_publickey);
            break;
    }
}

Adding a card is handled through the doAddCard() example function and involves creating a payment_form <form> element, with an underlying <div> element that is then populated by the stripe.js functions called by the stripe.elements(options) and elements.create('payment') calls, which will result in a set of card input fields that the user can fill out and submit.

Note, using elements.create('card') would have resulted in a much reduced input form.

The submission of the add-card ['payment_form'] form will be caught by an event listener that will use the stripe.confirmSetup() function to verify the inputs, potentially call on authentication from the underlying bank, and then save the payment method on STRIPE - redirecting the user to the return_url once this is done.

stripe-elements-checkout-offsession-code.php :: Add Card
public function doAddCard($stripe, string $customerId, string $stripe_publickey) {

    try {

        $json = $stripe->setupIntents->create(['customer' => $customerId, 'payment_method_types' => ['card']]);

        $intentId = $json->id;
        $clientSecret = $json->client_secret;

        echo /**@lang HTML */ "
        
            <p class='info colPaleGreyBg'>
            HINT: Use the Test card options listed below (e.g. 4242424242424242 - expires 12/33, CVC 123.
            See <a href='https://stripe.com/docs/testing#cards' target='_blank'>STRIPE.com</a> for more test cards.
            </p>
            <p style='color:red'><i class='material-icons' style='vertical-align: middle; color: red'>warning</i> Never use real cards on this test page.</p>

            <form id='payment_form' data-secret='$clientSecret'>
                <div id='payment-element'>
                  <!-- Elements will create form elements here-->
                </div>
                
                <div id='error-message'></div>
                <button id='submit'>Save Card</button>
                <button id='cancel' type='button'>Cancel</button>
            </form>
            
            ";


        $url=$_SERVER['SCRIPT_NAME'];

        // See https://stripe.com/docs/payments/save-and-reuse?platform=web#web-collect-payment-details
        echo /**@lang Javascript */ "
            
            <script src='https://js.stripe.com/v3/'></script>
            <script>

            const stripe = Stripe('$stripe_publickey');
            const options = {
                  clientSecret: '$clientSecret'
                };
              
                
            // Set up Stripe.js and Elements to use in checkout form, passing the client secret obtained in step 2
            const elements = stripe.elements(options);
            
            // Create and mount the Payment Element
            const paymentElement = elements.create('payment');
            paymentElement.mount('#payment-element');
            paymentElement.on('ready', (e) => paymentElement.focus());
            
            const form = document.getElementById('payment_form');

            document.getElementById('cancel').addEventListener('click', async (event) => {
                doDisableButtons();
                event.preventDefault();
                window.location.href=window.location.origin+window.location.pathname;
            });
            
            
            form.addEventListener('submit', async (event) => {  // <---------------------
                event.preventDefault();
                
                var target=document.getElementById('submit');
                const messageContainer = document.querySelector('#error-message');
                messageContainer.style.padding = '';
                target.disabled=true;
                target.innerHTML='Checking...';
                
                document.getElementById('error-message').innerHTML='';
                
                const {error} = await stripe.confirmSetup({ // <-------------------------
                        elements,
                        confirmParams: {
                            return_url: window.location.origin+window.location.pathname+'?card=saved' // It looks like this would ignore an iframe if the processing is in an iframe
                        }
                    });
                
                
                if (error) {
                    // This point will only be reached if there is an immediate error when
                    // confirming the payment. Show error to your customer (for example, payment
                    // details incomplete)
                    messageContainer.textContent = error.message;
                    messageContainer.style.padding = '10px 20px 10px 20px';
                    target.disabled=false;
                    target.innerHTML='Save';
                } 
                else {
                    // Your customer will be redirected to your `return_url`. For some payment
                    // methods like iDEAL, your customer will be redirected to an intermediate
                    // site first to authorize the payment, then redirected to the `return_url`.
                    alert('This never happens');
                }
            })
            ;
            </script>
        ";
    }
    catch (Exception $e) {
        echo "<LI>Payment method creation failed " . $e->getMessage();
    }

}

Making a payment is handled through the doMakePayment() example function that responds with calling on STRIPE to create a payment-intent; as this is an off-session example, the code here is purely PHP and expected to run in the background - no authentication interactions from the user can occur here.

If the payment requires authentication, in live mode the user would be asked to verify their identity with a push notification, a text message, or another method chosen by their bank. This would then require a webhook in place to respond to the authentication response.

stripe-elements-checkout-offsession-code.php :: Make Payment
public function doMakePayment($stripe, string $customerId, string $paymentMethodId, string $stripe_publickey) {
    // NOTE: We have already verified $paymentMethodId belongs to $customerId

    if ( strcmp($_POST['step'], "paymentIntent")==0 ) {
        try {

            $selectedCardPMI=$_POST['selected-card-PMI'];
            $using="";
            $json = $stripe->customers->allPaymentMethods($customerId, ["type" => "card"]);
            if ($json and isset($json->data[0])) {
                //echo "<PRE>660:".print_r($json, true)."</PRE>";
                foreach ($json->data as $paymentMethod) {
                    $id = $paymentMethod->id;
                    $cardbrand = $paymentMethod->card->brand;
                    $expy = $paymentMethod->card->exp_year;
                    $expm = $paymentMethod->card->exp_month;
                    $last4X = $paymentMethod->card->last4;

                    $last4 = "**** **** **** $last4X";
                    $expires = "Expires " . date("M/y", strtotime("$expy-$expm-01"));


                    if ( strcmp($selectedCardPMI,$id)==0 ) {
                        if ( strcmp($customerId, $paymentMethod->customer )!=0 ) {
                            echo "<LI>Invalid customer Id attached to payment method.</LI>";
                            exit;
                        }
                        $using="$cardbrand - $last4 $expires";
                        break;
                    }
                }
            }

            if ( strcmp($using, "")==0 ) {
                echo "<LI>Opps. Something went wrong. Please retry or contact support</LI>";
                return;
            }

            $ccy="GBP";
            $amt=100 + random_int(0,100);
            $amtX=number_format($amt/100,2);
            $product="Product ABC";

            echo "<p id='notice'>charging $using card : $ccy$amtX for \"$product\"</p>";
            // See https://stripe.com/docs/payments/accept-a-payment-synchronously?platform=web#create-payment-intent
            $intent = $stripe->paymentIntents->create([
                              'payment_method'      => "$selectedCardPMI",
                              'amount'              => $amt,
                              'currency'            => "$ccy",
                              'description'         => "$product",
                              'customer'            => "$customerId",
                              'confirm'             => true,
                              'confirmation_method' => 'manual',
                              //'setup_future_usage'  => 'off_session'
                          ]);

            $note="";
            if ( strcmp($intent->status, "succeeded")==0 ) $class="colGreenBg";
            else {
                $class="colRedBg";
                $note="<p style='margin-left:20px'>
                Note, this process is assumed to be a background process in this case, and therefore cannot interactively request confirmation from the user.
                </p>";
            }
            echo "<LI>Background payment : <span class='$class'>".$intent->status."</span></LI>";
            echo $note;

            if ( strcmp($intent->status, "requires_action")==0 ) {
                if (isset($intent->next_action)) {
                    $nextAction = $intent->next_action;
                    $url=$nextAction->use_stripe_sdk->stripe_js;
                    $url0=substr($url, 0, 30)."...";
                    echo "<p>You are required to proceed to <a href='$url' target='_blank'>$url0</a></p>";
                }
            }
            //echo "<PRE>725:".print_r($intent, true)."</PRE>";
        }
        catch (\Stripe\Exception\CardException $e) {
            // Error code will be authentication_required if authentication is needed
            echo "<LI>PaymentIntents create Exception.542: Error code is: <span class='colRedBg'>" . $e->getError()->code . "</span></LI>";
            //$payment_intent_id = $e->getError()->payment_intent->id;
        }
    }


    echo "<LI>payment request finished";
}


Thex full code can be found here: open_in_new

stripe-elements-checkout-offsession-code.php :: Full code
<?php


include_once('class.const.php');

// class.const.php will contain php config settings:
//
//   class config {
//      Const stripePublicKey='pk_test.....';
//      Const stripeSecretKey='sk_test.....';
//      Const domain='.methodfish.com';
//      Const slash='/'; (or \\ in windows)
//      Const datFolder='C:\\TEMP\\'; // folder location where to save customer ids
//   }



class PaymentForm {

    var $posterUID;
    // --------------------------------------------------------------------------------------

    public function getHTMLAddHeadCSSstyles() {
        return /**@lang CSS */ "
        body {
            font-family:Roboto;
            font-size:13pt;
            line-height:1.6em;   
        }
        #payment_form {
            max-width:600px;
            margin:20px;
            margin-left:auto;
            margin-right:auto;
        }
        pre {
            background-color: #cccccc;
            white-space:pre-wrap;
        }
        iframe, .iframe {
            border:none!important;
            box-shadow: none;
        }
        .frame {
            border:solid 1px #cbcaca;
            background-color:#f8f7f3;
            padding:10px;
        }
        .frame > div {
            max-width:1200px;
            margin:0 20px;
            margin-left:auto;
            margin-right:auto;
        }
        .card {
            max-width:50px;
            border:solid 1px #cfc8c8;
            border-radius:9px;
            vertical-align:middle;
        }
        form > input, form > textarea {
            width:calc(100% - 26px);
            padding:8px;
            margin:4px;
            border:solid 1px grey;
            border-radius:9px;
        }
        input[type='radio'] {
            width:20px!important;
            border-radius: 50%;
            width: 23px;
            height: 23px;
        
            border: 2px solid #999;
            transition: 0.2s all linear;
            margin-top:6px;
            margin-right: 5px;
        
            position: relative;
            top: 3px;
        }
        form > button {
            margin-bottom:10px;
        }
        button, .button {
            font-size:10pt;
            padding:10px;
            margin-top:10px;
            margin-right:10px;
            background-color:#11b4ff;
            color:white;
            border:none;
            cursor:pointer;
            transition:all 0.25s ease-in-out;
            text-decoration: none;
            min-width:100px;
            
            -webkit-touch-callout: none; /* iOS Safari */
            -webkit-user-select: none; /* Safari */
            -khtml-user-select: none; /* Konqueror HTML */
            -moz-user-select: none; /* Old versions of Firefox */
            -ms-user-select: none; /* Internet Explorer/Edge */
            user-select: none; /* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */
  
        }
        button:hover, .button:hover {
            background-color:#087baf;
        }
        button:disabled, .button:disabled {
            background-color:grey;
        }
        .info {
            display:inline-block;
            background-color:#e4f2fb;
            padding:10px;
            border-radius:10px;
            width:calc(100% - 20px);
        }
        .colPaleGreyBg {
            background-color:#d9d8d8!important;
        }
        .small {
            font-size:70%;
        }
        #error-message {
            background-color:red;
            color:white;
            margin-top:10px;
            border-radius:35px;
        }
        .warning { border:solid 1px red; }
        .colRedBg {
            background-color:red;
            color:white;
            padding:10px;
            margin-right:10px;
            margin-bottom:20px;
        }
        .colGreenBg {
            background-color:#19b619;
            color:white;
            padding:10px;
            margin-bottom:20px;
        }
        
        .hourglass {
            position:fixed;
            top:0;
            left:0;
            width:100vw;
            height:100vh;
            background-color:grey;
            opacity:0.5;
        }
        .centerBody {
            margin: 0;
            position: absolute;
            left: 50%;
            top: 20%;
            height: 20px;
            width: 200px;
            margin-right: -50% !important;
            transform: translate(-50%, -50%);
        }
        .spinner {
            font-size:40px!important;
            transform:rotate(-10deg);
        }
        .spinning {
            width:0;
            display: inline-block;
            animation: rotation 0.5s infinite linear;
            text-align:right;
        }
        
        @keyframes rotation {
            0% {
                transform: rotate(0deg);
                transform-origin: 20px 20px;
            }
            100% {
                transform: rotate(-360deg);
                transform-origin: 27px 23px;
            }
        }

        form > table {
            width:100%;
        }
        form > table td {
            white-space:nowrap;
        }
        form label, form input[type='radio'] {
            cursor:pointer;
        }
        ";
    }

    // --------------------------------------------------------------------------------------

    public function getHTMLAddHeadJavascript() {
        return /**@lang Javascript */ "
            //----------------------------
            function doDisableButtons() {
                var elems=document.getElementsByTagName('button');
                for(var e=0; e<elems.length; e++) elems[e].disabled=true;
                
                var elems=document.getElementsByTagName('input');
                for(var e=0; e<elems.length; e++) elems[e].disabled=true;
                
                var div=document.createElement('div');
                div.className='hourglass';
                document.body.appendChild(div);
                
                var div=document.createElement('div');
                div.className='centerBody';
                div.innerHTML='<div class=\"spinning\"><div class=\"material-icons spinner\">cached</div></div>';
                document.body.appendChild(div);
            }
            //----------------------------
            function doEnablePayment() {
                document.getElementById('payment').disabled=false;
            }
         ";
    }

    //----------------------------------------------------------------------------------

    
    public function doHTMLPrehook() {

        if ( !isset($_COOKIE['posterUID']) ) {
            $this->posterUID=date('Ymd').substr(bin2hex(random_bytes(16)),0,16);
            $arr_cookie_options = array (
                'expires' => time() + 60*60*24*1000,
                'path' => '/',
                'domain' => config::domain, // leading dot for compatibility or use subdomain
                'secure' => true,     // or false
                'httponly' => true,    // or false
                'samesite' => 'Lax' // None || Lax  || Strict
            );
            setcookie('posterUID', $this->posterUID, $arr_cookie_options);
        }
        else $this->posterUID=$_COOKIE['posterUID'];

    }
    

    //----------------------------------------------------------------------------------

    public function getCardHelperNote(string $last4) {
        $result="";
        switch ($last4) {
            case "3178":$result= "(Authentication Test cards = Insufficient funds)";break;
            case "0446":$result= "(Authentication Test cards = Already set up)";break;
            case "3184":$result= "(Authentication Test cards = Always authenticate)";break;
            case "3155":$result= "(Authentication Test cards = Authenticate unless set up)";break;
            case "0002":$result= "(Declined Test cards: Generic decline)";break;
            case "9995":$result= "(Declined Test cards: Insufficient funds decline)";break;
            case "9987":$result= "(Declined Test cards: Lost card decline)";break;
            case "9979":$result= "(Declined Test cards: Stolen card decline)";break;
            case "0069":$result= "(Declined Test cards: Expired card decline)";break;
            case "0127":$result= "(Declined Test cards: Incorrect CVC decline)";break;
            case "0119":$result= "(Declined Test cards: Processing error decline)";break;
            case "4241":$result= "(Declined Test cards: Incorrect number decline)";break;
            case "0019":$result= "(Fraud prevention - always blocked)";break;
            case "4954":$result= "(Fraud prevention - Highest risk)";break;
            case "9235":$result= "(Fraud prevention - Elevated risk)";break;
            case "0101":$result= "(Fraud prevention - CVC check fails)";break;
            case "0036":$result= "(Fraud prevention - Postal code check fails)";break;
            case "0028":$result= "(Fraud prevention - Line1 check fails)";break;
            case "0010":$result= "(Fraud prevention - Address checks fail)";break;
            case "0044":$result= "(Fraud prevention - Address unavailable)";break;
            case "0259":$result= "(Disputes - Fraudulent)";break;
            case "2685":$result= "(Disputes - Not received)";break;
            case "1976":$result= "(Disputes - Enquiry)";break;
            case "5423":$result= "(Disputes - Warning)";break;
            case "3155":$result= "(Authenticate unless set up)";break;
            case "3184":$result= "(Alway authenticate)";break;
            case "3220":$result= "(Alway authenticate)";break;
            case "0446":$result= "(Authentication Already set up)";break;
            case "3178":$result= "(Authentication - Insufficient funds)";break;
        }
        if ( strcmp($result, "")!=0 ) return "<span class='small'>$result</span>";
        return "";
    }

    //----------------------------------------------------------------------------------
    public function getAlphaNumericTx($tx, $extraChars="") {
        $x="";
        for($p=0; $p<strlen($extraChars); $p++) {
            $x.="\\".substr($extraChars,$p,1);
        }
        return preg_replace("/[^a-zA-Z0-9$x]+/", "", $tx);
    }

    //----------------------------------------------------------------------------------
    public function getCustomerId(string $customerNm) {
        $fn=config::datFolder.config::slash."customer_$customerNm.dat";
        if ( file_exists($fn) ) {
            return file_get_contents($fn);
        }
        else return "";
    }

    //----------------------------------------------------------------------------------
    public function doStoreCustomer(string $customerNm, string $customerId) {
        file_put_contents(config::datFolder.config::slash."customer_$customerNm.dat", $customerId);
    }

    //----------------------------------------------------------------------------------
    
    function _showContent() {

        // This page will show a list of cards attached to the customer
        // and will offer to make a payment from one of them

        require 'autoload.php'; // (prerequisite: composer require stripe/stripe-php)

        $this->doHTMLPrehook();


        // Prevent page from caching
        header("Expires: Tue, 03 Jul 2001 06:00:00 GMT");
        header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
        header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
        header("Cache-Control: post-check=0, pre-check=0", false);
        header("Pragma: no-cache");
        header("Connection: close");


        echo /**@lang HTML */ "<!DOCTYPE html><html lang='en'>";
        if ( 1 ) {

            echo /**@lang HTML */ "
            <head>
                <meta name='viewport' content='width=device-width, initial-scale=0.9, maximum-scale=1.0, user-scalable=no'/>
                <meta name='description' content='STRIPE elements checkout sample PHP/Javascript code'>
                <meta name='Keywords' content='stripe,php,checkout,example,sample,javascript'>
                <meta http-equiv='content-type' content='text/html; charset=utf-8'>
                <link rel=\"icon\" type=\"image/png\" href=\"images/methodfish.ico\">
                <title>STRIPE elements checkout sample PHP/Javascript code</title>
                <style>
                    " . $this->getHTMLAddHeadCSSstyles() . "
                </style>
                <script>
                    " . $this->getHTMLAddHeadJavascript() . "
                </script>
                <link rel='stylesheet preload prefetch' as='style' href='https://fonts.googleapis.com/icon?family=Courier+Prime|Roboto|Material+Icons|Material+Icons+Outlined'>
            </head>";


            echo "<body>";
            if (1) {

                echo "<div class='frame'>";
                if (1) {

                    echo "<div>";
                    if (1) {

                        echo "<div style='height:30px'>
                                <i class='material-icons' style='cursor:pointer; float:right' onclick='window.parent.location.href=location.href' title='pop out'>open_in_new</i>
                                </div>";
                        echo "<h1 style='font-size:16pt'>STRIPE Test on ".date("D M d'y H:i:s")."</h1>";

                        $url="https://methodfish.com/stripe-elements-checkout-offsession.php";
                        $link="<a href='$url'>methodfish.com</a>";
                        echo "<p>This is a code test for STRIPE elements checkout card collection and background/offsession payment later. 
                                 The payment here is simulated through an onscreen button but the coding here assumes that 
                                 this make-payment request would happen offline and therefore with no user interaction.</p>";
                        echo "<p>If authentication is required for the payment to go through, then this is yet to be worked out.</p>";
                        echo "<p id='src-code'>See $link for more information</p>";
                        echo "<p style='background-color:#cbdeeb; padding:10px'><i class='material-icons' style='color:red; vertical-align: middle'>warning</i> WARNING: This code is not 100% working code and is still being worked out how to implement fully - handle with care</p>
";


                        $customerNm = "customer ".$this->getAlphaNumericTx($this->posterUID);
                        echo "<BR>Customer: $customerNm</BR>";
                        $customerId = "";
                        $email = "";
                        $script = $_SERVER['SCRIPT_NAME'];

                        $stripe_secretkey = config::stripeSecretKey;
                        $stripe_publickey = config::stripePublicKey;

                        $stripe = new \Stripe\StripeClient($stripe_secretkey);


                        if ( 1 ) {
                            // because stripe search is not 100% reliable, we use own solution to check if customer exists
                            // (see https://stripe.com/docs/search#data-freshness)
                            $customerId = $this->getCustomerId($customerNm);
                        }
                        else {
                            // don't bother using customers->search() as it's unreliable
                            echo "<LI>Searching for $customerNm...</LI>";
                            $json = $stripe->customers->search(['query' => 'name:\'' . $customerNm . '\'']);
                            foreach ($json->data as $key) {
                                echo "<LI>Found id:[" . $key->id . "]";
                                if (strcmp($key->name, $customerNm) == 0) {
                                    $customerId = $key->id;
                                    $email = $key->email;
                                    break;
                                }
                            }
                            echo "<LI>Result to search is <PRE style='margin-left:20px'>$json</PRE>";
                        }
                        echo "<hr>";


                        if (strcmp($customerId, "") == 0) {

                            $email = str_replace(" ", "", str_replace(" ", "", "abc@$customerNm.com"));
                            
                            $json = $stripe->customers->create(
                                [
                                    'email' => $email,
                                    'name'  => $customerNm,
                                ]
                            );
                            //echo "<LI>417:<PRE>" . print_r($json, true) . "</PRE>";
                            $customerId = $json->id;
                            
                            echo "<UL>Created New STRIPE customer id: [$customerId]</UL>";
                            $this->doStoreCustomer($customerNm, $customerId);
                        }

                        
                        $json = $stripe->customers->allPaymentMethods($customerId, ["type" => "card"]);
                        $cardsFound = false;
                        $disabled = "disabled";
                        $selectedPMI = "";


                        if ( strcmp($_SERVER['REQUEST_METHOD'], "GET")==0 ) {

                            echo "<form method='post' action='$script' name='paymentform'>";
                            if (1) {
                                echo "<input type='hidden' name='action' value='payment'>";
                                echo "<input type='hidden' name='step' value='paymentIntent'>";
                                echo "<input type='hidden' name='customerId' value='$customerId'>";

                                if ($json and isset($json->data[0])) {
                                    echo "<table>";
                                    foreach ($json->data as $paymentMethod) {
                                        //echo "442:<PRE>$paymentMethod</PRE>";
                                        $cardsFound = true;
                                        $id = $paymentMethod->id;
                                        $cardbrand = $paymentMethod->card->brand;
                                        $expy = $paymentMethod->card->exp_year;
                                        $expm = $paymentMethod->card->exp_month;
                                        $last4X = $paymentMethod->card->last4;

                                        $last4 = "**** **** **** $last4X";
                                        $expires = "Expires " . date("M/y", strtotime("$expy-$expm-01"));

                                        if (isset($_POST['selected-card-PMI']) and strcmp($_POST['selected-card-PMI'], $id) == 0) {
                                            $selected = "checked";
                                            $disabled = "";
                                            $selectedPMI = $id;
                                        }
                                        else $selected = "";


                                        $cardlogo = "<img src='https://methodfish.com/images/card_$cardbrand.png' class='card'>";
                                        $note = $this->getCardHelperNote($last4X);
                                        echo "<tr><td>";
                                        echo "<input type='radio' $selected name='selected-card-PMI' id='$id' value='$id' onClick='doEnablePayment()'>";
                                        echo "</td>";
                                        echo "<td>";
                                        echo "<label for='$id'>$cardlogo - $last4 $expires $note</label><br>";
                                        echo "</td></tr>";
                                    }
                                    echo "</table>";
                                }

                                if (!$cardsFound) echo "<p>no cards found</p>";

                                echo "<HR><button id='new' onClick='doDisableButtons(); window.location.href=\"$script?action=add\"'>Add New Card</button>";

                                if ($cardsFound) echo "<button id='payment' $disabled type='submit' onClick='paymentform.submit(); doDisableButtons(); '>Make Payment...</button>"; // This button will submit the form
                            }
                            echo "</form>";
                            

                        }
                        else {

                            if ($json and isset($json->data[0])) {
                                foreach ($json->data as $paymentMethod) {
                                    //echo "487:<PRE>$paymentMethod</PRE>";
                                    $cardsFound = true;
                                    $id = $paymentMethod->id;

                                    if (isset($_POST['selected-card-PMI']) and strcmp($_POST['selected-card-PMI'], $id) == 0) {
                                        $selectedPMI = $id;
                                    }
                                }
                            }

                        }


                        
                        if (isset($_GET['action'])) {
                            switch ($_GET['action']) {
                                case "add":
                                    $this->doAddCard($stripe, $customerId, $stripe_publickey);
                                    break;
                            }
                        }

                        if (isset($_POST['action'])) {
                            switch ($_POST['action']) {
                                case "payment":
                                    $this->doMakePayment($stripe, $customerId, $selectedPMI, $stripe_publickey);
                                    break;
                            }
                        }
                        

                        echo "<br><br><button onClick='window.location.href=window.location.origin+window.location.pathname;'>Restart</button>";
                    }
                    echo "</div>";
                }
                echo "</div>";

                // Hide the src-code link if the code is running inside an iframe
                echo "<script>";
                echo "if ( window!==window.parent ) document.getElementById('src-code').innerHTML='';";
                echo "</script>";

            }
            echo "</body>";
        }
        echo "</html>";

    }
    

    // -----------------------------------------------------------------------------------------

    
    public function doAddCard($stripe, string $customerId, string $stripe_publickey) {

        try {

            $json = $stripe->setupIntents->create(['customer' => $customerId, 'payment_method_types' => ['card']]);

            $intentId = $json->id;
            $clientSecret = $json->client_secret;

            echo /**@lang HTML */ "
            
                <p class='info colPaleGreyBg'>
                HINT: Use the Test card options listed below (e.g. 4242424242424242 - expires 12/33, CVC 123.
                See <a href='https://stripe.com/docs/testing#cards' target='_blank'>STRIPE.com</a> for more test cards.
                </p>
                <p style='color:red'><i class='material-icons' style='vertical-align: middle; color: red'>warning</i> Never use real cards on this test page.</p>

                <form id='payment_form' data-secret='$clientSecret'>
                    <div id='payment-element'>
                      <!-- Elements will create form elements here-->
                    </div>
                    
                    <div id='error-message'></div>
                    <button id='submit'>Save Card</button>
                    <button id='cancel' type='button'>Cancel</button>
                </form>
                
                ";


            $url=$_SERVER['SCRIPT_NAME'];

            // See https://stripe.com/docs/payments/save-and-reuse?platform=web#web-collect-payment-details
            echo /**@lang Javascript */ "
                
                <script src='https://js.stripe.com/v3/'></script>
                <script>

                const stripe = Stripe('$stripe_publickey');
                const options = {
                      clientSecret: '$clientSecret'
                    };
                  
                    
                // Set up Stripe.js and Elements to use in checkout form, passing the client secret obtained in step 2
                const elements = stripe.elements(options);
                
                // Create and mount the Payment Element
                const paymentElement = elements.create('payment');
                paymentElement.mount('#payment-element');
                paymentElement.on('ready', (e) => paymentElement.focus());
                
                const form = document.getElementById('payment_form');

                document.getElementById('cancel').addEventListener('click', async (event) => {
                    doDisableButtons();
                    event.preventDefault();
                    window.location.href=window.location.origin+window.location.pathname;
                });
                
                
                form.addEventListener('submit', async (event) => {  // <---------------------
                    event.preventDefault();
                    
                    var target=document.getElementById('submit');
                    const messageContainer = document.querySelector('#error-message');
                    messageContainer.style.padding = '';
                    target.disabled=true;
                    target.innerHTML='Checking...';
                    
                    document.getElementById('error-message').innerHTML='';
                    
                    const {error} = await stripe.confirmSetup({ // <-------------------------
                            elements,
                            confirmParams: {
                                return_url: window.location.origin+window.location.pathname+'?card=saved' // It looks like this would ignore an iframe if the processing is in an iframe
                            }
                        });
                    
                    
                    if (error) {
                        // This point will only be reached if there is an immediate error when
                        // confirming the payment. Show error to your customer (for example, payment
                        // details incomplete)
                        messageContainer.textContent = error.message;
                        messageContainer.style.padding = '10px 20px 10px 20px';
                        target.disabled=false;
                        target.innerHTML='Save';
                    } 
                    else {
                        // Your customer will be redirected to your `return_url`. For some payment
                        // methods like iDEAL, your customer will be redirected to an intermediate
                        // site first to authorize the payment, then redirected to the `return_url`.
                        alert('This never happens');
                    }
                })
                ;
                </script>
            ";
        }
        catch (Exception $e) {
            echo "<LI>Payment method creation failed " . $e->getMessage();
        }

    }
    

    // -----------------------------------------------------------------------------------------

    
    public function doMakePayment($stripe, string $customerId, string $paymentMethodId, string $stripe_publickey) {
        // NOTE: We have already verified $paymentMethodId belongs to $customerId

        if ( strcmp($_POST['step'], "paymentIntent")==0 ) {
            try {

                $selectedCardPMI=$_POST['selected-card-PMI'];
                $using="";
                $json = $stripe->customers->allPaymentMethods($customerId, ["type" => "card"]);
                if ($json and isset($json->data[0])) {
                    //echo "<PRE>660:".print_r($json, true)."</PRE>";
                    foreach ($json->data as $paymentMethod) {
                        $id = $paymentMethod->id;
                        $cardbrand = $paymentMethod->card->brand;
                        $expy = $paymentMethod->card->exp_year;
                        $expm = $paymentMethod->card->exp_month;
                        $last4X = $paymentMethod->card->last4;

                        $last4 = "**** **** **** $last4X";
                        $expires = "Expires " . date("M/y", strtotime("$expy-$expm-01"));


                        if ( strcmp($selectedCardPMI,$id)==0 ) {
                            if ( strcmp($customerId, $paymentMethod->customer )!=0 ) {
                                echo "<LI>Invalid customer Id attached to payment method.</LI>";
                                exit;
                            }
                            $using="$cardbrand - $last4 $expires";
                            break;
                        }
                    }
                }

                if ( strcmp($using, "")==0 ) {
                    echo "<LI>Opps. Something went wrong. Please retry or contact support</LI>";
                    return;
                }

                $ccy="GBP";
                $amt=100 + random_int(0,100);
                $amtX=number_format($amt/100,2);
                $product="Product ABC";

                echo "<p id='notice'>charging $using card : $ccy$amtX for \"$product\"</p>";
                // See https://stripe.com/docs/payments/accept-a-payment-synchronously?platform=web#create-payment-intent
                $intent = $stripe->paymentIntents->create([
                                  'payment_method'      => "$selectedCardPMI",
                                  'amount'              => $amt,
                                  'currency'            => "$ccy",
                                  'description'         => "$product",
                                  'customer'            => "$customerId",
                                  'confirm'             => true,
                                  'confirmation_method' => 'manual',
                                  //'setup_future_usage'  => 'off_session'
                              ]);

                $note="";
                if ( strcmp($intent->status, "succeeded")==0 ) $class="colGreenBg";
                else {
                    $class="colRedBg";
                    $note="<p style='margin-left:20px'>
                    Note, this process is assumed to be a background process in this case, and therefore cannot interactively request confirmation from the user.
                    </p>";
                }
                echo "<LI>Background payment : <span class='$class'>".$intent->status."</span></LI>";
                echo $note;

                if ( strcmp($intent->status, "requires_action")==0 ) {
                    if (isset($intent->next_action)) {
                        $nextAction = $intent->next_action;
                        $url=$nextAction->use_stripe_sdk->stripe_js;
                        $url0=substr($url, 0, 30)."...";
                        echo "<p>You are required to proceed to <a href='$url' target='_blank'>$url0</a></p>";
                    }
                }
                //echo "<PRE>725:".print_r($intent, true)."</PRE>";
            }
            catch (\Stripe\Exception\CardException $e) {
                // Error code will be authentication_required if authentication is needed
                echo "<LI>PaymentIntents create Exception.542: Error code is: <span class='colRedBg'>" . $e->getError()->code . "</span></LI>";
                //$payment_intent_id = $e->getError()->payment_intent->id;
            }
        }


        echo "<LI>payment request finished";
    }
    

}
try {
    $form = new PaymentForm();
    $form->_showContent(false);
}
catch (Exception $e) {
    echo "<BR>Exception Error.745: ".$e->getMessage();
}

Comments


New Comment

NOTE: (Put code blocks in [[[ and ]]] markup to be formatted.)

(Posted comments will be checked by the administrator before being published)



Sun Dec 04, 22 10:44:08