This is a runtime example of the PHP & Javascript code required for collecting a card payment method using
as described by the stripe.com/docs/payments/accept-a-payment-synchronously documentation page.
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.
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
Setup the HTML side of the equation using an empty form with id='card-element', and include the javascript side.
index.html
<div id="stripe">
<form id='payment-form' name='paymentForm'>
<label>
Card details
<!-- placeholder for Elements -->
<div id="card-element"></div>
</label>
<button type="submit">Submit Payment</button>
</form>
</div>
index.html
<script src="https://js.stripe.com/v3/"></script>
The script.js then calls on STRIPE to create a card input form and mount it into the card-element placeholder div. Note, this uses the elements.create('card') command which results in a small form for the card number, CVV and expiry date; this is different from a payment-intent input seen when elements.create('payment') is used.
script.js
var cardElement = elements.create('card', {style: style});
cardElement.mount('#card-element');
cardElement.on('ready', (e) => cardElement.focus());
The script.js will also set up an event to catch the submit button, that will create a payment method (using createPaymentMethod() against a guest customer (this customer will be marked as Guest on the stripe dashboard)
script.js
var form = document.getElementById('payment-form');
form.addEventListener('submit', function(event) {
// We don't want to let default form submission happen here,
// which would refresh the page.
event.preventDefault();
console.log('32: createPaymentMethod');
stripe.createPaymentMethod({
type: 'card',
card: cardElement,
billing_details: {
// Include any additional collected billing details.
name: 'Guest Customer',
},
}).then(stripePaymentMethodHandler);
});
Figure: STRIPE dashboard / customer list showing grey [Guest] markers
When the user clicks on the submit button, this triggers the stripePaymentMethodHandler() function...
script.js :: stripePaymentMethodHandler
function stripePaymentMethodHandler(result) {
if (result.error) {
// Show error in payment form
alert('46:'+result.error.message);
} else {
// Otherwise send paymentMethod.id to your server (see Step 4)
console.log('49:Sending payment method');
fetch('server.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
payment_method_id: result.paymentMethod.id,
})
}).then(function(result) {
// Handle server response (see Step 4)
result.json().then(function(json) {
handleServerResponse(json);
})
});
}
}
script.js :: handleServerResponse
function handleServerResponse(response) {
console.log('handleServerResponse.75');
if (response.error) {
// Show error from server on payment form
alert('71:'+response.error);
} else if (response.requires_action) {
// Use Stripe.js to handle required card action
stripe.handleCardAction(
response.payment_intent_client_secret
).then(handleStripeJsResult);
} else {
// Show success message
alert('75: Success');
}
}
.. which requests a response from the server.php script (seen below with "Entry point A") and then with that result calls handleServerResponse() (seen above).
server.php :: create PaymentIntent
require_once('autoload.php');
include_once('class.const.php');
\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);
$intent = null;
try {
if (isset($json_obj->payment_method_id)) { // <------------- entry point A
# Create the PaymentIntent
$amt=100 + random_int(0,100);
$intent = \Stripe\PaymentIntent::create([
'payment_method' => $json_obj->payment_method_id,
'amount' => $amt,
'currency' => 'GBP',
'confirmation_method' => 'manual',
'confirm' => true,
]);
}
if (isset($json_obj->payment_intent_id)) { // <------------- entry point B
$intent = \Stripe\PaymentIntent::retrieve(
$json_obj->payment_intent_id
);
$intent->confirm();
}
generateResponse($intent);
} catch (\Stripe\Exception\ApiErrorException $e) {
# Display error on client
echo json_encode([
'error' => $e->getMessage()
]);
}
server.php :: generateResponse
function generateResponse($intent) {
# Note that if your API version is before 2019-02-11, 'requires_action'
# appears as 'requires_source_action'.
if ($intent->status == 'requires_action' &&
$intent->next_action->type == 'use_stripe_sdk') {
# Tell the client to handle the action
echo json_encode([
'requires_action' => true,
'payment_intent_client_secret' => $intent->client_secret
]);
} else if ($intent->status == 'succeeded') {
# The payment didn’t need any additional actions and completed!
# Handle post-payment fulfillment
echo json_encode([
"success" => true
]);
} else {
# Invalid status
http_response_code(500);
echo json_encode(['error' => 'Invalid PaymentIntent status']);
}
}
handleServerResponse() calls on STRIPEs stripe.handleCardAction() if action is required, before then calling handleStripeJsResult(); this potentially calls on the bank to request confirmation (or on STRIPE for confirmation while in Test mode). If no action is required, then either an error or a success message is given.
Figure: STRIPE testmode 3d confirmation screen
script.js
function handleStripeJsResult(result) {
console.log('handleStripeJsResult.94');
if (result.error) {
// Show error in payment form
alert('84:'+result.error.message);
} else {
// The card action has been handled
// The PaymentIntent can be confirmed again on the server
fetch('server.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ payment_intent_id: result.paymentIntent.id })
}).then(function(confirmResult) {
return confirmResult.json();
}).then(handleServerResponse);
}
}
Note, it is not clear what confirmResult() does.
If we're going through the "action required" branch, then handleStripeJsResult() will call on the server side of the scripts to request a confirm() for the payment-intent (see entry point B)...
server.php
if (isset($json_obj->payment_intent_id)) { // <------------- entry point B
$intent = \Stripe\PaymentIntent::retrieve(
$json_obj->payment_intent_id
);
$intent->confirm();
}
.... and finally after the confirm, the script will call on generateResponse() to form the json to be returned to the caller.
.... afterwhich, the fetch calls the handleServerResponse() function to go through the loop again.
To see the full code for these parts, see server.php, script.js and index.html.