last changed Mar-2024
Using PHP to set up collect a payment using a custom input form with Elavon/ Opayo [previously SagePay]
This is a walk through of the PHP & JavaScript code required for collecting a card payment method using the payment gateway.
This code is based on an example found on opayo
The results shown are actual running code using the Elavon
The code here is using the Opayo developers test environment and will not collect real funds.
- warning WARNING: This code is not 100% working code and is still being worked out how to implement fully - handle with care
Opayo fees in the UK don't seem to be published any more, but were around £32p
Create an MSK
Step 1 : Create a merchant session key (MSK)
custom-form.php
echo "<div style='background-color:red; color:white; padding:10px'><i class='material-icons'>info</ i> ";
if ( 1==1 ) {
echo "This is a test/ demonstrator of the results of calling on opayo for making payments; the environment is a sandbox ";
echo "and no transactions are actually made.";
echo "<p id='linkback'>";
echo "<BR>Please refer to <a href='../ ../ Articles/ opayo/ docs'>the accompanying documentation</ a> for a code walk through and explanation.";
echo "</ p>";
}
echo "</ div>";
$hms = date("YmdHis");
$testprofile = "";
if( isset($_GET['testprofile']) ) $testprofile = $_GET['testprofile'];
$extra_checks = "";
echo "<h1>Step 1</ h1>";
//--CodeStep1:start
// Keys taken from https://developer-eu.elavon.com/ docs/ opayo/ test-sandbox
$ikey = "hJYxsw7HLbj40cB8udES8CDRFLhuJ8G54O6rDpUXvE6hYDrria";
$ipwd = "o2iHSrFybYMZpmWOQMuhsXP52V4fBtpuSDshrKDSWsBY1OiN6hwd9Kb12z4j5Us5u";
$vendorName = "sandbox";
if( isset($testprofile) and strcmp($testprofile, "Extra Checks") == 0 ) { // See Step 2 input form
$ikey = "dq9w6WkkdD2y8k3t4olqu8H6a0vtt3IY7VEsGhAtacbCZ2b5Ud";
$ipwd = "hno3JTEwDHy7hJckU4WuxfeTrjD0N92pIaituQBw5Mtj7RG3V8zOdHCSPKwJ02wAV";
$vendorName = "sandboxEC";
$extra_checks = " selected";
}
$key = base64_encode("$ikey:$ipwd");
$curl = curl_init();
if ( 1 ) {
curl_setopt_array($curl, array(
CURLOPT_URL => "https://pi-test.sagepay.com/ api/ v1/ merchant-session-keys",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => '{ "vendorName": "' . $vendorName . '" }',
CURLOPT_HTTPHEADER => array(
"Authorization: Basic " . $key,
"Cache-Control: no-cache",
"Content-Type: application/ json"
)
));
$responseJson = curl_exec($curl);
$response = json_decode($responseJson, true); // false will allow you to do $response->merchantSessionKey, true will allow you to do $response['merchantSessionKey']
$err = curl_error($curl);
}
curl_close($curl);
echo "<LI>Response<PRE>$responseJson</ PRE></ LI>";
if( $err ) echo "Err<PRE>$err</ PRE>";
$msk = $response['merchantSessionKey'];
echo "<LI>MSK:$msk</ LI>";
Generate input form
Now we have the Elavon
This form will also include a hidden card-identifier field, that is populated by the submit button before the form is submitted, and uses the sagepayOwnForm()
(from the sagepay.js) function to tokenize the card details. You will notice that the card number, expiry, and csv do not include <input name=''>
parameters and are therefore not posted to the processor - this is deliberate to keep them secure - opting to pass the information via the card-identifier that sage computes and populates before the posting.
The form also includes any fancy footwork you want to make it appear professional - in this ones case, we are adding card-type background-images to the card-number when it's set, and a CVV background-image to assist the UX. We also have some JavaScript around the card-number to format it once entered, the expiry date to improve the UX (allowing dates without the slash to be given, etc). We also use some inputmode and pattern parameters to some of the inputs to improve mobile UX (giving an appropriate keyboard when necessary).
We have also included some CSS to provide a responsive UX and to use a smaller screen footprint through side-by-side fields.
Note the special handling of the card brand logo, which would have had the image on the card-number field, but this is blocked from on mobile devices, so we've had to overlay another field to handle this.
You can find the example footwork in js
When clicking on the Make Payment button, the form will be processed and then submitted to the next script (mysagepay-processor.php). For readability, I have also used a <form target='...'>
to allow the output of the php call to be directed to the current page; whether this is a good idea or not is yet to be known.
Note, we are still missing google
custom-form.php :: Custom Card input form
echo / **@ lang HTML */ "
<p style='color:red'><i class='material-icons' style='vertical-align: middle; color: red'>warning</ i>
Never use real cards on this test page.
<div class='inputform'>
<div>
<h2>Input form</ h2>
<form action='functions/ mysagepay-processor2.php' target='formtarget' method='POST' class='payment_form'>
Test Profile:<i class='material-icons handcursor noselect' onClick='showHelp(this)'>help_outline</ i>
<select id='testprofile' name='testprofile' onChange='doChangeProfile(this)'><option>Basic</ option><option$extra_checks>Extra Checks</ option></ select>
<div id='profilehelp' class='hidden'></ div>
<div class='field field50'>
<label for='firstnm'>First Name</ label>
<div><input id='firstnm' name='firstnm' data-card-details='cardholder-firstname'
onChange='doChange(this)' onKeyUp='doChange(this)'></ div>
</ div>
<div class='field field50'>
<label for=''>Last Name</ label>
<div><input id='lastnm' name='lastnm' data-card-details='cardholder-lastname'
onChange='doChange(this)' onKeyUp='doChange(this)'></ div>
</ div>
<div class='field field50'>
<label for='card-number'>Card</ label>
<div><input id='card-number' type='text' inputmode='numeric' data-card-details='card-number'
placeholder='4242 4242 4242 4242'
onChange='doChange(this)' pattern='[0-9\s]*'>
<div class='cardlogo'><div></ div></ div>
</ div>
</ div>
<div class='field field25'>
<label for='expiry'>Expiry</ label>
<div><input id='expiry' type='text' data-card-details='expiry-date'
placeholder='MM/ YY' onChange='doChange(this)' pattern='[0-9\/ \s]*'></ div>
</ div>
<div class='field field25'>
<label for='security-code'>CVV</ label>
<div><input id='security-code' type='text' data-card-details='security-code'
onChange='doChange(this)' onKeyUp='doChange(this)'
placeholder='123'></ div>
</ div>
<div class='field'>
<label for='billing_country'>Country</ label>
<div>
<select id='billing_country' name='billing_country' data-card-details='billing_country' onChange='doChange(this)'>
<option></ option>
<option value='AF'>Afghanistan</ option>
<option value='AX'>Åland Islands</ option>
<option value='AL'>Albania</ option>
.
.
. <option value='ZM'>Zambia</ option>
<option value='ZW'>Zimbabwe</ option></ select>
</ select>
</ div>
</ div>
<div class='field'>
<label for='billing_address'>Address Line 1</ label>
<div><input id='billing_address' name='billing_address' data-card-details='billing_address'
onChange='doChange(this)' onKeyUp='doChange(this)'></ div>
</ div>
<div class='field field50'>
<label for='billing_city'>Town/ City</ label>
<div><input id='billing_city' name='billing_city' data-card-details='billing_city'
onChange='doChange(this)' onKeyUp='doChange(this)'></ div>
</ div>
<div class='field field50'>
<label for='billing_zip' id='billing_zip_label'>Postal Code</ label>
<div><input id='billing_zip' name='billing_zip' data-card-details='billing_zip'
onChange='doChange(this)' onKeyUp='doChange(this)'></ div>
</ div>
<input type='hidden' name='card-identifier'>
<BR><BR><HR>
MSK:<input type='text' readonly name='merchantSessionKey' value='$msk'>
<br>
<br>
<div id='submit-container'>
<button type='submit' id='submit_but' onClick='doShowTarget()' disabled='true'>Make Payment</ button>
<div id='submitbutmsg'>Please fill in card details to enable the payment button</ div>
</ div>
</ form>
</ div>
<div>
" . getCardHelperTable() . "
</ div>
</ div>
<br>
<div id='messages' class='warningBg hidden'></ div>
<p>Clicking Make Payment will make the sagepay.js call the form action page and into the step 3 target below</ p>
";
custom-form.php :: form submit-tokenizer
echo / **@ lang Javascript */ "
<script src='https://pi-test.sagepay.com/ api/ v1/ js/ sagepay.js'></ script>
<script src='https://pi-test.opayo.co.uk/ api/ v1/ js/ sagepay-dropin.js'></ script>
<script>
document.querySelector('[type=submit]')
.addEventListener('click', function(e) {
e.preventDefault(); // to prevent form submission
dge('messages').innerHTML='';
addClassName(dge('messages'),'hidden');
sagepayOwnForm({ merchantSessionKey: '$msk' })
.tokeniseCardDetails({
cardDetails: {
cardholderName: dge('firstnm').value+' '+dge('lastnm').value,
cardNumber: dge('card-number').value.replace(/ / g, '').trim(),
expiryDate: dge('expiry').value.replace(/ \//g, ''),
securityCode: dge('security-code').value
},
onTokenised:function(result) {
if (result.success) {
// Put the token result into card-identifier before posting the <form> to the processing stage...
document.querySelector('[name=\"card-identifier\"]').value = result.cardIdentifier;
document.querySelector('form').submit();
}
else {
console.log('ERROR: '+JSON.stringify(result));
removeClassName(dge('messages'), 'hidden');
dge('messages').innerHTML=JSON.stringify(result);
removeClassName(dge('firstnm'), 'warning');
removeClassName(dge('lastnm'), 'warning');
removeClassName(dge('expiry'), 'warning');
removeClassName(dge('card-number'), 'warning');
removeClassName(dge('security-code'),'warning');
if ( JSON.stringify(result).indexOf('The cardholder name is required')>-1 ) addClassName(dge('firstnm'),'warning');
if ( JSON.stringify(result).indexOf('The cardholder name is required')>-1 ) addClassName(dge('lastnm'),'warning');
if ( JSON.stringify(result).indexOf('The expiry date is invalid')>-1 ) addClassName(dge('expiry'),'warning');
if ( JSON.stringify(result).indexOf('The expiry date has to be in MMYY format')>-1 ) addClassName(dge('expiry'),'warning');
if ( JSON.stringify(result).indexOf('The card number length is invalid')>-1 ) addClassName(dge('card-number'),'warning');
if ( JSON.stringify(result).indexOf('The card number is invalid')>-1 ) addClassName(dge('card-number'),'warning');
if ( JSON.stringify(result).indexOf('The security code length is invalid')>-1 ) addClassName(dge('security-code'),'warning');
}
}
});
}, false);
// TODO: Find a better way to detect when the credit card number has been auto-filled by a mobile:
setInterval(function() {
setCardLogo(dge('card-number'));
},2500);
//-----------------------------------------------------------------------------------------
function addClassName(elem, classN) {
if ( !elem.className ) elem.className='';
if ( elem.className=='undefined' ) elem.className='';
const regex = new RegExp('\\\b'+classN+'\\\b', 'g');
if (!regex.test(elem.className)) {
elem.className=(elem.className+' '+classN).trim();
}
}
//-----------------------------------------------------------------------------------------
function removeClassName(elem, classN) {
var elemcn=elem.className;
const regex = new RegExp('\\\b'+classN+'\\\b', 'g');
var newcn=elemcn.replace(regex, '');
elem.className=newcn;
}
// ---------------------------------------------
function doShowTarget() {
console.log('doShowTarget');
addClassName(dge('formtarget0'),'hidden');
removeClassName(dge('formtarget'),'hidden');
document.getElementById('formtarget').src='';
setTimeout(function() {
console.log(document.getElementById('formtarget').contentDocument);
if ( !document.getElementById('formtarget').contentDocument || document.getElementById('formtarget').contentDocument.getElementsByTagName('body')[0].innerHTML!='') {
document.getElementById('formtarget').scrollIntoView({behavior: 'smooth', block: 'end', inline: 'nearest'});
}
},1000);
}
</ script>
";
Post card details
The credit card core details have now been posted through card-identifier, and the rest are passed through regular <form> posted fields, to our next step, mysagepay-processor2 that then requests a payment, adding in amounts and currency information before posting it to sagepay.com via a curl POST request.
In response to the curl, it is possible that Elavon
OK | Process executed without error and the transaction has been authorised |
NOTAUTHED | The Sage Pay gateway could not authorise the transaction because the details provided by the customer were incorrect, or insufficient funds were available. However, the transaction has completed. Also returned for PayPal transactions in response to the PayPal Completion Post (if Accept=NO was sent to complete PayPal transaction, see Appendix A8). |
REJECTED | The Sage Pay System rejected the transaction because of the fraud screening rules you have set on your account.Note: The bank may have authorised the transaction but your own rule bases for AVS |
AUTHENTICATED | The 3D-Secure checks were performed successfully, and the card details13. Sage Pay Direct Integration and Protocol and Guidelines 3.00 Page 102 of 115 secured at Sage Pay. Only returned if TxType is AUTHENTICATE. |
REGISTERED | 3D-Secure checks failed or were not performed, but the card details are still secured at Sage Pay. Only returned if TxType is AUTHENTICATE. |
PPREDIRECT | |
MALFORMED | Input message was missing fields or badly formatted – normally will only occur during development. |
3DAUTH | The customer needs to be directed to their card issuer for 3D-Authentication |
INVALID | Transaction was not registered because although the POST format was valid, some information supplied was invalid. e.g. incorrect vendor name or currency. |
ERROR | A problem occurred at Sage Pay which prevented transaction registration. Please notify Sage Pay if a Status of ERROR is seen, together with your Vendor, VendorTxCode and the StatusDetail. |
See page 100 of the Sage Pay Direct Integration and Protocol Guidelines 4.00 for more information.
custom-form.php :: target iframe for demo
<?php
//--CodeAll:start -all
include_once('functions/ test_card_helper_table.php');
// --------------------------------------------
function main() {
// Keys taken from https://developer-eu.elavon.com/ docs/ opayo/ test-sandbox
$ikey = "hJYxsw7HLbj40cB8udES8CDRFLhuJ8G54O6rDpUXvE6hYDrria";
$ipwd = "o2iHSrFybYMZpmWOQMuhsXP52V4fBtpuSDshrKDSWsBY1OiN6hwd9Kb12z4j5Us5u";
$key = base64_encode("$ikey:$ipwd");
//--CodeStep1:start
echo "<div style='background-color:red; color:white; padding:10px'><i class='material-icons'>info</ i> ";
if ( 1==1 ) {
echo "This is a test/ demonstrator of the results of calling on opayo for making payments; the environment is a sandbox ";
echo "and no transactions are actually made.";
echo "<p id='linkback'>";
echo "<BR>Please refer to <a href='../ ../ Articles/ opayo/ docs'>the accompanying documentation</ a> for a code walk through and explanation.";
echo "</ p>";
}
echo "</ div>";
$hms = date("YmdHis");
$testprofile = "";
if( isset($_GET['testprofile']) ) $testprofile = $_GET['testprofile'];
$extra_checks = "";
echo "<h1>Step 1</ h1>";
//--CodeStep1:start
// Keys taken from https://developer-eu.elavon.com/ docs/ opayo/ test-sandbox
$ikey = "hJYxsw7HLbj40cB8udES8CDRFLhuJ8G54O6rDpUXvE6hYDrria";
$ipwd = "o2iHSrFybYMZpmWOQMuhsXP52V4fBtpuSDshrKDSWsBY1OiN6hwd9Kb12z4j5Us5u";
$vendorName = "sandbox";
if( isset($testprofile) and strcmp($testprofile, "Extra Checks") == 0 ) { // See Step 2 input form
$ikey = "dq9w6WkkdD2y8k3t4olqu8H6a0vtt3IY7VEsGhAtacbCZ2b5Ud";
$ipwd = "hno3JTEwDHy7hJckU4WuxfeTrjD0N92pIaituQBw5Mtj7RG3V8zOdHCSPKwJ02wAV";
$vendorName = "sandboxEC";
$extra_checks = " selected";
}
$key = base64_encode("$ikey:$ipwd");
$curl = curl_init();
if ( 1 ) {
curl_setopt_array($curl, array(
CURLOPT_URL => "https://pi-test.sagepay.com/ api/ v1/ merchant-session-keys",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => '{ "vendorName": "' . $vendorName . '" }',
CURLOPT_HTTPHEADER => array(
"Authorization: Basic " . $key,
"Cache-Control: no-cache",
"Content-Type: application/ json"
)
));
$responseJson = curl_exec($curl);
$response = json_decode($responseJson, true); // false will allow you to do $response->merchantSessionKey, true will allow you to do $response['merchantSessionKey']
$err = curl_error($curl);
}
curl_close($curl);
echo "<LI>Response<PRE>$responseJson</ PRE></ LI>";
if( $err ) echo "Err<PRE>$err</ PRE>";
$msk = $response['merchantSessionKey'];
echo "<LI>MSK:$msk</ LI>"; //--CodeStep1:end
$yy = date("y");
$mm = date("m");
echo "<p class='info colPaleGreyBg'>HINT: Use the Test card options listed below (e.g. 4929000000006 - expires $mm/ $yy, CVC 123, etc)</ p>";
//--CodeStep2a:start
echo / **@ lang HTML */ "
<p style='color:red'><i class='material-icons' style='vertical-align: middle; color: red'>warning</ i>
Never use real cards on this test page.
<div class='inputform'>
<div>
<h2>Input form</ h2>
<form action='functions/ mysagepay-processor2.php' target='formtarget' method='POST' class='payment_form'>
Test Profile:<i class='material-icons handcursor noselect' onClick='showHelp(this)'>help_outline</ i>
<select id='testprofile' name='testprofile' onChange='doChangeProfile(this)'><option>Basic</ option><option$extra_checks>Extra Checks</ option></ select>
<div id='profilehelp' class='hidden'></ div>
<div class='field field50'>
<label for='firstnm'>First Name</ label>
<div><input id='firstnm' name='firstnm' data-card-details='cardholder-firstname'
onChange='doChange(this)' onKeyUp='doChange(this)'></ div>
</ div>
<div class='field field50'>
<label for=''>Last Name</ label>
<div><input id='lastnm' name='lastnm' data-card-details='cardholder-lastname'
onChange='doChange(this)' onKeyUp='doChange(this)'></ div>
</ div>
<div class='field field50'>
<label for='card-number'>Card</ label>
<div><input id='card-number' type='text' inputmode='numeric' data-card-details='card-number'
placeholder='4242 4242 4242 4242'
onChange='doChange(this)' pattern='[0-9\s]*'>
<div class='cardlogo'><div></ div></ div>
</ div>
</ div>
<div class='field field25'>
<label for='expiry'>Expiry</ label>
<div><input id='expiry' type='text' data-card-details='expiry-date'
placeholder='MM/ YY' onChange='doChange(this)' pattern='[0-9\/ \s]*'></ div>
</ div>
<div class='field field25'>
<label for='security-code'>CVV</ label>
<div><input id='security-code' type='text' data-card-details='security-code'
onChange='doChange(this)' onKeyUp='doChange(this)'
placeholder='123'></ div>
</ div>
<div class='field'>
<label for='billing_country'>Country</ label>
<div>
<select id='billing_country' name='billing_country' data-card-details='billing_country' onChange='doChange(this)'>
<option></ option>
<option value='AF'>Afghanistan</ option>
<option value='AX'>Åland Islands</ option>
<option value='AL'>Albania</ option>
.
.
. <option value='ZM'>Zambia</ option>
<option value='ZW'>Zimbabwe</ option></ select>
</ select>
</ div>
</ div>
<div class='field'>
<label for='billing_address'>Address Line 1</ label>
<div><input id='billing_address' name='billing_address' data-card-details='billing_address'
onChange='doChange(this)' onKeyUp='doChange(this)'></ div>
</ div>
<div class='field field50'>
<label for='billing_city'>Town/ City</ label>
<div><input id='billing_city' name='billing_city' data-card-details='billing_city'
onChange='doChange(this)' onKeyUp='doChange(this)'></ div>
</ div>
<div class='field field50'>
<label for='billing_zip' id='billing_zip_label'>Postal Code</ label>
<div><input id='billing_zip' name='billing_zip' data-card-details='billing_zip'
onChange='doChange(this)' onKeyUp='doChange(this)'></ div>
</ div>
<input type='hidden' name='card-identifier'>
<BR><BR><HR>
MSK:<input type='text' readonly name='merchantSessionKey' value='$msk'>
<br>
<br>
<div id='submit-container'>
<button type='submit' id='submit_but' onClick='doShowTarget()' disabled='true'>Make Payment</ button>
<div id='submitbutmsg'>Please fill in card details to enable the payment button</ div>
</ div>
</ form>
</ div>
<div>
" . getCardHelperTable() . "
</ div>
</ div>
<br>
<div id='messages' class='warningBg hidden'></ div>
<p>Clicking Make Payment will make the sagepay.js call the form action page and into the step 3 target below</ p>
";//--CodeStep2a:end
//--CodeStep2b:start
echo / **@ lang Javascript */ "
<script src='https://pi-test.sagepay.com/ api/ v1/ js/ sagepay.js'></ script>
<script src='https://pi-test.opayo.co.uk/ api/ v1/ js/ sagepay-dropin.js'></ script>
<script>
document.querySelector('[type=submit]')
.addEventListener('click', function(e) {
e.preventDefault(); // to prevent form submission
dge('messages').innerHTML='';
addClassName(dge('messages'),'hidden');
sagepayOwnForm({ merchantSessionKey: '$msk' })
.tokeniseCardDetails({
cardDetails: {
cardholderName: dge('firstnm').value+' '+dge('lastnm').value,
cardNumber: dge('card-number').value.replace(/ / g, '').trim(),
expiryDate: dge('expiry').value.replace(/ \//g, ''),
securityCode: dge('security-code').value
},
onTokenised:function(result) {
if (result.success) {
// Put the token result into card-identifier before posting the <form> to the processing stage...
document.querySelector('[name=\"card-identifier\"]').value = result.cardIdentifier;
document.querySelector('form').submit();
}
else {
console.log('ERROR: '+JSON.stringify(result));
removeClassName(dge('messages'), 'hidden');
dge('messages').innerHTML=JSON.stringify(result);
removeClassName(dge('firstnm'), 'warning');
removeClassName(dge('lastnm'), 'warning');
removeClassName(dge('expiry'), 'warning');
removeClassName(dge('card-number'), 'warning');
removeClassName(dge('security-code'),'warning');
if ( JSON.stringify(result).indexOf('The cardholder name is required')>-1 ) addClassName(dge('firstnm'),'warning');
if ( JSON.stringify(result).indexOf('The cardholder name is required')>-1 ) addClassName(dge('lastnm'),'warning');
if ( JSON.stringify(result).indexOf('The expiry date is invalid')>-1 ) addClassName(dge('expiry'),'warning');
if ( JSON.stringify(result).indexOf('The expiry date has to be in MMYY format')>-1 ) addClassName(dge('expiry'),'warning');
if ( JSON.stringify(result).indexOf('The card number length is invalid')>-1 ) addClassName(dge('card-number'),'warning');
if ( JSON.stringify(result).indexOf('The card number is invalid')>-1 ) addClassName(dge('card-number'),'warning');
if ( JSON.stringify(result).indexOf('The security code length is invalid')>-1 ) addClassName(dge('security-code'),'warning');
}
}
});
}, false);
// TODO: Find a better way to detect when the credit card number has been auto-filled by a mobile:
setInterval(function() {
setCardLogo(dge('card-number'));
},2500);
//-----------------------------------------------------------------------------------------
function addClassName(elem, classN) {
if ( !elem.className ) elem.className='';
if ( elem.className=='undefined' ) elem.className='';
const regex = new RegExp('\\\b'+classN+'\\\b', 'g');
if (!regex.test(elem.className)) {
elem.className=(elem.className+' '+classN).trim();
}
}
//-----------------------------------------------------------------------------------------
function removeClassName(elem, classN) {
var elemcn=elem.className;
const regex = new RegExp('\\\b'+classN+'\\\b', 'g');
var newcn=elemcn.replace(regex, '');
elem.className=newcn;
}
// ---------------------------------------------
function doShowTarget() {
console.log('doShowTarget');
addClassName(dge('formtarget0'),'hidden');
removeClassName(dge('formtarget'),'hidden');
document.getElementById('formtarget').src='';
setTimeout(function() {
console.log(document.getElementById('formtarget').contentDocument);
if ( !document.getElementById('formtarget').contentDocument || document.getElementById('formtarget').contentDocument.getElementsByTagName('body')[0].innerHTML!='') {
document.getElementById('formtarget').scrollIntoView({behavior: 'smooth', block: 'end', inline: 'nearest'});
}
},1000);
}
</ script>
";//--CodeStep2b:end
//--CodeStep3:start
echo "<h1>Step 3</ h1>";
echo "<div class='demo'><iframe name='formtarget' id='formtarget' class='hidden'></ iframe></ div>";//--CodeStep3:end
}
// -------------------------------------------------------------------
echo "<!DOCTYPE html>";
echo "<html>";
echo "<head>";
echo "<link rel='stylesheet preload prefetch' as='style' href='https://fonts.googleapis.com/ icon?family=Courier+Prime|Roboto|Material+Icons|Material+Icons+Outlined&display=block'>";
echo "<link rel='stylesheet preload prefetch' as='style' href='css/ custom-form.css'>";
echo "<script src='https://methodfish.com/ js/ v'></ script><!-- No download -->";
echo "<link rel='icon' type='image/ png' href='https://methodfish.com/ images/ methodfish.ico'>";
echo "<script type='text/ JavaScript' SRC='js/ custom-form.js' type='text/ javascript'></ script>";
echo "<style>";
echo / **@ lang CSS */ "
body {
font-family: Roboto;
font-size: 13pt;
line-height: 1.6em;
}
.colPaleGreyBg {
background-color: #d9d8d8!important;
}
.info {
display: inline-block;
background-color: #e4f2fb;
padding: 10px;
border-radius: 10px;
width: calc(100% - 20px);
margin-bottom: 8px;
}
#sp-container {
width: calc(100% - 20px);
}
iframe, #formtarget0 {
border: solid 1px red!important;
-webkit-box-shadow: 5px 5px 30px 0px rgba(255, 0, 0, 0.62);
-moz-box-shadow: 5px 5px 30px 0px rgba(255, 0, 0, 0.62);
box-shadow: 5px 5px 30px 0px rgba(255, 0, 0, 0.62);
height: 320px;
}
iframe, .iframe {
border: solid 1px red;
-webkit-box-shadow: 0 1px 11px 1px rgba(255,0,0,.62);
-moz-box-shadow: 0 1px 11px 1px rgba(255,0,0,.62);
box-shadow: 0 1px 11px 1px rgba(255,0,0,.62);
min-height: 100px;
padding: 10px;
text-align: center;
width: calc(100% - 20px);
}
.inputform {
width:calc(100% - 10px);
display:flex;
flex-wrap:wrap;
gap:10px;
}
.inputform > div {
flex:1;
xwidth:calc(50% - 40px);
xdisplay:inline-block;
vertical-align:top;
overflow:auto;
padding-right:10px;
}
.inputform > div:last-child {
color:white;
font-size:80%;
padding-left:10px;
background-color:darkgrey;
max-width:600px;
}
.inputform > div:last-child a {
color:white;
}
.inputform h2 {
font-size:16px;
margin-top:0;
border-bottom:solid 1px black;
}
@ media (max-width:800px) {
.inputform > div {
flex-basis:100%;
width:100vw;
}
}
.form-group {
line-height: 40px;
overflow: auto;
border-bottom: 1px solid #ddd;
box-sizing: border-box;
}
.label-column {
float: left;
width: 12%;
max-width: 200px;
min-width: 100px;
margin-right: 4px;
clear: left;
}
.input-column {
float: left;
width: 70%;
min-width: 220px;
}
.input-column input {
border: none!important;
border-radius: 0!important;
padding: 2px 4px!important;
line-height: 1.3em;
}
input {
width: calc(100% - 25px);
}
form {
background-color:#ebebeb;
}
input, select {
font-family: Roboto;
font-size: 13pt;
padding: 9px;
border: solid 1px grey;
border-radius: 9px;
}
.input-column input {
border: none!important;
border-radius: 0!important;
padding: 2px 4px!important;
line-height: 1.3em;
}
button, .button {
font-size: 10pt;
padding: 10px;
margin-top: 10px;
margin-right: 10px;
background-color: #11b4ff;
color: white;
border: 0;
cursor: pointer;
transition: all .25s ease-in-out;
text-decoration: none;
min-width: 100px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
input:read-only {
background-color: inherit;
}
form>input, form>textarea {
width: calc(100% - 26px);
padding: 8px;
margin: 4px;
border: solid 1px grey;
border-radius: 9px;
}
.hidden {
display: none!important;
}
pre {
font-family: Courier Prime;
font-size: 9pt;
margin: 15px;
padding: 10px;
overflow: auto;
border: solid 1px #b3b0b0;
width: calc(100% - 100px);
max-height: 500px;
line-height: 1.5em;
background-color: #e7e7e7;
resize: vertical;
min-height: 20px;
}
table.testcards td {
white-space:nowrap;
}
";
echo "</ style>";
echo "</ head>";
echo "<body>";
main();
echo "<script>";
echo "
if (window.self !== window.top) {
// if the page is loaded in an iframe, hide the div with id 'myDiv'
var myDiv = document.getElementById('linkback');
myDiv.style.display = 'none';
}
";
echo "</ script>";
echo "</ body>";
echo "</ html>";
mysagepay-processor2.php :: payment request processor
<?php
//--CodeAll:start -all
include_once('class.const.php');
function getSafePostTx(string $val=null) {
return (str_replace("&", "&"
,str_replace("<", "<"
,str_replace(">", ">"
, strip_tags($val)))));
}
$http="https://";
if ( config::localDeveloper ) $home ="http://localhost/ methodfish/ public_html";
else $home = $http.$_SERVER['SERVER_NAME'];
$trace="";
if ( 1 ) {
foreach ($_GET as $k => $v) {
if (strcmp($k, "t") != 0) $trace .= "<LI>GET ".getSafePostTx("[$k]=[$v]");
}
foreach ($_POST as $k => $v) {
if (strcmp($k, "t") != 0) $trace .= "<LI>POST ".getSafePostTx("[$k]=[$v]");
}
if (strcmp($trace, "") != 0) {
echo "<div style='color:white;background-color:red; padding:8px;'>GET/ POST arguments detected:<BR>$trace</ div>";
}
}
if ( 1 ) {
//--CodeStep3.2:start
if (isset($_GET['confirmed'])) {
echo "<p>Transaction completed</ p>";
echo "<script>alert('custom alert message here - see Step 3 code / mysagepay-processor2');</ script>";
exit;
}
$merchantSessionKey = getSafePostTx($_POST["merchantSessionKey"]);
$cardIdentifier = getSafePostTx($_POST["card-identifier"]);
$firstName = getSafePostTx($_POST["firstnm"]);
$lastName = getSafePostTx($_POST["lastnm"]);
$billing_address = getSafePostTx($_POST["billing_address"]);
$billing_city = getSafePostTx($_POST["billing_city"]);
$billing_zip = getSafePostTx($_POST["billing_zip"]);
$billing_country = getSafePostTx($_POST["billing_country"]);
$testProfile = getSafePostTx($_POST['testprofile']);
$amount = 100 + random_int(0,100);
$currency = "GBP";
// Keys taken from https://developer-eu.elavon.com/ docs/ opayo/ test-sandbox
$profile="Basic";
if ( strcmp($testProfile, "Extra Checks")==0 ) $profile="Extra Checks";
if ( strcmp($profile, "Basic")==0 ) {
$ikey = "hJYxsw7HLbj40cB8udES8CDRFLhuJ8G54O6rDpUXvE6hYDrria";
$ipwd = "o2iHSrFybYMZpmWOQMuhsXP52V4fBtpuSDshrKDSWsBY1OiN6hwd9Kb12z4j5Us5u";
echo "<LI>Basic checks</ LI>";
}
else {
$ikey = "dq9w6WkkdD2y8k3t4olqu8H6a0vtt3IY7VEsGhAtacbCZ2b5Ud";
$ipwd = "hno3JTEwDHy7hJckU4WuxfeTrjD0N92pIaituQBw5Mtj7RG3V8zOdHCSPKwJ02wAV";
echo "<LI>Extra checks</ LI>";
}
$key = base64_encode("$ikey:$ipwd");
// vendorTxCode is your unique transaction reference identifying each separate transaction registration
// (less than 40 chars, no special characters, and no spaces)
$vcd="Methodfish-Example".time();
$curl = curl_init();
if ( 1 ) {
$postfields = '{' .
'"transactionType" : "Payment",' .
'"paymentMethod" : {' .
' "card" : {' .
' "merchantSessionKey" : "' . $merchantSessionKey . '",' .
' "cardIdentifier" : "' . $cardIdentifier . '"' .
' }' .
'},' .
'"vendorTxCode" : "' . $vcd . '",' .
'"amount" : ' . $amount . ',' .
'"currency" : "' . $currency . '",' .
'"customerFirstName": "' . $firstName . '",' .
'"customerLastName" : "' . $lastName . '",' .
'"description" : "Methodfish.com Example",' .
'"apply3DSecure" : "UseMSPSetting",' .
'"entryMethod" : "Ecommerce",' .
'"billingAddress" : {' .
' "address1" : "' . $billing_address . '",' .
' "city" : "' . $billing_city . '",' .
' "postalCode" : "' . $billing_zip . '",' .
' "country" : "' . $billing_country . '"' .
'}' .
'}';
echo "<LI>POSTFIELDS:<div style='border:solid 1px grey; background-color:#e8e8e8'>" .
str_replace(",", ",<BR>",
str_replace("{", "{<OL>",
str_replace("}", "}</ OL>", $postfields))) . "</ div>";
curl_setopt_array($curl, array(
CURLOPT_URL => "https://pi-test.sagepay.com/ api/ v1/ transactions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => $postfields,
CURLOPT_HTTPHEADER => array(
"Authorization: Basic $key",
"Cache-Control: no-cache",
"Content-Type: application/ json"
),
));
$responseJson = curl_exec($curl);
$response = json_decode($responseJson, true); // false will allow you to do $response->merchantSessionKey, true will allow you to do $response['merchantSessionKey']
$err = curl_error($curl);
echo "<LI>result:</ LI><PRE style='border:solid 1px grey; background-color:#e8e8e8; padding:10px'>$responseJson</ PRE>";
if( 1 ) {
echo "<PRE>";
print_r($response);
echo "</ PRE>";
}
if( strcmp($err, "") != 0 ) echo "<LI>err:</ LI><PRE>$err</ PRE><HR>";
}
curl_close($curl);
if ( isset($response['description']) and strcmp($response['description'], "Authentication values are missing")==0 ) {
echo "<LI>Authentication values are missing = check your curl header includes \"Authorization: Basic <encoded key>\"";
}
if ( isset($response['description']) and strcmp($response['description'], "Authentication values are missing")==0 ) {
echo "<LI>Authentication values are missing = check your curl header includes \"Authorization: Basic <encoded key>\"";
}
if ( isset($response['status']) and strcmp($response['status'], '3DAuth')==0 ) {
// See https://developer-eu.elavon.com/ docs/ opayo/ 3d-secure-authentication
$txnid = $response['transactionId'];
$acsUrl = $response['acsUrl'];
$paReq = $response['paReq'];
echo / ** @ lang HTML */ "
<div style='background-color:white; border:solid 1px grey; padding:10px;'>
<p>3DAuth requires customer to complete the transaction using the following link</ p>
<form action='$acsUrl' method='post' id='pa-form'>
<input type='hidden' name='PaReq' value='$paReq'> <!-- [PREVIOUSLY RETURNED PAREQ] -->
<input type='hidden' name='TermUrl' value='$home/ mysagepay-processor2.php?confirmed=y'> <!-- [ENDPOINT ON YOUR SERVER WHICH HANDLES RESPONSE FROM 3DSECURE PROVIDER] -->
<input type='hidden' name='MD' value=''> <!-- [YOUR UNIQUE REFERENCE NUMBER FOR THIS AUTHENTICATION] -->
<input type='hidden' name='some-information' value='some-value'> <!-- This demonstrates the field won't go anywhere -->
<!-- Prompt the user to submit the 3DAuth form
Alternatively, use auto-submitting javascript
e.g. <script>document.addEventListener('DOMContentLoaded',function(){var b=document.getElementById('pa-form');b&&b.submit()})</ script>
-->
<input type='submit' style='padding:8px; background-color:#11b4ff; color:white; border:none; cursor:pointer' value='Continue to Card Verification...'/ >
</ form>
</ div>
";
}
if ( isset($response['errors']) ) {
$errors=($response['errors']);
foreach($errors as $e=>$msg) {
$desc=$msg['description'];
$prop=$msg['property'];
$code=$msg['code'];
echo "<LI>ERRORS: $e = [$desc] [$prop] [$code]";
}
}
//--CodeStep3.2:end
}
?>
link Click to view a demo
square