@akinon/pz-masterpass 2.0.0-beta.2 → 2.0.0-beta.20

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/CHANGELOG.md CHANGED
@@ -1,18 +1,136 @@
1
1
  # @akinon/pz-masterpass
2
2
 
3
- ## 2.0.0-beta.2
3
+ ## 2.0.0-beta.20
4
4
 
5
- ## 2.0.0-beta.1
5
+ ## 1.125.0
6
+
7
+ ## 1.124.0
8
+
9
+ ## 1.123.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 572f4f7b: ZERO-3959: Add binNumberConfig prop to MasterpassCardList for customizable BIN extraction
14
+ - 88cb4f62: ZERO-3959: Update onSelectCard to handle masked BIN extraction based on card value format
15
+
16
+ ## 1.122.0
17
+
18
+ ## 1.121.0
19
+
20
+ ## 1.120.0
21
+
22
+ ### Minor Changes
23
+
24
+ - 6ad72e8d: ZERO-4032: Add loading state management for payment submissions across multiple components and add safe guarding
25
+
26
+ ## 1.119.0
27
+
28
+ ## 1.118.0
29
+
30
+ ## 1.117.0
31
+
32
+ ## 1.116.0
33
+
34
+ ## 1.115.0
35
+
36
+ ## 1.114.0
37
+
38
+ ## 1.113.0
39
+
40
+ ## 1.112.0
41
+
42
+ ## 1.111.0
43
+
44
+ ## 1.110.0
45
+
46
+ ## 1.109.0
47
+
48
+ ## 1.108.0
49
+
50
+ ## 1.107.0
51
+
52
+ ## 1.106.0
53
+
54
+ ## 1.105.0
55
+
56
+ ## 1.104.0
57
+
58
+ ## 1.103.0
59
+
60
+ ### Minor Changes
61
+
62
+ - ad0bec9: ZERO-3364 :Update readme file for masterpass
63
+ - b31333e: ZERO-3357 :Add components with renderer properties for customizable UI elements in pz-masterpass
64
+
65
+ ## 1.102.0
66
+
67
+ ## 1.101.0
68
+
69
+ ## 1.100.0
70
+
71
+ ### Minor Changes
72
+
73
+ - e57cd93: ZERO-3460: prevent installment loop on payment method switch
74
+
75
+ ## 1.99.0
76
+
77
+ ### Minor Changes
78
+
79
+ - d58538b: ZERO-3638: Enhance RC pipeline: add fetch, merge, and pre-release setup with conditional commit
80
+
81
+ ## 1.98.0
82
+
83
+ ## 1.97.0
84
+
85
+ ## 1.96.0
86
+
87
+ ## 1.95.0
88
+
89
+ ## 1.94.0
90
+
91
+ ## 1.93.0
92
+
93
+ ## 1.92.0
94
+
95
+ ## 1.91.0
96
+
97
+ ## 1.90.0
98
+
99
+ ## 1.89.0
100
+
101
+ ## 1.88.0
102
+
103
+ ## 1.87.0
104
+
105
+ ## 1.86.0
106
+
107
+ ## 1.85.0
108
+
109
+ ## 1.84.0
6
110
 
7
111
  ### Minor Changes
8
112
 
9
- - ZERO-3091: Upgrade Next.js to v15 and React to v19
113
+ - 624a4eb: ZERO-3276: Update installation instructions across multiple README files to standardize format and improve clarity
114
+
115
+ ## 1.83.0
116
+
117
+ ## 1.82.0
118
+
119
+ ## 1.81.0
120
+
121
+ ### Minor Changes
122
+
123
+ - fdbf156: ZERO-3137: Trim whitespace from account alias name input in card registration form
124
+
125
+ ## 1.80.0
126
+
127
+ ## 1.79.0
10
128
 
11
- ## 2.0.0-beta.0
129
+ ## 1.78.0
12
130
 
13
- ### Major Changes
131
+ ## 1.77.0
14
132
 
15
- - be6c09d: ZERO-3114: Create beta version.
133
+ ## 1.76.0
16
134
 
17
135
  ## 1.75.0
18
136
 
package/README.md CHANGED
@@ -1,21 +1,25 @@
1
1
  # @akinon/pz-masterpass
2
2
 
3
- ### Install the npm package
3
+ ## Installation method
4
+
5
+ You can use the following command to install the extension with the latest plugins:
4
6
 
5
7
  ```bash
6
- # For latest version
7
- yarn add @akinon/pz-masterpass
8
8
 
9
- # Preferred installation method
10
9
  npx @akinon/projectzero@latest --plugins
10
+
11
11
  ```
12
12
 
13
13
  ## Available Props
14
14
 
15
- ### Masterpass Provider
15
+ ## Masterpass Provider
16
16
 
17
17
  ##### File Path: src/app/[commerce]/[locale]/[currency]/orders/checkout/page.tsx
18
18
 
19
+ ### Usage Examples
20
+
21
+ ##### Default Usage
22
+
19
23
  ```javascript
20
24
  <PluginModule
21
25
  component={Component.MasterpassProvider}
@@ -29,7 +33,7 @@ npx @akinon/projectzero@latest --plugins
29
33
  </PluginModule>
30
34
  ```
31
35
 
32
- ### Additional Params
36
+ ##### Customized Usage with Additional Params
33
37
 
34
38
  ##### To add extra parameters, it can be added by passing additional Params property to the Masterpass Provider.
35
39
 
@@ -51,8 +55,30 @@ npx @akinon/projectzero@latest --plugins
51
55
 
52
56
  ### Delete Confirmation Modal
53
57
 
58
+ #### Props
59
+
60
+ | Props | Type | Required | Description |
61
+ | --- | --- | --- | --- |
62
+ | translations | typeof defaultTranslations | Optional | Used to customize the default texts. |
63
+ | renderer | RendererProps | Optional | Renderer functions that can be used to customize all subcomponents. |
64
+
65
+ #### RendererProps
66
+
67
+ | Props | Type | Description |
68
+ | --- | --- | --- |
69
+ | open | boolean | It is information whether the modal is open or not. |
70
+ | setOpen | (open: boolean) => void | Used to manage modal on/off operation. |
71
+ | onConfirm | () => void | This is the function that will perform the deletion operation. |
72
+ | onCancel | () => void | This is the function that will be run if the user cancels the deletion process. |
73
+ | loading | boolean | Indicates the loading status during the deletion process. |
74
+ | error | string / null | Error message to be displayed if an error occurs during deletion. |
75
+
54
76
  ##### File Path: src/app/[commerce]/[locale]/[currency]/orders/checkout/page.tsx
55
77
 
78
+ ### Usage Examples
79
+
80
+ ##### Default Usage
81
+
56
82
  ```javascript
57
83
  <PluginModule
58
84
  component={Component.MasterpassDeleteConfirmationModal}
@@ -66,10 +92,70 @@ npx @akinon/projectzero@latest --plugins
66
92
  />
67
93
  ```
68
94
 
95
+ ##### Customized Usage with Renderer
96
+
97
+ ```javascript
98
+ <PluginModule
99
+ component={Component.MasterpassDeleteConfirmationModal}
100
+ props={{
101
+ renderer: {
102
+ Content: ({ open, setOpen, onConfirm, onCancel, loading, error }) => (
103
+ <Modal
104
+ open={open}
105
+ setOpen={setOpen}
106
+ className="w-full sm:w-[28rem]"
107
+ portalId="masterpass-remove-card-modal"
108
+ >
109
+ <div className="flex flex-col items-center p-5">
110
+ <div className="text-xs mb-3">
111
+ Are you sure you want to delete your card?
112
+ </div>
113
+ <Button onClick={onConfirm} className="bg-red-600 text-black">
114
+ {loading ? 'Deletion...' : 'Delete'}
115
+ </Button>
116
+ <Button appearance="outlined" onClick={onCancel}>
117
+ Cancel
118
+ </Button>
119
+ {error && (
120
+ <p className="text-red-500 text-sm mt-2 text-center">
121
+ 🚨 {error}
122
+ </p>
123
+ )}
124
+ </div>
125
+ </Modal>
126
+ )
127
+ }
128
+ }}
129
+ />
130
+ ```
131
+
69
132
  ### OTP Modal
70
133
 
134
+ #### Props
135
+
136
+ | Props | Type | Required | Description |
137
+ | --- | --- | --- | --- |
138
+ | translations | { [key: string]: string } | Optional | Translation object to customize all texts. Overrides default texts. |
139
+ | renderer | RendererProps | Optional | Renderer functions that can be used to customize all subcomponents. |
140
+
141
+ #### RendererProps
142
+
143
+ | Props | Type | Description |
144
+ | --- | --- | --- |
145
+ | open | boolean | It is information whether the modal is open or not. |
146
+ | setOpen | (open: boolean) => void | Used to manage modal on/off operation. |
147
+ | onSubmit | (data: { otp_code: string }) => void | Sends the OTP code entered by the user. |
148
+ | loading | boolean | Indicates the loading status during validation. |
149
+ | error | string / null | Error message to be displayed if there is an error. |
150
+ | resendSms | () => void | Function triggered when the user requests SMS again. |
151
+ | resendSmsFetching | boolean | Indicates the loading status during the resending process. |
152
+ | targetDate | number | When the countdown will end (as a timestamp). |
153
+ | otpRef | string / null | Transaction reference number, if applicable. |
154
+
71
155
  ##### File Path: src/app/[commerce]/[locale]/[currency]/orders/checkout/page.tsx
72
156
 
157
+ ##### Default Usage
158
+
73
159
  ```javascript
74
160
  <PluginModule
75
161
  component={Component.MasterpassOtpModal}
@@ -86,10 +172,127 @@ npx @akinon/projectzero@latest --plugins
86
172
  />
87
173
  ```
88
174
 
175
+ ##### Customized Usage with Renderer
176
+
177
+ ```javascript
178
+ <PluginModule
179
+ component={Component.MasterpassOtpModal}
180
+ props={{
181
+ renderer: {
182
+ Content: ({
183
+ open,
184
+ setOpen,
185
+ onSubmit,
186
+ loading,
187
+ error,
188
+ resendSms,
189
+ resendSmsFetching,
190
+ targetDate,
191
+ otpRef
192
+ }) => {
193
+ const [code, setCode] = useState('');
194
+ const [remainingSeconds, setRemainingSeconds] = useState(
195
+ Math.ceil((targetDate - Date.now()) / 1000)
196
+ );
197
+ useEffect(() => {
198
+ if (!targetDate) return;
199
+ const interval = setInterval(() => {
200
+ const secondsLeft = Math.ceil((targetDate - Date.now()) / 1000);
201
+ setRemainingSeconds(secondsLeft);
202
+ if (secondsLeft <= 0) {
203
+ clearInterval(interval);
204
+ }
205
+ }, 1000);
206
+ return () => clearInterval(interval);
207
+ }, [targetDate]);
208
+ return (
209
+ <Modal
210
+ open={open}
211
+ setOpen={setOpen}
212
+ className="w-full sm:w-[28rem]"
213
+ portalId="otp-masterpass"
214
+ >
215
+ <div className="px-6 py-4">
216
+ <h2 className="text-center text-xl text-primary-600 font-semibold">
217
+ Verification Required
218
+ </h2>
219
+ <form
220
+ onSubmit={(e) => {
221
+ e.preventDefault();
222
+ onSubmit({ otp_code: code });
223
+ }}
224
+ className="flex flex-col items-center mt-4"
225
+ >
226
+ <label
227
+ htmlFor="otp_code"
228
+ className="text-sm mb-1 text-gray-700"
229
+ >
230
+ SMS Code
231
+ </label>
232
+ <input
233
+ id="otp_code"
234
+ maxLength={6}
235
+ value={code}
236
+ onChange={(e) => setCode(e.target.value)}
237
+ placeholder="••••••"
238
+ className="border p-2 rounded w-full max-w-[200px] text-center tracking-widest"
239
+ />
240
+ <Button
241
+ type="submit"
242
+ className="mt-4 px-4 py-2 bg-blue-600 text-black rounded"
243
+ >
244
+ Verify
245
+ </Button>
246
+ {error && <p className="mt-2 text-xs text-red-500">{error}</p>}
247
+ </form>
248
+ <div className="mt-2 flex justify-center text-sm text-gray-600">
249
+ {remainingSeconds > 0
250
+ ? `Time remaining: ${remainingSeconds} seconds`
251
+ : 'Time is up'}
252
+ </div>
253
+ <div className="mt-2 flex justify-center">
254
+ <Button
255
+ onClick={resendSms}
256
+ disabled={loading}
257
+ className="mt-4 text-sm underline text-secondary-500"
258
+ >
259
+ {resendSmsFetching ? 'Code sent' : 'Resend SMS code'}
260
+ </Button>
261
+ </div>
262
+ </div>
263
+ </Modal>
264
+ );
265
+ }
266
+ }
267
+ }}
268
+ />
269
+ ```
270
+
89
271
  ### Link Modal
90
272
 
273
+ #### Props
274
+
275
+ | Props | Type | Required | Description |
276
+ | --- | --- | --- | --- |
277
+ | translations | { [key: string]: string } | Optional | Translation object to customize all texts. Overrides default texts. |
278
+ | renderer | RendererProps | Optional | Renderer functions that can be used to customize all subcomponents. |
279
+
280
+ #### RendererProps
281
+
282
+ | Props | Type | Description |
283
+ | --- | --- | --- |
284
+ | open | boolean | It is information whether the modal is open or not. |
285
+ | setOpen | (open: boolean) => void | Used to manage modal on/off operation. |
286
+ | onClick | () => void | It is the function that is called when the user clicks the "Use" button. |
287
+ | loading | boolean | This is the loading status that will be displayed during the API request. |
288
+ | error | string | If there is an error in the API response, a message is displayed in this field. |
289
+
91
290
  ##### File Path: src/app/[commerce]/[locale]/[currency]/orders/checkout/page.tsx
92
291
 
292
+ ### Usage Examples
293
+
294
+ ##### Default Usage
295
+
93
296
  ```javascript
94
297
  <PluginModule
95
298
  component={Component.MasterpassLinkModal}
@@ -103,10 +306,67 @@ npx @akinon/projectzero@latest --plugins
103
306
  />
104
307
  ```
105
308
 
309
+ ##### Customized Usage with Renderer
310
+
311
+ ```javascript
312
+ <PluginModule
313
+ component={Component.MasterpassLinkModal}
314
+ props={{
315
+ renderer: {
316
+ Content: ({ open, setOpen, onClick, loading, error }) => (
317
+ <Modal
318
+ open={open}
319
+ setOpen={setOpen}
320
+ className="w-full sm:w-[28rem]"
321
+ portalId="masterpass-check-user"
322
+ >
323
+ <div className="p-10">
324
+ <p className="text-sm text-center">
325
+ You have cards registered to your Masterpass account. Do you want
326
+ to use your cards?
327
+ </p>
328
+ <Button
329
+ onClick={onClick}
330
+ disabled={loading}
331
+ className="bg-green-600 text-error w-full py-2 mt-4"
332
+ >
333
+ {loading ? 'Loading...' : 'Use'}
334
+ </Button>
335
+ {error && <p className="text-error text-sm">{error}</p>}
336
+ </div>
337
+ </Modal>
338
+ )
339
+ }
340
+ }}
341
+ />
342
+ ```
343
+
106
344
  ### Card List
107
345
 
346
+ #### Props
347
+
348
+ | Props | Type | Required | Description |
349
+ | --- | --- | --- | --- |
350
+ | className | string | Optional | Gives an additional style class to the component's outer container. |
351
+ | translations | { [key: string]: string } | Optional | Translation object to customize all texts. Overrides default texts. |
352
+ | renderer | RendererProps | Optional | Renderer functions that can be used to customize all subcomponents. |
353
+
354
+ #### RendererProps
355
+
356
+ | Props | Type | Description |
357
+ | --- | --- | --- |
358
+ | Header | JSX.Element | Used to customize the title area at the top of the card list. |
359
+ | CardItem | (params: { card, selectedCard, onSelect, DeleteButton }) => JSX.Element | It allows you to fully customize the card element. |
360
+ | Loader | () => JSX.Element | Used to define a custom loader component to be displayed when loading cards. |
361
+ | ErrorFallback | (params: { error: string; onRetry: () => void }) => JSX.Element | Custom widget to be shown if an error occurs while loading cards. |
362
+ | SwitchPaymentButton | (props: { onClick: () => void; label: string }) => JSX.Element | Used to customize the "Pay with a new card" option. |
363
+
108
364
  ##### File Path: src/views/checkout/steps/payment/options/credit-card/index.tsx
109
365
 
366
+ ### Usage Examples
367
+
368
+ ##### Default Usage
369
+
110
370
  ```javascript
111
371
  <PluginModule
112
372
  component={Component.MasterpassCardList}
@@ -121,10 +381,127 @@ npx @akinon/projectzero@latest --plugins
121
381
  />
122
382
  ```
123
383
 
384
+ ##### Customized Usage with Renderer
385
+
386
+ ```javascript
387
+ <PluginModule
388
+ component={Component.MasterpassCardList}
389
+ props={{
390
+ className: 'px-10',
391
+ form: {
392
+ control,
393
+ register,
394
+ errors,
395
+ setFormValue,
396
+ clearErrors
397
+ },
398
+ renderer: {
399
+ Header: () => (
400
+ <div className="flex items-center gap-2 mt-4 px-10 text-success">
401
+ Select a card to pay
402
+ </div>
403
+ ),
404
+ Loader: () => (
405
+ <div className="flex justify-center items-center p-10">
406
+ <span className="text-lg font-bold text-error">
407
+ Loading your cards...
408
+ </span>
409
+ </div>
410
+ ),
411
+ ErrorFallback: ({ error, onRetry }) => (
412
+ <div className="text-center p-8">
413
+ <h2 className="text-red-500 text-xl mb-4">Oops! {error}</h2>
414
+ <Button onClick={onRetry}>Try Again</Button>
415
+ </div>
416
+ ),
417
+ CardItem: ({ card, onSelect, DeleteButton, selectedCard }) => (
418
+ <li
419
+ key={card.UniqueId}
420
+ className="p-4 cursor-pointer rounded-[10px] border-gray border space-x-5 pl-5 mb-6 md:mb-2.5"
421
+ onClick={() => onSelect(card)}
422
+ >
423
+ <div className="flex justify-between items-center">
424
+ <div className="flex items-center">
425
+ <Checkbox
426
+ key={
427
+ selectedCard?.UniqueId === card.UniqueId
428
+ ? 'selected'
429
+ : 'not-selected'
430
+ }
431
+ checked={selectedCard?.UniqueId === card.UniqueId}
432
+ onChange={() => onSelect(card)}
433
+ className="mr-2"
434
+ />
435
+ <div>
436
+ <h3 className="font-semibold">{card.Name}</h3>
437
+ <p className="text-xs text-gray-500">{card.Value1}</p>
438
+ </div>
439
+ </div>
440
+ <DeleteButton cardAliasName={card.Name} />
441
+ </div>
442
+ </li>
443
+ ),
444
+ SwitchPaymentButton: ({ onClick, label }) => (
445
+ <div className="px-10">
446
+ <button
447
+ onClick={onClick}
448
+ className="p-4 w-full text-left cursor-pointer rounded-[10px] border-gray border space-x-5 pl-5 mb-6 md:mb-2.5"
449
+ >
450
+ {label}
451
+ </button>
452
+ </div>
453
+ )
454
+ }
455
+ }}
456
+ />
457
+ ```
458
+
124
459
  ### Card Registration
125
460
 
461
+ #### Props
462
+
463
+ | Props | Type | Required | Description |
464
+ | --- | --- | --- | --- |
465
+ | getValues | () => Record<string, string> | Required | Returns the values ​​of form fields. |
466
+ | className | string | Optional | Gives an additional CSS class to the outer container. |
467
+ | infoModalContent | React.ReactNode | Optional | Body content for the informational modal. |
468
+ | infoModalIcon | React.ReactNode | Optional | Used to override the notification icon. |
469
+ | translations | typeof defaultTranslations | Optional | Can be used to override default texts. |
470
+ | renderer | RendererProps | Optional | Renderer functions that can be used to customize all subcomponents. |
471
+
472
+ #### RendererProps
473
+
474
+ | Props | Type | Description |
475
+ | --- | --- | --- |
476
+ | Content | (props: { isChecked, toggle, setIsInfoModalOpen, onChange, onSubmit, loading, error, showGoBackLink?, onClickGoBackLink? }) => JSX.Element | Used to render the main content. |
477
+ | InfoModal | (props: { open: boolean; setOpen: (value: boolean) => void }) => JSX.Element | Can be used to override the notification modal. |
478
+
126
479
  ##### File Path: src/views/checkout/steps/payment/options/credit-card/index.tsx
127
480
 
481
+ ### Usage Examples
482
+
483
+ ##### Default Usage
484
+
485
+ ```javascript
486
+ <PluginModule
487
+ component={Component.MasterpassCardRegistration}
488
+ props={{
489
+ // Do not remove getValues, it is used to get the form values
490
+ getValues,
491
+ translations: {
492
+ enter_card_name: 'Enter card name',
493
+ continue: 'Continue',
494
+ pay_with_my_masterpass_card: 'Pay with my Masterpass card',
495
+ terms_and_conditions: 'Masterpass terms and conditions',
496
+ card_registration_consent:
497
+ 'I want to store my card information in the Mastercard infrastructure and use it again in my next purchase.'
498
+ }
499
+ }}
500
+ />
501
+ ```
502
+
503
+ ##### Customized Usage with className, infoModalContent, infoModalIcon
504
+
128
505
  ```javascript
129
506
  <PluginModule
130
507
  component={Component.MasterpassCardRegistration}
@@ -145,3 +522,86 @@ npx @akinon/projectzero@latest --plugins
145
522
  }}
146
523
  />
147
524
  ```
525
+
526
+ ##### Customized Usage with Renderer
527
+
528
+ ```javascript
529
+ <PluginModule
530
+ component={Component.MasterpassCardRegistration}
531
+ props={{
532
+ // Do not remove getValues, it is used to get the form values
533
+ getValues,
534
+ renderer: {
535
+ InfoModal: ({ open, setOpen }) => (
536
+ <Modal
537
+ open={open}
538
+ setOpen={setOpen}
539
+ portalId="masterpass-info-modal"
540
+ >
541
+ masterpass information
542
+ </Modal>
543
+ ),
544
+ Content: ({
545
+ isChecked,
546
+ toggle,
547
+ setIsInfoModalOpen,
548
+ onChange,
549
+ onSubmit,
550
+ loading,
551
+ error,
552
+ showGoBackLink,
553
+ onClickGoBackLink
554
+ }) => (
555
+ <div className="bg-[#F8F8F8] mx-10 mb-5 px-2 py-1">
556
+ <div className="flex items-center gap-2 pb-4 mb-4">
557
+ <Checkbox checked={isChecked} onChange={toggle} />
558
+ <h1>Masterpass</h1>
559
+ <div>
560
+ <Icon
561
+ name="info"
562
+ size={15}
563
+ className="fill-[#000000] cursor-pointer"
564
+ onClick={() => {
565
+ setIsInfoModalOpen(true);
566
+ }}
567
+ />
568
+ </div>
569
+ </div>
570
+ <p className="text-xs">
571
+ I want to store my card information in the Mastercard infrastructure and use it again in my next purchase.
572
+ </p>
573
+ {isChecked && (
574
+ <>
575
+ <Input
576
+ label="Give your card a name"
577
+ onChange={(e) =>
578
+ onChange((e.target as HTMLInputElement).value)
579
+ }
580
+ className="mt-2"
581
+ />
582
+ <Button
583
+ onClick={onSubmit}
584
+ className="bg-green-600 text-black w-full mt-3"
585
+ >
586
+ {loading ? 'Loading...' : 'Save'}
587
+ </Button>
588
+ {error && (
589
+ <p className="text-xs text-red-600 mt-2">🚨 {error}</p>
590
+ )}
591
+ </>
592
+ )}
593
+ {showGoBackLink && (
594
+ <div>
595
+ <button
596
+ onClick={onClickGoBackLink}
597
+ className="text-xs text-red-600 underline mt-3"
598
+ >
599
+ I want to pay with my card registered to Masterpass.
600
+ </button>
601
+ </div>
602
+ )}
603
+ </div>
604
+ )
605
+ }
606
+ }}
607
+ ```
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@akinon/pz-masterpass",
3
- "version": "2.0.0-beta.2",
3
+ "version": "2.0.0-beta.20",
4
4
  "license": "MIT",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.d.ts",
7
7
  "peerDependencies": {
8
- "react": "^19.0.0",
9
- "react-dom": "^19.0.0"
8
+ "react": "^18.0.0 || ^19.0.0",
9
+ "react-dom": "^18.0.0 || ^19.0.0"
10
10
  },
11
11
  "devDependencies": {
12
- "@types/node": "^22.10.2",
13
- "@types/react": "^19.0.2",
14
- "@types/react-dom": "^19.0.2",
12
+ "@types/node": "^18.7.8",
13
+ "@types/react": "^18.0.17",
14
+ "@types/react-dom": "^18.0.6",
15
15
  "@types/jquery": "^3.5.16",
16
- "typescript": "^5.7.2"
16
+ "typescript": "^4.7.4"
17
17
  }
18
18
  }
@@ -116,6 +116,19 @@ const rootSlice = createSlice({
116
116
  },
117
117
  setCvcRequired: (state, { payload }: PayloadAction<boolean>) => {
118
118
  state.cvcRequired = payload;
119
+ },
120
+ resetMasterpassState: (state) => {
121
+ state.cards = [];
122
+ state.otp = {
123
+ isModalVisible: false
124
+ };
125
+ state.isDirectPurchase = true;
126
+ state.deletion = {
127
+ isModalVisible: false
128
+ };
129
+ state.additionalParams = {};
130
+ state.cvcRequired = false;
131
+ state.selectedCard = undefined;
119
132
  }
120
133
  }
121
134
  });
@@ -137,7 +150,8 @@ export const {
137
150
  setDeletionModalVisible,
138
151
  setDeletionCardAliasName,
139
152
  setAdditionalParams,
140
- setCvcRequired
153
+ setCvcRequired,
154
+ resetMasterpassState
141
155
  } = rootSlice.actions;
142
156
 
143
157
  export default rootSlice.reducer;
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { JSX } from 'react';
3
4
  import { LoaderSpinner } from 'components';
4
5
  import { MasterpassStatus } from '../../types';
5
6
  import { getCreditCardType } from '../../utils';
@@ -48,6 +49,25 @@ const defaultTranslations = {
48
49
  security_code_info: 'What’s CVC?'
49
50
  };
50
51
 
52
+ interface RendererProps {
53
+ Header?: () => JSX.Element;
54
+ CardItem?: (params: {
55
+ card: any;
56
+ selectedCard: any;
57
+ onSelect: (card: any) => Promise<void>;
58
+ DeleteButton?: React.FC<{ cardAliasName: string; onDelete: () => void }>;
59
+ }) => JSX.Element;
60
+ Loader?: () => JSX.Element;
61
+ ErrorFallback?: (params: {
62
+ error: string;
63
+ onRetry: () => void;
64
+ }) => JSX.Element;
65
+ SwitchPaymentButton?: (props: {
66
+ onClick: () => void;
67
+ label: string;
68
+ }) => JSX.Element;
69
+ }
70
+
51
71
  export interface MasterpassCardListProps {
52
72
  className?: string;
53
73
  translations?: typeof defaultTranslations;
@@ -58,12 +78,14 @@ export interface MasterpassCardListProps {
58
78
  register: UseFormRegister<any>;
59
79
  setFormValue: UseFormSetValue<any>;
60
80
  };
81
+ renderer?: RendererProps;
61
82
  }
62
83
 
63
84
  export const MasterpassCardList = ({
64
85
  className,
65
86
  translations,
66
- form
87
+ form,
88
+ renderer
67
89
  }: MasterpassCardListProps) => {
68
90
  const { preOrder } = useAppSelector((state) => state.checkout);
69
91
  const { accountStatus, isDirectPurchase, selectedCard, cvcRequired } =
@@ -73,7 +95,34 @@ export const MasterpassCardList = ({
73
95
  const { DeleteButton } = useDeleteCard();
74
96
  const dispatch = useAppDispatch();
75
97
 
76
- const switchPaymentButton = (
98
+ const onSelectCard = async (card: any) => {
99
+ if (selectedCard?.UniqueId !== card.UniqueId) {
100
+ const maskStartIndex = card.Value1.indexOf('*');
101
+ const binNumber =
102
+ maskStartIndex !== -1
103
+ ? card.Value1.substring(0, maskStartIndex)
104
+ : card.Value1.substring(0, 6);
105
+
106
+ await setMasterpassBinNumber(binNumber).unwrap();
107
+ dispatch(setSelectedCard(card));
108
+ dispatch(setCvcRequired(false));
109
+ if (form) {
110
+ form.setFormValue('card_cvv', '');
111
+ form.clearErrors('card_cvv');
112
+ }
113
+ }
114
+ };
115
+
116
+ const switchPaymentButton = renderer?.SwitchPaymentButton ? (
117
+ renderer.SwitchPaymentButton({
118
+ onClick: () => {
119
+ dispatch(setIsDirectPurchase(true));
120
+ dispatch(setInstallmentOptions([]));
121
+ },
122
+ label:
123
+ translations?.pay_with_new_card ?? defaultTranslations.pay_with_new_card
124
+ })
125
+ ) : (
77
126
  <a
78
127
  href="#"
79
128
  className="text-xs underline"
@@ -92,7 +141,9 @@ export const MasterpassCardList = ({
92
141
  }
93
142
 
94
143
  if (loading) {
95
- return (
144
+ return renderer?.Loader ? (
145
+ renderer.Loader()
146
+ ) : (
96
147
  <div className="p-5">
97
148
  <LoaderSpinner className="w-8 h-8" />
98
149
  </div>
@@ -100,18 +151,14 @@ export const MasterpassCardList = ({
100
151
  }
101
152
 
102
153
  if (error && !isDirectPurchase) {
103
- return (
154
+ return renderer?.ErrorFallback ? (
155
+ renderer.ErrorFallback({ error, onRetry: refreshCards })
156
+ ) : (
104
157
  <div className="flex flex-col items-center">
105
158
  <div className="p-5">{error}</div>
106
- <Button
107
- className="w-48 mb-5"
108
- onClick={() => {
109
- refreshCards();
110
- }}
111
- >
159
+ <Button className="w-48 mb-5" onClick={refreshCards}>
112
160
  {translations?.retryFetchCards ?? defaultTranslations.retryFetchCards}
113
161
  </Button>
114
-
115
162
  {switchPaymentButton}
116
163
  </div>
117
164
  );
@@ -126,38 +173,75 @@ export const MasterpassCardList = ({
126
173
  }
127
174
 
128
175
  return (
129
- <div className={twMerge('w-full', className)}>
130
- <Image
131
- className="mb-4"
132
- width={140}
133
- height={25}
134
- src={masterpassLogo.src}
135
- alt="Masterpass Logo"
136
- />
137
- <p className="text-xs">
138
- {translations?.title ?? defaultTranslations.title}
139
- </p>
140
- <ul className="mt-4 text-xs">
176
+ <div className="w-full">
177
+ {renderer?.Header ? (
178
+ renderer.Header()
179
+ ) : (
180
+ <>
181
+ <Image
182
+ className="mb-4"
183
+ width={140}
184
+ height={25}
185
+ src={masterpassLogo.src}
186
+ alt="Masterpass Logo"
187
+ />
188
+ <p className="text-xs">
189
+ {translations?.title ?? defaultTranslations.title}
190
+ </p>
191
+ </>
192
+ )}
193
+
194
+ <ul className={twMerge('mt-4 text-xs', className)}>
141
195
  {cards?.map((card) => {
142
- return (
196
+ const cvcField =
197
+ selectedCard?.UniqueId === card.UniqueId && form && cvcRequired ? (
198
+ <div
199
+ className={twMerge(
200
+ clsx('flex items-center justify-start mt-2', {
201
+ 'items-baseline': form.errors.card_cvv
202
+ })
203
+ )}
204
+ >
205
+ <label
206
+ className="text-xs text-black-400 mr-1.5"
207
+ htmlFor="card_cvv"
208
+ >
209
+ {translations?.security_code ??
210
+ defaultTranslations.security_code}
211
+ </label>
212
+ <Input
213
+ format="###"
214
+ mask="_"
215
+ control={form.control}
216
+ allowEmptyFormatting={true}
217
+ {...form.register('card_cvv')}
218
+ error={form.errors.card_cvv}
219
+ />
220
+ <div className="group relative flex items-center justify-start text-gray-600 cursor-pointer ml-2 transition-all hover:text-secondary">
221
+ <span className="text-xs underline">
222
+ {translations?.security_code_info ??
223
+ defaultTranslations.security_code_info}
224
+ </span>
225
+ <Icon name="cvc" size={16} className="leading-none ml-2" />
226
+ <div className="hidden group-hover:block absolute right-0 bottom-5 w-[11rem] lg:w-[21rem] lg:left-auto lg:right-auto border-2">
227
+ <Image src="/cvv.jpg" alt="Cvv" width={385} height={262} />
228
+ </div>
229
+ </div>
230
+ </div>
231
+ ) : null;
232
+
233
+ return renderer?.CardItem ? (
234
+ renderer.CardItem({
235
+ card,
236
+ selectedCard,
237
+ onSelect: onSelectCard,
238
+ DeleteButton
239
+ })
240
+ ) : (
143
241
  <li
144
242
  key={card.UniqueId}
145
243
  className="p-4 mb-2 border-2 border-gray-200 flex flex-col space-x-2 cursor-pointer"
146
- onClick={async () => {
147
- if (selectedCard?.UniqueId !== card.UniqueId) {
148
- await setMasterpassBinNumber(
149
- card.Value1.substring(0, 6)
150
- ).unwrap();
151
-
152
- dispatch(setSelectedCard(card));
153
- dispatch(setCvcRequired(false));
154
-
155
- if (form) {
156
- form.setFormValue('card_cvv', '');
157
- form.clearErrors('card_cvv');
158
- }
159
- }
160
- }}
244
+ onClick={() => onSelectCard(card)}
161
245
  >
162
246
  <div className="flex justify-between items-center">
163
247
  <input
@@ -183,49 +267,9 @@ export const MasterpassCardList = ({
183
267
  alt={card.Name}
184
268
  />
185
269
  </label>
186
-
187
270
  <DeleteButton cardAliasName={card.Name} />
188
271
  </div>
189
- {selectedCard?.UniqueId === card.UniqueId && form && cvcRequired && (
190
- <div
191
- className={twMerge(
192
- clsx('flex items-center justify-start mt-2', {
193
- 'items-baseline': form.errors.card_cvv
194
- })
195
- )}
196
- >
197
- <label
198
- className="text-xs text-black-400 mr-1.5"
199
- htmlFor="card_cvv"
200
- >
201
- {translations?.security_code ??
202
- defaultTranslations.security_code}
203
- </label>
204
- <Input
205
- format="###"
206
- mask="_"
207
- control={form.control}
208
- allowEmptyFormatting={true}
209
- {...form.register('card_cvv')}
210
- error={form.errors.card_cvv}
211
- />
212
- <div className="group relative flex items-center justify-start text-gray-600 cursor-pointer ml-2 transition-all hover:text-secondary">
213
- <span className="text-xs underline">
214
- {translations?.security_code_info ??
215
- defaultTranslations.security_code_info}
216
- </span>
217
- <Icon name="cvc" size={16} className="leading-none ml-2" />
218
- <div className="hidden group-hover:block absolute right-0 bottom-5 w-[11rem] lg:w-[21rem] lg:left-auto lg:right-auto border-2">
219
- <Image
220
- src="/cvv.jpg"
221
- alt="Cvv"
222
- width={385}
223
- height={262}
224
- />
225
- </div>
226
- </div>
227
- </div>
228
- )}
272
+ {cvcField}
229
273
  </li>
230
274
  );
231
275
  })}
@@ -1,6 +1,6 @@
1
1
  import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
2
2
  import { Button, Checkbox, Icon, Input, LoaderSpinner } from 'components';
3
- import React, { useEffect, useState } from 'react';
3
+ import React, { JSX, useEffect, useState } from 'react';
4
4
  import { RootState } from 'redux/store';
5
5
  import masterpassLogo from '../../../assets/img/mp_masterpass-logo.png';
6
6
  import { formCreator } from '../../utils';
@@ -32,13 +32,31 @@ export const MasterpassCardRegistration = ({
32
32
  className,
33
33
  infoModalContent,
34
34
  infoModalIcon,
35
- translations
35
+ translations,
36
+ renderer = {}
36
37
  }: {
37
38
  getValues: () => Record<string, string | number | boolean>;
38
39
  className?: string;
39
40
  infoModalContent?: React.ReactNode;
40
41
  infoModalIcon?: React.ReactNode;
41
42
  translations?: typeof defaultTranslations;
43
+ renderer?: {
44
+ Content?: (props: {
45
+ isChecked: boolean;
46
+ toggle: () => void;
47
+ setIsInfoModalOpen: (value: boolean) => void;
48
+ onChange: (value: string) => void;
49
+ onSubmit: () => void;
50
+ loading: boolean;
51
+ error: string | null;
52
+ showGoBackLink?: boolean;
53
+ onClickGoBackLink?: () => void;
54
+ }) => JSX.Element;
55
+ InfoModal?: (props: {
56
+ open: boolean;
57
+ setOpen: (value: boolean) => void;
58
+ }) => JSX.Element;
59
+ };
42
60
  }) => {
43
61
  const { preOrder } = useAppSelector((state: RootState) => state.checkout);
44
62
  const { msisdn, token, language, otp, isDirectPurchase, accountStatus } =
@@ -168,13 +186,56 @@ export const MasterpassCardRegistration = ({
168
186
  return null;
169
187
  }
170
188
 
189
+ if (renderer.Content) {
190
+ return (
191
+ <>
192
+ {renderer.InfoModal ? (
193
+ <renderer.InfoModal
194
+ open={isInfoModalOpen}
195
+ setOpen={setIsInfoModalOpen}
196
+ />
197
+ ) : (
198
+ <InfoModal
199
+ open={isInfoModalOpen}
200
+ setOpen={setIsInfoModalOpen}
201
+ content={infoModalContent}
202
+ />
203
+ )}
204
+
205
+ <renderer.Content
206
+ isChecked={isAgreementChecked}
207
+ toggle={() => setIsAgreementChecked(!isAgreementChecked)}
208
+ setIsInfoModalOpen={setIsInfoModalOpen}
209
+ onChange={(value) => setAccountAliasName(value)}
210
+ onSubmit={registerCard}
211
+ loading={isBusy}
212
+ error={formError}
213
+ showGoBackLink={
214
+ accountStatus !== MasterpassStatus.NoAccount && cards?.length > 0
215
+ }
216
+ onClickGoBackLink={() => {
217
+ dispatch(setIsDirectPurchase(false));
218
+ dispatch(setInstallmentOptions([]));
219
+ }}
220
+ />
221
+ </>
222
+ );
223
+ }
224
+
171
225
  return (
172
226
  <div className={twMerge('w-full', className)}>
173
- <InfoModal
174
- open={isInfoModalOpen}
175
- setOpen={setIsInfoModalOpen}
176
- content={infoModalContent}
177
- />
227
+ {renderer.InfoModal ? (
228
+ <renderer.InfoModal
229
+ open={isInfoModalOpen}
230
+ setOpen={setIsInfoModalOpen}
231
+ />
232
+ ) : (
233
+ <InfoModal
234
+ open={isInfoModalOpen}
235
+ setOpen={setIsInfoModalOpen}
236
+ content={infoModalContent}
237
+ />
238
+ )}
178
239
 
179
240
  <div className="border border-[#ddd]">
180
241
  <div className="p-4">
@@ -232,7 +293,7 @@ export const MasterpassCardRegistration = ({
232
293
  defaultTranslations.enter_card_name
233
294
  }
234
295
  onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
235
- setAccountAliasName(e.target.value);
296
+ setAccountAliasName(e.target.value.trim());
236
297
  }}
237
298
  />
238
299
  <Button
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { JSX } from 'react';
3
4
  import { Button, LoaderSpinner, Modal, Image } from '@akinon/next/components';
4
5
 
5
6
  import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
@@ -15,15 +16,39 @@ const defaultTranslations = {
15
16
 
16
17
  export interface MasterpassDeleteConfirmationModalProps {
17
18
  translations?: typeof defaultTranslations;
19
+ renderer?: {
20
+ Content?: (props: {
21
+ open: boolean;
22
+ setOpen: (open: boolean) => void;
23
+ onConfirm: () => void;
24
+ onCancel: () => void;
25
+ loading: boolean;
26
+ error: string | null;
27
+ }) => JSX.Element;
28
+ };
18
29
  }
19
30
 
20
31
  export const MasterpassDeleteConfirmationModal = ({
21
- translations
32
+ translations,
33
+ renderer = {}
22
34
  }: MasterpassDeleteConfirmationModalProps) => {
23
35
  const { deletion } = useAppSelector((state) => state.masterpass);
24
36
  const { deleteCard, isLoading, error } = useDeleteCard();
25
37
  const dispatch = useAppDispatch();
26
38
 
39
+ if (renderer.Content) {
40
+ return (
41
+ <renderer.Content
42
+ open={deletion.isModalVisible}
43
+ setOpen={() => dispatch(setDeletionModalVisible(false))}
44
+ onConfirm={deleteCard}
45
+ onCancel={() => dispatch(setDeletionModalVisible(false))}
46
+ loading={isLoading}
47
+ error={error}
48
+ />
49
+ );
50
+ }
51
+
27
52
  return (
28
53
  <Modal
29
54
  portalId="masterpass-remove-card-modal"
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
4
4
  import { Button, LoaderSpinner, Modal } from 'components';
5
- import { useCallback, useEffect, useState } from 'react';
5
+ import { JSX, useCallback, useEffect, useState } from 'react';
6
6
  import masterpassLogo from '../../../assets/img/mp_masterpass-logo.png';
7
7
  import {
8
8
  setAccountStatus,
@@ -21,10 +21,20 @@ const defaultTranslations = {
21
21
 
22
22
  export interface MasterpassLinkModalProps {
23
23
  translations?: typeof defaultTranslations;
24
+ renderer?: {
25
+ Content?: (props: {
26
+ open: boolean;
27
+ setOpen: (open: boolean) => void;
28
+ onClick: () => void;
29
+ loading: boolean;
30
+ error: string | null;
31
+ }) => JSX.Element;
32
+ };
24
33
  }
25
34
 
26
35
  export const MasterpassLinkModal = ({
27
- translations
36
+ translations,
37
+ renderer = {}
28
38
  }: MasterpassLinkModalProps) => {
29
39
  const { msisdn, token, accountStatus, otp, language } = useAppSelector(
30
40
  (state) => state.masterpass
@@ -93,6 +103,18 @@ export const MasterpassLinkModal = ({
93
103
  }
94
104
  }, [otp.response]);
95
105
 
106
+ if (renderer.Content) {
107
+ return (
108
+ <renderer.Content
109
+ open={isOpen}
110
+ setOpen={setIsOpen}
111
+ onClick={onLinkButtonClick}
112
+ loading={isLoading}
113
+ error={error}
114
+ />
115
+ );
116
+ }
117
+
96
118
  return (
97
119
  <Modal
98
120
  portalId="masterpass-check-user"
@@ -3,7 +3,7 @@
3
3
  import { Modal } from '@akinon/next/components';
4
4
  import masterpassLogo from '../../../assets/img/mp_masterpass-logo.png';
5
5
 
6
- import { useCallback, useEffect, useState } from 'react';
6
+ import { JSX, useCallback, useEffect, useState } from 'react';
7
7
 
8
8
  import { formCreator } from '../../utils';
9
9
 
@@ -17,10 +17,24 @@ export interface MasterpassOtpModalProps {
17
17
  translations?: {
18
18
  5001?: string;
19
19
  } & OtpFormProps['translations'];
20
+ renderer?: {
21
+ Content?: (props: {
22
+ open: boolean;
23
+ setOpen: (open: boolean) => void;
24
+ onSubmit: (data: { otp_code: string }) => void;
25
+ loading: boolean;
26
+ error: string | null;
27
+ resendSms: () => void;
28
+ resendSmsFetching: boolean;
29
+ targetDate: number;
30
+ otpRef: string | null;
31
+ }) => JSX.Element;
32
+ };
20
33
  }
21
34
 
22
35
  export const MasterpassOtpModal = ({
23
- translations
36
+ translations,
37
+ renderer = {}
24
38
  }: MasterpassOtpModalProps) => {
25
39
  const { token, otp, language } = useAppSelector((state) => state.masterpass);
26
40
  const [isModalOpen, setIsModalOpen] = useState(false);
@@ -33,7 +47,7 @@ export const MasterpassOtpModal = ({
33
47
 
34
48
  const onFormSubmit = useCallback(
35
49
  (data: { otp_code: string }) => {
36
- if (!token || !language) {
50
+ if (!token || !language || isBusy) {
37
51
  return;
38
52
  }
39
53
 
@@ -78,7 +92,7 @@ export const MasterpassOtpModal = ({
78
92
  const resendSms = () => {
79
93
  const token = window.MFS.getLastToken();
80
94
 
81
- if (!token && isBusy) {
95
+ if (!token || isBusy) {
82
96
  return;
83
97
  }
84
98
 
@@ -111,6 +125,22 @@ export const MasterpassOtpModal = ({
111
125
  }
112
126
  }, [isModalOpen]);
113
127
 
128
+ if (renderer.Content) {
129
+ return (
130
+ <renderer.Content
131
+ open={isModalOpen}
132
+ setOpen={handleModalVisibility}
133
+ onSubmit={onFormSubmit}
134
+ loading={isBusy}
135
+ error={otpError}
136
+ resendSms={resendSms}
137
+ resendSmsFetching={isBusy}
138
+ targetDate={otpTime}
139
+ otpRef={otpRef}
140
+ />
141
+ );
142
+ }
143
+
114
144
  return (
115
145
  <Modal
116
146
  portalId="otp-masterpass"
@@ -51,7 +51,7 @@ export const OtpForm = ({
51
51
  reset,
52
52
  formState: { errors }
53
53
  } = useForm({
54
- resolver: yupResolver(formSchema())
54
+ resolver: yupResolver(formSchema()) as any
55
55
  });
56
56
 
57
57
  useEffect(() => {
@@ -68,7 +68,7 @@ export const OtpForm = ({
68
68
  max="999999"
69
69
  min="000000"
70
70
  label={translations?.sms_code ?? defaultTranslations.sms_code}
71
- control={control}
71
+ control={control as any}
72
72
  {...register('otp_code')}
73
73
  error={errors.otp_code}
74
74
  />