Skip to content

Commit b25bc49

Browse files
committed
Init
0 parents  commit b25bc49

14 files changed

+1179
-0
lines changed

composer.json

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "getcandy/opayo",
3+
"type": "project",
4+
"description": "Opayo payment driver for GetCandy.",
5+
"keywords": ["getcandy", "laravel", "ecommerce", "e-commerce", "headless", "store", "shop", "cart", "opayo"],
6+
"license": "MIT",
7+
"authors": [
8+
{
9+
"name": "GetCandy",
10+
"homepage": "https://getcandy.io/"
11+
}
12+
],
13+
"require": {
14+
"php": "^8.0",
15+
"getcandy/core": "*",
16+
"livewire/livewire": "^2.0"
17+
},
18+
"require-dev": {
19+
"phpunit/phpunit": "^9.5.10",
20+
"mockery/mockery": "^1.4.4",
21+
"orchestra/testbench": "^6.0|^7.0"
22+
},
23+
"autoload": {
24+
"psr-4": {
25+
"GetCandy\\Opayo\\": "src/"
26+
}
27+
},
28+
"autoload-dev": {
29+
"psr-4": {
30+
"Tests\\": "tests/"
31+
}
32+
},
33+
"extra": {
34+
"getcandy": {
35+
"name": "Opayo Payments"
36+
},
37+
"laravel": {
38+
"providers": [
39+
"GetCandy\\Opayo\\OpayoServiceProvider"
40+
]
41+
}
42+
},
43+
"minimum-stability": "dev",
44+
"prefer-stable": true
45+
}

config/opayo.php

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
return [
4+
/*
5+
|--------------------------------------------------------------------------
6+
| Capture policy
7+
|--------------------------------------------------------------------------
8+
|
9+
| Here is where you can set whether you want to capture and charge payments
10+
| straight away, or create the Payment Intent and release them at a later date.
11+
|
12+
| automatic - Capture the payment straight away.
13+
| manual - Don't take payment straight away and capture later.
14+
|
15+
*/
16+
'policy' => 'automatic',
17+
];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<div x-data="{
2+
{{-- We use AlpineJs modelling here as we do not want the card details to go up to Livewire. --}}
3+
name: '{{ $this->billing->first_name }} {{ $this->billing->last_name }}',
4+
card: '4929000000006',
5+
expiry: '1232',
6+
cvv: '123',
7+
processing: @entangle('processing'),
8+
{{-- This is the tokenised card we need to send up to Livewire --}}
9+
identifier: @entangle('identifier'),
10+
merchantKey: @entangle('merchantKey'),
11+
errors: [],
12+
init() {
13+
window.addEventListener('opayo_threed_secure_response', e => {
14+
$wire.call('processThreed', {
15+
mdx: e.detail.mdx,
16+
md: e.detail.md,
17+
pares: e.detail.PaRes,
18+
cres: e.detail.cres
19+
})
20+
});
21+
},
22+
handleSubmit () {
23+
this.errors = []
24+
this.processing = true
25+
26+
const date = new Date();
27+
const tzOffset = date.getTimezoneOffset();
28+
29+
let screenSize = 'Large';
30+
31+
if (window.outerWidth < 400) {
32+
screenSize = 'Small';
33+
}
34+
35+
if (window.outerWidth < 800) {
36+
screenSize = 'Medium';
37+
}
38+
39+
$wire.set('browser', {
40+
browserLanguage: navigator.language,
41+
challengeWindowSize: screenSize,
42+
browserUserAgent: navigator.userAgent,
43+
browserJavaEnabled: navigator.javaEnabled(),
44+
browserColorDepth: window.screen.colorDepth,
45+
browserScreenHeight: window.outerHeight,
46+
browserScreenWidth: window.outerWidth,
47+
browserTZ: tzOffset,
48+
})
49+
50+
sagepayOwnForm({
51+
merchantSessionKey: this.merchantKey,
52+
}).tokeniseCardDetails({
53+
onTokenised: (result) => {
54+
if (!result.success) {
55+
this.errors = result.errors
56+
this.processing = false
57+
return
58+
}
59+
60+
$wire.set('identifier', result.cardIdentifier)
61+
$wire.set('sessionKey', this.merchantKey)
62+
$wire.call('process')
63+
},
64+
cardDetails: {
65+
cardholderName: this.name,
66+
cardNumber: this.card,
67+
expiryDate: this.expiry,
68+
securityCode: this.cvv,
69+
}
70+
})
71+
}
72+
}">
73+
@if($showChallenge)
74+
@include('getcandy::opayo.partials.threed-secure-modal')
75+
@endif
76+
77+
<form class="space-y-2" x-on:submit.prevent="handleSubmit()">
78+
<label class="space-y-1">
79+
<span class="text-sm font-medium">Cardholder Name</span>
80+
<input type="text" x-model="name" class="w-full border-gray-300 rounded shadow-sm" />
81+
</label>
82+
<div class="flex space-x-2">
83+
<label class="space-y-1 grow">
84+
<span class="text-sm font-medium">Card Number</span>
85+
<input type="number" x-model="card" class="w-full border-gray-300 rounded shadow-sm" placeholder="0000 0000 0000 0000" />
86+
</label>
87+
88+
<label class="w-24 space-y-1">
89+
<span class="text-sm font-medium">CVV</span>
90+
<input type="number" x-model="cvv" class="w-full border-gray-300 rounded shadow-sm" placeholder="123" />
91+
</label>
92+
93+
<label class="w-24 space-y-1">
94+
<span class="text-sm font-medium">Expiry</span>
95+
<input type="text" x-model="expiry" class="w-full border-gray-300 rounded shadow-sm" placeholder="MM/YY" />
96+
</label>
97+
</div>
98+
99+
<button
100+
class="flex items-center px-5 py-3 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-500 disabled:opacity-50"
101+
type="submit"
102+
x-bind:disabled="processing"
103+
>
104+
<span
105+
x-show="!processing"
106+
>
107+
Make Payment
108+
</span>
109+
<span
110+
x-show="processing"
111+
class="block mr-2"
112+
>
113+
<svg
114+
class="w-5 h-5 text-white animate-spin"
115+
xmlns="http://www.w3.org/2000/svg"
116+
fill="none"
117+
viewBox="0 0 24 24"
118+
>
119+
<circle
120+
class="opacity-25"
121+
cx="12"
122+
cy="12"
123+
r="10"
124+
stroke="currentColor"
125+
stroke-width="4"
126+
></circle>
127+
<path
128+
class="opacity-75"
129+
fill="currentColor"
130+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
131+
></path>
132+
</svg>
133+
</span>
134+
<span
135+
x-show="processing"
136+
>
137+
Processing
138+
</span>
139+
</button>
140+
</form>
141+
142+
<div x-show="errors.length" class="p-4 mt-4 space-y-2 rounded bg-red-50" x-cloak>
143+
<template x-for="(error, errorIndex) in errors" :key="errorIndex" hidden>
144+
<span x-text="error.message" class="block text-red-600"></span>
145+
</template>
146+
</div>
147+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
@php
2+
$id = $id ?? md5('showChallenge');
3+
4+
switch ($maxWidth ?? '2xl') {
5+
case 'sm':
6+
$maxWidth = 'sm:max-w-sm';
7+
break;
8+
case 'md':
9+
$maxWidth = 'sm:max-w-md';
10+
break;
11+
case 'lg':
12+
$maxWidth = 'sm:max-w-lg';
13+
break;
14+
case 'xl':
15+
$maxWidth = 'sm:max-w-xl';
16+
break;
17+
case '2xl':
18+
default:
19+
$maxWidth = 'sm:max-w-2xl';
20+
break;
21+
}
22+
@endphp
23+
24+
<div
25+
x-data="{
26+
show: true,
27+
focusables() {
28+
// All focusable element types...
29+
let selector = 'a, button, input, textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
30+
31+
return [...$el.querySelectorAll(selector)]
32+
// All non-disabled elements...
33+
.filter(el => ! el.hasAttribute('disabled'))
34+
},
35+
firstFocusable() { return this.focusables()[0] },
36+
lastFocusable() { return this.focusables().slice(-1)[0] },
37+
nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() },
38+
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
39+
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
40+
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
41+
autofocus() { let focusable = $el.querySelector('[autofocus]'); if (focusable) focusable.focus() },
42+
}"
43+
x-init="$watch('show', value => value && setTimeout(autofocus, 50))"
44+
x-on:close.stop="show = false"
45+
x-on:keydown.escape.window="show = false"
46+
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
47+
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
48+
x-show="show"
49+
id="{{ $id }}"
50+
class="fixed inset-x-0 top-0 px-4 pt-6 z-75 sm:px-0 sm:flex sm:items-top sm:justify-center"
51+
style="display: none;"
52+
>
53+
<div x-show="show" class="fixed inset-0 transition-all" x-on:click="show = false" x-transition:enter="ease-out duration-300"
54+
x-transition:enter-start="opacity-0"
55+
x-transition:enter-end="opacity-100"
56+
x-transition:leave="ease-in duration-200"
57+
x-transition:leave-start="opacity-100"
58+
x-transition:leave-end="opacity-0">
59+
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
60+
</div>
61+
62+
<div x-show="show" class="bg-white rounded-lg shadow-xl transition-all sm:w-full z-50 {{ $maxWidth }}"
63+
x-transition:enter="ease-out duration-300"
64+
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
65+
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
66+
x-transition:leave="ease-in duration-200"
67+
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
68+
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
69+
<iframe class="w-full" style="height:500px;" src="{{ route('opayo.threed.iframe', [
70+
'acsUrl' => $threeDSecure['acsUrl'],
71+
'cReq' => $threeDSecure['cReq'],
72+
]) }}"></iframe>
73+
</div>
74+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta http-equiv="X-UA-Compatible" content="ie=edge">
7+
<title>Document</title>
8+
</head>
9+
<body>
10+
<form id="pa-form" method="POST" action="{{ app()->request->acsUrl }}">
11+
@if(app()->request->paReq)
12+
<input type="hidden" name="PaReq" value="{{ str_replace(' ', '+', app()->request->paReq) }}">
13+
@else
14+
<input type="hidden" name="creq" value="{{ app()->request->cReq }}" />
15+
@endif
16+
<input type="hidden" name="TermUrl" value="{{ route('opayo.threed.response') }}">
17+
<input type="hidden" name="MD" value="{{ Str::random(30) }}">
18+
</form>
19+
<script>document.addEventListener("DOMContentLoaded",function(){var b=document.getElementById("pa-form");b&&b.submit()})</script>
20+
</body>
21+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta http-equiv="X-UA-Compatible" content="ie=edge">
7+
<title>Document</title>
8+
</head>
9+
<body>
10+
<script>
11+
(function () {
12+
if ( typeof window.CustomEvent === "function" ) return false;
13+
function CustomEvent ( event, params ) {
14+
params = params || { bubbles: false, cancelable: false, detail: undefined };
15+
var evt = document.createEvent('CustomEvent');
16+
evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
17+
return evt;
18+
}
19+
CustomEvent.prototype = window.Event.prototype;
20+
window.CustomEvent = CustomEvent;
21+
})();
22+
23+
var myEvent = new CustomEvent('opayo_threed_secure_response', {
24+
detail: {
25+
@if($PaRes ?? false)
26+
PaRes: "{{ $PaRes }}",
27+
@endif
28+
@if($cres ?? false)
29+
cres: "{{ $cres }}",
30+
@endif
31+
@if($md ?? false)
32+
MD: "{{ $MD }}",
33+
@endif
34+
MDX: @if (!empty($mdx)) "{{ $MDX }}" @else null @endif
35+
}
36+
})
37+
window.parent.dispatchEvent(myEvent)
38+
</script>
39+
</body>
40+
</html>

routes/web.php

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
use Illuminate\Http\Request;
4+
5+
Route::get('opayo-threedsecure', function () {
6+
return view('getcandy::opayo.threed-secure-iframe');
7+
})->name('opayo.threed.iframe');
8+
9+
10+
Route::post('opayo-threedsecure-response', function (Request $request) {
11+
return view('getcandy::opayo.threed-secure-response', [
12+
'cres' => $request->cres,
13+
'PaRes' => $request->PaRes,
14+
'md' => $request->md,
15+
'mdx' => $request->mdx,
16+
]);
17+
})->name('opayo.threed.response');

0 commit comments

Comments
 (0)