Using STRIPE Elements PHP API to collect card details and request on-session payment

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

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-onsession-code.php :: Create customer
$json = $stripe->customers->create(
    [
        'email' => $email,
        'name'  => $customerNm,
    ]
);
//echo "<LI><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-onsession-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 "<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='doEnablePaymentJS()'>";
                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='doDisableButtonsJS(); window.location.href=\"$script?action=add\"'>Add New Card</button>";

        if ($cardsFound) echo "<button id='payment' $disabled type='button' onClick='doMakePaymentJS()'>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 doMakePaymentJS() function, that performs front and back-end processing.

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

Adding Cards

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 intent on STRIPE - redirecting the user to the return_url once this is done.

stripe-elements-checkout-onsession-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>

            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) => {
                doDisableButtonsJS();
                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({ // <------------------------- to confirm a SetupIntent using data collected by the Payment Element
                        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();
    }

}

Requesting Payment

As we need to use STRIPE to do any authentications, making a payment is handled through client side using the doMakePaymentJS() javascript function that passes the payment method id and customer through a JSON request into the doHTMLPrehook() (pre HTML output) function which calls $stripe->paymentIntent->create() for the payment_method and customer, along with purchase details.

In addition to the STRIPE information being passed around, I am including my own myTransactionUID that represents a database record UID that might want to track outstanding purchases (care is needed not to expose 'secret' internal keys - so you may want to encrypt this too). By passing this around, I can update my tables once STRIPE tells me things are successful.

The doGenerateResponse() turns the result back into JSON and returns the response back to doMakePaymentJS() on the client, which then calls on doHandleServerResponseJS() to potentially request approval if necessary (via stripe.handleCardAction()).

Finally, in the javascript, if authentication occurred, then we will want to ask doHandleServerResponseJS to call doHandleStripeResult [if the response requires action] so that the payment-intent can be confirmed (see 'ACCESS POINT 2'). Also note that it is possible that the $intent->confirm() call may throw an [ApiErrorException] exception for things like insufficient-funds (see checkpoint.375).


stripe-elements-checkout-onsession-code.php :: doMakePayment JS function
function doMakePaymentJS() {
    event.preventDefault();
    
    doShowHourglass();
        
    console.log('229: doMakePaymentJS');
    fetch('stripe-elements-checkout-onsession-code.php', {
        method:'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            payment_method_id: new FormData(document.forms.paymentform).get('selected-card-PMI'),
            customer_id: new FormData(document.forms.paymentform).get('customerId'),
        })
    }).then(function(result) {
        // Handle server response
        result.json().then(function(json) {
            doHandleServerResponseJS(json);
        })
    });
}

stripe-elements-checkout-onsession-code.php :: doHandleServerResponse JS function
function doHandleServerResponseJS(response) {
    console.log('doHandleServerResponseJS.248 '+response.myTransactionUID);
    if (response.error) {
        // Show error from server on payment form
        if ( response.error.message) alert('251:'+response.error.message);
        else alert('252:'+response.error);
        doRemoveHourglass();
    } else if (response.requires_action) {
        console.log('254:requires_action');
        // Use Stripe.js to handle required card action
        stripe.handleCardAction(
            response.payment_intent_client_secret
        ).then(function(result) {
            doHandleStripeResult(result, response.myTransactionUID);
        });
    } else {
        // Show success message : if authentication occurred then response.msg will not be passed through
        // from the php, so we just use the basic response.paymentIntent... information
        console.log('264: Success');
        if ( response.myTransactionUID ) alert('265: Success for transaction '+response.myTransactionUID);
        else alert('266: Success');
        doRemoveHourglass();
    }
}
//----------------------------
function doHandleStripeResult(result, myTransactionUID) {
    console.log('doHandleServerResponseJS.271');
    console.log(result);
    if (result.error) {
        // Show error in payment form
        alert('275:'+result.error.message);
        doRemoveHourglass();
    } else {
        // The card action has been handled
        // The PaymentIntent can be confirmed again on the server
        fetch('stripe-elements-checkout-onsession-code.php', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ 
                    payment_intent_id: result.paymentIntent.id,
                    myTransactionUID: myTransactionUID  
                })
        }).then(function(confirmResult) {
            console.log('doHandleServerResponseJS.287');
            console.log(confirmResult);
            return confirmResult.json();
        }).then(doHandleServerResponseJS);
    }
}

stripe-elements-checkout-onsession-code.php :: doHTMLPrehook function - json responder
if ( strcmp($_SERVER['REQUEST_METHOD'], "POST")==0
and strcmp($_SERVER['CONTENT_TYPE'], "application/json")==0) {
    //echo "<LI>POST detected: ".print_r($_POST, false)."</LI>";

    \Stripe\Stripe::setApiKey(config::stripeSecretKey);

    header('Content-Type: application/json');

    # retrieve json from POST body
    $json_str = file_get_contents('php://input');
    $json_obj = json_decode($json_str);

    $myTransactionUID="";
    $intent = null;
    try {

        if (isset($json_obj->payment_method_id)) { // request payment <----- ACCESS POINT 1
            $myTransactionUID = random_int(0,10000);
            $this->doLogTrace("<span style='background-color:#c8d0c8'>START</span>: checkpoint.331 " .substr($json_obj->payment_method_id, 0, 10).".... uid=$myTransactionUID");
            # Create the PaymentIntent
            $ccy="GBP";
            $amt=100 + random_int(0,100);
            $amtX = number_format($amt/100,2);
            $product="Product ABC";
            $intent = \Stripe\PaymentIntent::create([
                            'payment_method'      => $json_obj->payment_method_id,
                            'customer'            => $json_obj->customer_id,
                            'amount'              => $amt,
                            'currency'            => $ccy,
                            'confirmation_method' => 'manual',
                            'confirm'             => true,
                            'description'         => "$product",
                        ]);
        }

        if (isset($json_obj->payment_intent_id)) { // Confirm Authentication  <----- ACCESS POINT 2
            $this->doLogTrace("checkpoint.361 ".substr($json_obj->payment_intent_id,0,10)."....<UL><PRE>".print_r($json_obj, true)."</PRE></UL>");
            $myTransactionUID = $json_obj->myTransactionUID;
            //echo "<LI>326:".$json_obj->payment_intent_id;
            $intent = \Stripe\PaymentIntent::retrieve($json_obj->payment_intent_id);
            $intent->confirm();
        }
        $this->doGenerateResponse($myTransactionUID, $intent);

    } catch (\Stripe\Exception\ApiErrorException $e) { // exception from the STRIPE retrieve/confirm calls
        # Display error on client
        $this->doLogTrace("checkpoint.375 [$myTransactionUID] <span style='background-color:#be370a; color:white'>" .$e->getMessage()."</span>");
        echo json_encode([
                             'error' => $e->getMessage()
                         ]);
    }
    catch (Exception $e) {
        $this->doLogTrace("checkpoint.368 ".$e->getMessage());
        echo json_encode([
                             'error' => "369 Exception: <span style='background-color:#ef4913; color:white'>" .$e->getMessage()."</span>"
                         ]);
    }

    exit;
}

stripe-elements-checkout-onsession-code.php :: doGenerateResponse function
public function doGenerateResponse(int $myTransactionUID, $intent_json) {
    # Note that if your API version is before 2019-02-11, 'requires_action'
    # appears as 'requires_source_action'.
    //echo "<PRE>$intent</PRE>";
    if ($intent_json->status == 'requires_action' and $intent_json->next_action->type == 'use_stripe_sdk') {
        # Tell the client to handle the action
        $this->doLogTrace("checkpoint.393 ".$intent_json->status);
        echo json_encode([
                             'myTransactionUID' => $myTransactionUID
                             , 'requires_action' => true
                             , 'payment_intent_client_secret' => $intent_json->client_secret
                         ]);
    } else if ($intent_json->status == 'succeeded') {
        # The payment didn’t need any additional actions and completed!
        # Handle post-payment fulfillment
        $this->doLogTrace("checkpoint.402 ".$intent_json->status." for transaction ".$myTransactionUID);
        echo json_encode([
                             'myTransactionUID' => $myTransactionUID
                             , 'success' => true
                         ]);
        $this->doSaveTransaction($myTransactionUID);
    } else {
        # Invalid status
        $this->doLogTrace("checkpoint.411 <UL><PRE>".print_r($intent_json, true)."</PRE></UL>");
        http_response_code(500);
        echo json_encode([
                            'error' => '413:Invalid PaymentIntent status'
                         ]);
    }
}

A final note, the tracing that occurs via the php doLogTrace calls can be tracked using the Logfile link, allowing you to see where processing moves to, along with the browser console to see the console.log calls that track the client side.



The full code can be found here: open_in_new

stripe-elements-checkout-onsession-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:10px;
        }
        .colGreenBg {
            background-color:#19b619;
            color:white;
            padding:10px;
            margin-bottom:10px;
        }
        
        .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 doRemoveHourglass() {
                var hg=document.getElementById('hourglass');
                if ( hg ) hg.parentNode.removeChild(hg);
            }
            //----------------------------
            function doShowHourglass() {
                doRemoveHourglass();
                var bg=document.createElement('div');
                bg.id='hourglass';
                bg.className='hourglass';
                document.body.appendChild(bg);
                
                var div=document.createElement('div');
                div.className='centerBody';
                div.innerHTML='<div class=\"spinning\"><div class=\"material-icons spinner\">cached</div></div>';
                bg.appendChild(div);
            }
            //----------------------------
            function doDisableButtonsJS() {
                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;
                
                doShowHourglass();
            }
            //----------------------------
            function doEnablePaymentJS() {
                document.getElementById('payment').disabled=false;
            }
            //----------------------------
            
            function doMakePaymentJS() {
                event.preventDefault();
                
                doShowHourglass();
                    
                console.log('229: doMakePaymentJS');
                fetch('stripe-elements-checkout-onsession-code.php', {
                    method:'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        payment_method_id: new FormData(document.forms.paymentform).get('selected-card-PMI'),
                        customer_id: new FormData(document.forms.paymentform).get('customerId'),
                    })
                }).then(function(result) {
                    // Handle server response
                    result.json().then(function(json) {
                        doHandleServerResponseJS(json);
                    })
                });
            }
            
            //----------------------------
            
            function doHandleServerResponseJS(response) {
                console.log('doHandleServerResponseJS.248 '+response.myTransactionUID);
                if (response.error) {
                    // Show error from server on payment form
                    if ( response.error.message) alert('251:'+response.error.message);
                    else alert('252:'+response.error);
                    doRemoveHourglass();
                } else if (response.requires_action) {
                    console.log('254:requires_action');
                    // Use Stripe.js to handle required card action
                    stripe.handleCardAction(
                        response.payment_intent_client_secret
                    ).then(function(result) {
                        doHandleStripeResult(result, response.myTransactionUID);
                    });
                } else {
                    // Show success message : if authentication occurred then response.msg will not be passed through
                    // from the php, so we just use the basic response.paymentIntent... information
                    console.log('264: Success');
                    if ( response.myTransactionUID ) alert('265: Success for transaction '+response.myTransactionUID);
                    else alert('266: Success');
                    doRemoveHourglass();
                }
            }
            //----------------------------
            function doHandleStripeResult(result, myTransactionUID) {
                console.log('doHandleServerResponseJS.271');
                console.log(result);
                if (result.error) {
                    // Show error in payment form
                    alert('275:'+result.error.message);
                    doRemoveHourglass();
                } else {
                    // The card action has been handled
                    // The PaymentIntent can be confirmed again on the server
                    fetch('stripe-elements-checkout-onsession-code.php', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify({ 
                                payment_intent_id: result.paymentIntent.id,
                                myTransactionUID: myTransactionUID  
                            })
                    }).then(function(confirmResult) {
                        console.log('doHandleServerResponseJS.287');
                        console.log(confirmResult);
                        return confirmResult.json();
                    }).then(doHandleServerResponseJS);
                }
            }

            
                
            const stripe = Stripe('".config::stripePublicKey."');   
         ";
    }


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

    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'];
        $this->posterUID=$this->getAlphaNumericTx($this->posterUID);
        

        if ( ! isset($_GET['getlog'])) {
            $this->doLogTrace("checkpoint.310 " . $_SERVER['REQUEST_METHOD']);
        }

        
        if ( strcmp($_SERVER['REQUEST_METHOD'], "POST")==0
        and strcmp($_SERVER['CONTENT_TYPE'], "application/json")==0) {
            //echo "<LI>POST detected: ".print_r($_POST, false)."</LI>";

            \Stripe\Stripe::setApiKey(config::stripeSecretKey);

            header('Content-Type: application/json');

            # retrieve json from POST body
            $json_str = file_get_contents('php://input');
            $json_obj = json_decode($json_str);

            $myTransactionUID="";
            $intent = null;
            try {

                if (isset($json_obj->payment_method_id)) { // request payment <----- ACCESS POINT 1
                    $myTransactionUID = random_int(0,10000);
                    $this->doLogTrace("<span style='background-color:#c8d0c8'>START</span>: checkpoint.331 " .substr($json_obj->payment_method_id, 0, 10).".... uid=$myTransactionUID");
                    # Create the PaymentIntent
                    $ccy="GBP";
                    $amt=100 + random_int(0,100);
                    $amtX = number_format($amt/100,2);
                    $product="Product ABC";
                    $intent = \Stripe\PaymentIntent::create([
                                    'payment_method'      => $json_obj->payment_method_id,
                                    'customer'            => $json_obj->customer_id,
                                    'amount'              => $amt,
                                    'currency'            => $ccy,
                                    'confirmation_method' => 'manual',
                                    'confirm'             => true,
                                    'description'         => "$product",
                                ]);
                }

                if (isset($json_obj->payment_intent_id)) { // Confirm Authentication  <----- ACCESS POINT 2
                    $this->doLogTrace("checkpoint.361 ".substr($json_obj->payment_intent_id,0,10)."....<UL><PRE>".print_r($json_obj, true)."</PRE></UL>");
                    $myTransactionUID = $json_obj->myTransactionUID;
                    //echo "<LI>326:".$json_obj->payment_intent_id;
                    $intent = \Stripe\PaymentIntent::retrieve($json_obj->payment_intent_id);
                    $intent->confirm();
                }
                $this->doGenerateResponse($myTransactionUID, $intent);

            } catch (\Stripe\Exception\ApiErrorException $e) { // exception from the STRIPE retrieve/confirm calls
                # Display error on client
                $this->doLogTrace("checkpoint.375 [$myTransactionUID] <span style='background-color:#be370a; color:white'>" .$e->getMessage()."</span>");
                echo json_encode([
                                     'error' => $e->getMessage()
                                 ]);
            }
            catch (Exception $e) {
                $this->doLogTrace("checkpoint.368 ".$e->getMessage());
                echo json_encode([
                                     'error' => "369 Exception: <span style='background-color:#ef4913; color:white'>" .$e->getMessage()."</span>"
                                 ]);
            }

            exit;
        }
        
    }

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

    
    public function doGenerateResponse(int $myTransactionUID, $intent_json) {
        # Note that if your API version is before 2019-02-11, 'requires_action'
        # appears as 'requires_source_action'.
        //echo "<PRE>$intent</PRE>";
        if ($intent_json->status == 'requires_action' and $intent_json->next_action->type == 'use_stripe_sdk') {
            # Tell the client to handle the action
            $this->doLogTrace("checkpoint.393 ".$intent_json->status);
            echo json_encode([
                                 'myTransactionUID' => $myTransactionUID
                                 , 'requires_action' => true
                                 , 'payment_intent_client_secret' => $intent_json->client_secret
                             ]);
        } else if ($intent_json->status == 'succeeded') {
            # The payment didn’t need any additional actions and completed!
            # Handle post-payment fulfillment
            $this->doLogTrace("checkpoint.402 ".$intent_json->status." for transaction ".$myTransactionUID);
            echo json_encode([
                                 'myTransactionUID' => $myTransactionUID
                                 , 'success' => true
                             ]);
            $this->doSaveTransaction($myTransactionUID);
        } else {
            # Invalid status
            $this->doLogTrace("checkpoint.411 <UL><PRE>".print_r($intent_json, true)."</PRE></UL>");
            http_response_code(500);
            echo json_encode([
                                'error' => '413:Invalid PaymentIntent status'
                             ]);
        }
    }
    

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

    public function doSaveTransaction(string $myTransactionUID) {
        $this->doLogTrace("checkpoint.429 <span style='background-color:#68d068'>doSaveTransaction($myTransactionUID, 'success')</span>");
    }

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

    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 "0341":$result= "(Declined Test cards: Decline after attaching)";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 "0446":$result= "(Authentication Already set up)";break;
            case "3220":$result= "(Alway authenticate)";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");


        if ( isset($_GET['getlog'])) {
            echo "<html>";
            echo "<head>";
            echo "<script>";
            echo "//-----------------------------------------------------------------------------------------
                    function getURLParameterByName(name, url) {
                        // returns the page URL argument,
                        // e.g.
                        //    getURLParameterByName('Info'); will return 123 for the current URL...
                        //
                        //    ....abc/page.php?Info=123#mp1273643
                        //
                        if (!url) url = window.location.href;
                        name = name.replace(/[\[\]]/g, \"\\$&\");
                        var regex = new RegExp(\"[?&]\" + name + \"(=([^&#]*)|&|#|$)\");
                        var results = regex.exec(url);
                        if (!results) return null;
                        if (!results[2]) return '';
                        return decodeURIComponent(results[2].replace(/\+/g, \" \"));
                    }";
            echo "</script>";
            echo "<body>";
            $tx= str_replace("", "<BR>", file_get_contents(config::logsdir.config::slash."logfile.".$this->posterUID));
            echo substr($tx, strlen($tx)-5000);
            echo "<script>setTimeout(function() {window.scrollTo(0, document.body.scrollHeight);},10);</script>";
            echo "<script>setTimeout(function() {var c=getURLParameterByName('countdown'); c--; if ( c>0 ) window.location.href=window.location.href.substr(0, window.location.href.indexOf('?'))+'?getlog=Y&countdown='+c;}, 1000);</script>";
            exit;
        }

        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 src='https://js.stripe.com/v3/'></script>
                <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-onsession.php";
                        $link="<a href='$url'>methodfish.com</a>";
                        echo "<p>This is a code test for STRIPE elements checkout card collection and on-session payments.</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>";



                        $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>";

                        // echo "<UL>Found [$customerId] on STRIPE; attached email is [$email]</UL>";


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

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


                        
                        $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 "<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='doEnablePaymentJS()'>";
                                        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='doDisableButtonsJS(); window.location.href=\"$script?action=add\"'>Add New Card</button>";

                                if ($cardsFound) echo "<button id='payment' $disabled type='button' onClick='doMakePaymentJS()'>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 "<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;
                            }
                        }
                        

                    }
                    echo "<br><br><button onClick='window.location.href=window.location.origin+window.location.pathname;'>Restart</button> ";
                    echo "<a href='{$_SERVER['SCRIPT_NAME']}?getlog=Y&countdown=100' target='_blank'>Logfile</a>";
                    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>

                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) => {
                    doDisableButtonsJS();
                    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({ // <------------------------- to confirm a SetupIntent using data collected by the Payment Element
                            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 doLogTrace(string $msg) {
        error_log(date("Y-m-d H:i:s")." :: ".$msg."<HR>", 3, config::logsdir.config::slash."logfile.".$this->posterUID);
    }

    //----------------------------------------------------------------------------------
    public function getLogTrace(string $logfile) {
        return file_get_contents(config::logsdir.".$logfile");
    }
}
try {
    $form = new PaymentForm();
    $form->_showContent(false);
}
catch (Exception $e) {
    echo "<BR>Exception Error.650: ".$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 13:13:13