@danielseres9/adobe-commerce-checkout 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danielseres9/adobe-commerce-checkout",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Worldpay integration package for Adobe Commerce Checkout Drop-ins",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -1,10 +1,37 @@
1
- import { WORLDPAY_PAYMENT_METHOD } from './constants.js';
1
+ import {
2
+ WORLDPAY_DIRECT_PAYMENT_METHOD,
3
+ WORLDPAY_PAYMENT_METHOD,
4
+ } from './constants.js';
2
5
 
3
6
  export function getWorldpayMethodConfig(method) {
4
7
  return method?.additionalData?.oope_payment_method_config
5
8
  ?? method?.oope_payment_method_config;
6
9
  }
7
10
 
11
+ export function getWorldpayCustomConfig(methodOrAdditionalData) {
12
+ const config = methodOrAdditionalData?.additionalData?.oope_payment_method_config
13
+ ?? methodOrAdditionalData?.oope_payment_method_config
14
+ ?? methodOrAdditionalData;
15
+ const entries = config?.custom_config ?? [];
16
+
17
+ if (!Array.isArray(entries)) return {};
18
+
19
+ return Object.fromEntries(
20
+ entries
21
+ .filter(({ key, value }) => key && value != null)
22
+ .map(({ key, value }) => [key, value]),
23
+ );
24
+ }
25
+
26
+ export function getWorldpayDirectConfig(methodOrAdditionalData) {
27
+ const customConfig = getWorldpayCustomConfig(methodOrAdditionalData);
28
+
29
+ return {
30
+ checkoutId: customConfig.checkout_id ?? customConfig.checkoutId,
31
+ environment: customConfig.environment ?? 'try',
32
+ };
33
+ }
34
+
8
35
  export function getWorldpayBackendUrl(cartData, paymentMethodCode) {
9
36
  const selectedMethod = cartData?.selectedPaymentMethod;
10
37
  const availableMethod = cartData?.availablePaymentMethods?.find(
@@ -67,6 +94,32 @@ export function buildWorldpayPaymentPayload({
67
94
  };
68
95
  }
69
96
 
97
+ export function buildWorldpayDirectPaymentPayload({
98
+ orderData,
99
+ returnUrl,
100
+ appBuilderUrl,
101
+ merchantConfigId = getMerchantConfigId(appBuilderUrl),
102
+ paymentMethod = WORLDPAY_DIRECT_PAYMENT_METHOD,
103
+ sessionState,
104
+ }) {
105
+ const payload = buildWorldpayPaymentPayload({
106
+ orderData,
107
+ returnUrl,
108
+ appBuilderUrl,
109
+ merchantConfigId,
110
+ paymentMethod,
111
+ });
112
+
113
+ if (!sessionState) {
114
+ throw new Error('Worldpay direct session state missing.');
115
+ }
116
+
117
+ return {
118
+ ...payload,
119
+ sessionState,
120
+ };
121
+ }
122
+
70
123
  export async function createWorldpayPayment({
71
124
  appBuilderUrl,
72
125
  orderData,
@@ -113,3 +166,42 @@ export async function createWorldpayPayment({
113
166
  data,
114
167
  };
115
168
  }
169
+
170
+ export async function createWorldpayDirectPayment({
171
+ appBuilderUrl,
172
+ orderData,
173
+ returnUrl,
174
+ merchantConfigId,
175
+ paymentMethod = WORLDPAY_DIRECT_PAYMENT_METHOD,
176
+ sessionState,
177
+ fetchImpl = getDefaultFetch(),
178
+ }) {
179
+ if (!appBuilderUrl) {
180
+ throw new Error('Worldpay backend integration URL missing.');
181
+ }
182
+
183
+ if (typeof fetchImpl !== 'function') {
184
+ throw new Error('Worldpay fetch implementation missing.');
185
+ }
186
+
187
+ const response = await fetchImpl(appBuilderUrl, {
188
+ method: 'POST',
189
+ headers: {
190
+ 'Content-Type': 'application/json',
191
+ },
192
+ body: JSON.stringify(buildWorldpayDirectPaymentPayload({
193
+ orderData,
194
+ returnUrl,
195
+ appBuilderUrl,
196
+ merchantConfigId,
197
+ paymentMethod,
198
+ sessionState,
199
+ })),
200
+ });
201
+
202
+ if (!response.ok) {
203
+ throw new Error('Worldpay direct payment creation failed.');
204
+ }
205
+
206
+ return response.json();
207
+ }
package/src/checkout.js CHANGED
@@ -1,10 +1,24 @@
1
- import { WORLDPAY_PAYMENT_METHOD, isWorldpayPaymentMethod } from './constants.js';
2
1
  import {
2
+ WORLDPAY_DIRECT_PAYMENT_METHOD,
3
+ WORLDPAY_PAYMENT_METHOD,
4
+ isWorldpayDirectPaymentMethod,
5
+ isWorldpayPaymentMethod,
6
+ } from './constants.js';
7
+ import {
8
+ createWorldpayDirectPayment,
3
9
  createWorldpayPayment,
4
10
  getAbsoluteUrl,
11
+ getWorldpayDirectConfig,
5
12
  getWorldpayBackendUrl,
6
13
  } from './app-builder-client.js';
7
14
 
15
+ const WORLDPAY_CHECKOUT_SDK_URLS = {
16
+ try: 'https://try.access.worldpay.com/access-checkout/v2/checkout.js',
17
+ production: 'https://access.worldpay.com/access-checkout/v2/checkout.js',
18
+ };
19
+
20
+ let worldpayCheckoutSdkPromise;
21
+
8
22
  function getDefaultLocation() {
9
23
  return typeof window !== 'undefined' ? window.location : undefined;
10
24
  }
@@ -13,6 +27,317 @@ function getDefaultOrigin() {
13
27
  return typeof window !== 'undefined' ? window.location?.origin : undefined;
14
28
  }
15
29
 
30
+ function getDefaultDocument() {
31
+ return typeof document !== 'undefined' ? document : undefined;
32
+ }
33
+
34
+ function getDefaultWindow() {
35
+ return typeof window !== 'undefined' ? window : undefined;
36
+ }
37
+
38
+ function afterNextFrame(callback, windowRef = getDefaultWindow()) {
39
+ if (windowRef?.requestAnimationFrame) {
40
+ windowRef.requestAnimationFrame(callback);
41
+ return;
42
+ }
43
+
44
+ setTimeout(callback, 0);
45
+ }
46
+
47
+ function getWorldpayCheckoutSdkUrl(environment = 'try') {
48
+ return WORLDPAY_CHECKOUT_SDK_URLS[environment] ?? WORLDPAY_CHECKOUT_SDK_URLS.try;
49
+ }
50
+
51
+ function loadWorldpayCheckoutSdk({
52
+ environment = 'try',
53
+ documentRef = getDefaultDocument(),
54
+ windowRef = getDefaultWindow(),
55
+ } = {}) {
56
+ if (!documentRef || !windowRef) {
57
+ return Promise.reject(new Error('Worldpay Checkout SDK requires a browser document.'));
58
+ }
59
+
60
+ if (windowRef.Worldpay?.checkout?.init) {
61
+ return Promise.resolve(windowRef.Worldpay);
62
+ }
63
+
64
+ if (!worldpayCheckoutSdkPromise) {
65
+ worldpayCheckoutSdkPromise = new Promise((resolve, reject) => {
66
+ const script = documentRef.createElement('script');
67
+ script.src = getWorldpayCheckoutSdkUrl(environment);
68
+ script.async = true;
69
+ script.onload = () => {
70
+ if (windowRef.Worldpay?.checkout?.init) {
71
+ resolve(windowRef.Worldpay);
72
+ } else {
73
+ reject(new Error('Worldpay Checkout SDK loaded without checkout API.'));
74
+ }
75
+ };
76
+ script.onerror = () => reject(new Error('Worldpay Checkout SDK failed to load.'));
77
+ documentRef.head.appendChild(script);
78
+ });
79
+ }
80
+
81
+ return worldpayCheckoutSdkPromise;
82
+ }
83
+
84
+ function toIdSuffix() {
85
+ return Math.random().toString(36).slice(2, 10);
86
+ }
87
+
88
+ function createWorldpayDirectForm(documentRef = getDefaultDocument()) {
89
+ const suffix = toIdSuffix();
90
+ const form = documentRef.createElement('form');
91
+ form.className = 'worldpay-direct-form';
92
+ form.id = `worldpay-direct-form-${suffix}`;
93
+ form.noValidate = true;
94
+ form.addEventListener('submit', (event) => event.preventDefault());
95
+
96
+ form.innerHTML = `
97
+ <div class="worldpay-direct-header">
98
+ <span class="worldpay-direct-title">Card details</span>
99
+ <span class="worldpay-direct-secure">Secured by Worldpay</span>
100
+ </div>
101
+ <div class="worldpay-direct-field-group">
102
+ <label class="worldpay-direct-label" for="worldpay-direct-pan-${suffix}">Card number</label>
103
+ <section id="worldpay-direct-pan-${suffix}" class="worldpay-direct-field"></section>
104
+ </div>
105
+ <div class="worldpay-direct-row">
106
+ <div class="worldpay-direct-field-group">
107
+ <label class="worldpay-direct-label" for="worldpay-direct-expiry-${suffix}">Expiry date</label>
108
+ <section id="worldpay-direct-expiry-${suffix}" class="worldpay-direct-field"></section>
109
+ </div>
110
+ <div class="worldpay-direct-field-group">
111
+ <label class="worldpay-direct-label" for="worldpay-direct-cvv-${suffix}">Security code</label>
112
+ <section id="worldpay-direct-cvv-${suffix}" class="worldpay-direct-field"></section>
113
+ </div>
114
+ </div>
115
+ <p class="worldpay-direct-error" hidden></p>
116
+ `;
117
+
118
+ const style = documentRef.createElement('style');
119
+ style.textContent = `
120
+ .worldpay-direct-form {
121
+ background: #f7f9fb;
122
+ border: 1px solid #d8dee8;
123
+ border-radius: 8px;
124
+ box-sizing: border-box;
125
+ display: grid;
126
+ gap: 12px;
127
+ max-width: 520px;
128
+ padding: 16px;
129
+ }
130
+
131
+ .worldpay-direct-header {
132
+ align-items: baseline;
133
+ display: flex;
134
+ flex-wrap: wrap;
135
+ gap: 8px 12px;
136
+ justify-content: space-between;
137
+ margin-bottom: 2px;
138
+ }
139
+
140
+ .worldpay-direct-title {
141
+ color: #111827;
142
+ font-size: 16px;
143
+ font-weight: 700;
144
+ }
145
+
146
+ .worldpay-direct-secure {
147
+ color: #5f6f86;
148
+ font-size: 12px;
149
+ font-weight: 600;
150
+ }
151
+
152
+ .worldpay-direct-row {
153
+ display: grid;
154
+ gap: 12px;
155
+ grid-template-columns: 1fr;
156
+ }
157
+
158
+ .worldpay-direct-label {
159
+ display: block;
160
+ font-size: 14px;
161
+ font-weight: 600;
162
+ margin-bottom: 6px;
163
+ }
164
+
165
+ .worldpay-direct-field {
166
+ background: #fff;
167
+ border: 1px solid #aeb8c7;
168
+ border-radius: 6px;
169
+ box-sizing: border-box;
170
+ height: 46px;
171
+ overflow: hidden;
172
+ padding: 0 12px;
173
+ transition: border-color 120ms ease, box-shadow 120ms ease, background-color 120ms ease;
174
+ }
175
+
176
+ .worldpay-direct-field iframe {
177
+ border: 0;
178
+ display: block;
179
+ height: 100%;
180
+ width: 100%;
181
+ }
182
+
183
+ .worldpay-direct-field.is-onfocus {
184
+ background: #fff;
185
+ border-color: #0f62fe;
186
+ box-shadow: 0 0 0 2px rgba(15, 98, 254, 0.16);
187
+ }
188
+
189
+ .worldpay-direct-field.is-valid {
190
+ border-color: #198754;
191
+ }
192
+
193
+ .worldpay-direct-field.is-invalid {
194
+ border-color: #b42318;
195
+ box-shadow: 0 0 0 2px rgba(180, 35, 24, 0.12);
196
+ }
197
+
198
+ .worldpay-direct-error {
199
+ color: #b42318;
200
+ font-size: 14px;
201
+ margin: 0;
202
+ }
203
+
204
+ @media (min-width: 600px) {
205
+ .worldpay-direct-row {
206
+ grid-template-columns: 1fr 1fr;
207
+ }
208
+ }
209
+ `;
210
+ form.prepend(style);
211
+
212
+ return {
213
+ form,
214
+ selectors: {
215
+ form: `#${form.id}`,
216
+ pan: `#worldpay-direct-pan-${suffix}`,
217
+ expiry: `#worldpay-direct-expiry-${suffix}`,
218
+ cvv: `#worldpay-direct-cvv-${suffix}`,
219
+ },
220
+ error: form.querySelector('.worldpay-direct-error'),
221
+ };
222
+ }
223
+
224
+ function showWorldpayDirectError(errorElement, error) {
225
+ if (!errorElement) return;
226
+
227
+ errorElement.textContent = error?.message ?? 'Worldpay card fields are unavailable.';
228
+ errorElement.hidden = false;
229
+ }
230
+
231
+ function initializeWorldpayDirectCheckout({
232
+ additionalData,
233
+ formRef,
234
+ elements,
235
+ documentRef = getDefaultDocument(),
236
+ windowRef = getDefaultWindow(),
237
+ }) {
238
+ const { checkoutId, environment } = getWorldpayDirectConfig(additionalData);
239
+ const { form, selectors, error } = elements ?? createWorldpayDirectForm(documentRef);
240
+
241
+ if (!checkoutId) {
242
+ showWorldpayDirectError(error, new Error('Worldpay checkout id is missing.'));
243
+ return { form };
244
+ }
245
+
246
+ const checkoutPromise = loadWorldpayCheckoutSdk({ environment, documentRef, windowRef })
247
+ .then((Worldpay) => new Promise((resolve, reject) => {
248
+ Worldpay.checkout.init(
249
+ {
250
+ id: checkoutId,
251
+ form: selectors.form,
252
+ fields: {
253
+ pan: {
254
+ selector: selectors.pan,
255
+ placeholder: '4444 3333 2222 1111',
256
+ },
257
+ expiry: {
258
+ selector: selectors.expiry,
259
+ placeholder: 'MM/YY',
260
+ },
261
+ cvv: {
262
+ selector: selectors.cvv,
263
+ placeholder: '123',
264
+ },
265
+ },
266
+ accessibility: {
267
+ ariaLabel: {
268
+ pan: 'Card number',
269
+ expiry: 'Expiry date',
270
+ cvv: 'Security code',
271
+ },
272
+ lang: {
273
+ locale: 'en-GB',
274
+ },
275
+ },
276
+ enablePanFormatting: true,
277
+ styles: {
278
+ input: {
279
+ color: '#111827',
280
+ 'font-family': 'Arial, Helvetica, sans-serif',
281
+ 'font-size': '16px',
282
+ 'font-weight': '400',
283
+ 'letter-spacing': '0',
284
+ 'line-height': '20px',
285
+ },
286
+ 'input#pan': {
287
+ 'letter-spacing': '1px',
288
+ },
289
+ 'input.is-onfocus': {
290
+ color: '#111827',
291
+ },
292
+ 'input.is-invalid': {
293
+ color: '#b42318',
294
+ },
295
+ },
296
+ },
297
+ (initError, checkout) => {
298
+ if (initError) {
299
+ reject(initError);
300
+ return;
301
+ }
302
+ resolve(checkout);
303
+ },
304
+ );
305
+ }))
306
+ .catch((sdkError) => {
307
+ showWorldpayDirectError(error, sdkError);
308
+ throw sdkError;
309
+ });
310
+
311
+ if (formRef) {
312
+ formRef.current = {
313
+ clear: async () => {
314
+ const checkout = await checkoutPromise;
315
+ return new Promise((resolve) => {
316
+ checkout.clearForm(resolve);
317
+ });
318
+ },
319
+ generateSessionState: async () => {
320
+ const checkout = await checkoutPromise;
321
+ return new Promise((resolve, reject) => {
322
+ checkout.generateSessionState((sessionError, sessionState) => {
323
+ if (sessionError) {
324
+ reject(sessionError);
325
+ return;
326
+ }
327
+ resolve(sessionState);
328
+ });
329
+ });
330
+ },
331
+ remove: async () => {
332
+ const checkout = await checkoutPromise;
333
+ checkout.remove?.();
334
+ },
335
+ };
336
+ }
337
+
338
+ return { form };
339
+ }
340
+
16
341
  export async function startWorldpayPayment({
17
342
  cartData,
18
343
  orderData,
@@ -78,3 +403,87 @@ export async function handleWorldpayPlaceOrder({
78
403
 
79
404
  return true;
80
405
  }
406
+
407
+ export function renderWorldpayDirectFields(ctx, { formRef } = {}) {
408
+ const elements = createWorldpayDirectForm();
409
+
410
+ ctx.replaceHTML(elements.form);
411
+
412
+ afterNextFrame(() => {
413
+ initializeWorldpayDirectCheckout({
414
+ additionalData: ctx.additionalData,
415
+ formRef,
416
+ elements,
417
+ });
418
+ });
419
+ }
420
+
421
+ export async function startWorldpayDirectPayment({
422
+ cartData,
423
+ orderData,
424
+ returnUrl,
425
+ sessionState,
426
+ paymentMethodCode = WORLDPAY_DIRECT_PAYMENT_METHOD,
427
+ merchantConfigId,
428
+ fetchImpl,
429
+ }) {
430
+ const appBuilderUrl = getWorldpayBackendUrl(cartData, paymentMethodCode);
431
+
432
+ return createWorldpayDirectPayment({
433
+ appBuilderUrl,
434
+ orderData,
435
+ returnUrl,
436
+ merchantConfigId,
437
+ paymentMethod: paymentMethodCode,
438
+ sessionState,
439
+ fetchImpl,
440
+ });
441
+ }
442
+
443
+ export async function handleWorldpayDirectPlaceOrder({
444
+ cartId,
445
+ code,
446
+ cartData,
447
+ buildOrderUrl,
448
+ placeOrder,
449
+ formRef,
450
+ merchantConfigId,
451
+ fetchImpl,
452
+ locationRef = getDefaultLocation(),
453
+ origin = getDefaultOrigin(),
454
+ }) {
455
+ if (!isWorldpayDirectPaymentMethod(code)) return false;
456
+
457
+ if (typeof buildOrderUrl !== 'function') {
458
+ throw new Error('Worldpay order URL builder missing.');
459
+ }
460
+
461
+ if (typeof placeOrder !== 'function') {
462
+ throw new Error('Worldpay place order function missing.');
463
+ }
464
+
465
+ if (typeof formRef?.current?.generateSessionState !== 'function') {
466
+ throw new Error('Worldpay direct card fields are not ready.');
467
+ }
468
+
469
+ const sessionState = await formRef.current.generateSessionState();
470
+ const orderData = await placeOrder(cartId);
471
+ const returnUrl = getAbsoluteUrl(buildOrderUrl(orderData), origin);
472
+ const data = await startWorldpayDirectPayment({
473
+ cartData,
474
+ orderData,
475
+ returnUrl,
476
+ sessionState,
477
+ paymentMethodCode: code,
478
+ merchantConfigId,
479
+ fetchImpl,
480
+ });
481
+
482
+ const redirectUrl = data.redirectUrl ?? data.redirect_url ?? returnUrl;
483
+
484
+ if (locationRef && redirectUrl) {
485
+ locationRef.href = redirectUrl;
486
+ }
487
+
488
+ return true;
489
+ }
package/src/constants.js CHANGED
@@ -1,7 +1,14 @@
1
1
  export const WORLDPAY_PAYMENT_METHOD = 'Worldpay';
2
2
  export const WORLDPAY_LOCAL_PAYMENT_METHOD_PREFIX = 'WorldpayLocal';
3
+ export const WORLDPAY_DIRECT_PAYMENT_METHOD = 'WorldpayDirect';
4
+ export const WORLDPAY_DIRECT_LOCAL_PAYMENT_METHOD_PREFIX = 'WorldpayDirectLocal';
3
5
 
4
6
  export function isWorldpayPaymentMethod(code) {
5
7
  return code === WORLDPAY_PAYMENT_METHOD
6
8
  || String(code ?? '').startsWith(WORLDPAY_LOCAL_PAYMENT_METHOD_PREFIX);
7
9
  }
10
+
11
+ export function isWorldpayDirectPaymentMethod(code) {
12
+ return code === WORLDPAY_DIRECT_PAYMENT_METHOD
13
+ || String(code ?? '').startsWith(WORLDPAY_DIRECT_LOCAL_PAYMENT_METHOD_PREFIX);
14
+ }
package/src/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  export {
2
+ WORLDPAY_DIRECT_LOCAL_PAYMENT_METHOD_PREFIX,
3
+ WORLDPAY_DIRECT_PAYMENT_METHOD,
2
4
  WORLDPAY_LOCAL_PAYMENT_METHOD_PREFIX,
3
5
  WORLDPAY_PAYMENT_METHOD,
6
+ isWorldpayDirectPaymentMethod,
4
7
  isWorldpayPaymentMethod,
5
8
  } from './constants.js';
6
9
  export { worldpayCheckoutGqlOperations } from './gql.js';
@@ -10,15 +13,22 @@ export {
10
13
  worldpayOrderDataModelTransformer,
11
14
  } from './transformers.js';
12
15
  export {
16
+ buildWorldpayDirectPaymentPayload,
13
17
  buildWorldpayPaymentPayload,
18
+ createWorldpayDirectPayment,
14
19
  createWorldpayPayment,
15
20
  getAbsoluteUrl,
16
21
  getMerchantConfigId,
17
22
  getOrderEntityId,
23
+ getWorldpayCustomConfig,
18
24
  getWorldpayBackendUrl,
25
+ getWorldpayDirectConfig,
19
26
  getWorldpayMethodConfig,
20
27
  } from './app-builder-client.js';
21
28
  export {
29
+ handleWorldpayDirectPlaceOrder,
22
30
  handleWorldpayPlaceOrder,
31
+ renderWorldpayDirectFields,
32
+ startWorldpayDirectPayment,
23
33
  startWorldpayPayment,
24
34
  } from './checkout.js';