@akinon/pz-masterpass-rest 2.0.56-beta.0 → 2.0.56
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 +5 -1
- package/jest/__mocks__/akinon-next-components.js +26 -0
- package/jest/__mocks__/akinon-next-redux-hooks.js +6 -0
- package/jest/__mocks__/akinon-next-rtk.js +4 -0
- package/jest/__mocks__/akinon-next-utils.js +4 -0
- package/jest/__mocks__/hookform-resolvers.js +6 -0
- package/jest/__mocks__/host-store.js +16 -0
- package/jest/file-mock.js +1 -0
- package/jest/setup.ts +37 -0
- package/jest/style-mock.js +1 -0
- package/jest.config.js +46 -0
- package/package.json +4 -1
- package/src/hooks/use-masterpass-account-otp.test.tsx +189 -0
- package/src/hooks/use-masterpass-payment-pending.test.tsx +255 -0
- package/src/hooks/useMasterpassAccount.ts +30 -1
- package/src/hooks/useMasterpassPayment.ts +67 -3
- package/src/redux/reducer.ts +2 -0
- package/src/types/custom-render.types.ts +4 -0
- package/src/types/payment.types.ts +3 -0
- package/src/utils/payment-pending.test.ts +89 -0
- package/src/utils/payment-utils.ts +47 -0
- package/src/views/masterpass-rest-option.tsx +56 -6
- package/src/views/order-no-mismatch.test.tsx +434 -0
- package/src/views/payment-pending-guard.test.tsx +276 -0
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
handleMasterpassResponse,
|
|
20
20
|
handleVerification
|
|
21
21
|
} from '../utils/response-handler';
|
|
22
|
+
import { getPaymentPending, setPaymentPending } from '../utils/payment-utils';
|
|
22
23
|
import type {
|
|
23
24
|
AccountAccessSuccessResponse,
|
|
24
25
|
CardModel
|
|
@@ -186,6 +187,7 @@ export const useMasterpassAccount = () => {
|
|
|
186
187
|
dispatch(
|
|
187
188
|
updateModalState({
|
|
188
189
|
otpType: result.otpType,
|
|
190
|
+
otpContext: 'account',
|
|
189
191
|
verificationData: result.data,
|
|
190
192
|
showLinkModal: false,
|
|
191
193
|
showOTPModal: true
|
|
@@ -205,9 +207,22 @@ export const useMasterpassAccount = () => {
|
|
|
205
207
|
const handleOTPSubmit = useCallback(
|
|
206
208
|
async (otp: string, texts?: any) => {
|
|
207
209
|
const result = await handleVerification(otp, token, tokenData?.MerchantId);
|
|
210
|
+
const isPaymentContext = modalState.otpContext === 'payment';
|
|
208
211
|
|
|
209
212
|
if (result.success) {
|
|
210
213
|
dispatch(setShowOTPModal(false));
|
|
214
|
+
|
|
215
|
+
// A payment-context OTP still has an open Masterpass transaction. The
|
|
216
|
+
// caller has to complete it against the backend; refreshing the account
|
|
217
|
+
// here would leave the order unfinalized.
|
|
218
|
+
if (isPaymentContext) {
|
|
219
|
+
return {
|
|
220
|
+
success: true,
|
|
221
|
+
isPaymentContext: true,
|
|
222
|
+
verificationData: result.data
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
211
226
|
await refreshAccountData();
|
|
212
227
|
return { success: true };
|
|
213
228
|
} else if (result.sessionExpired) {
|
|
@@ -230,6 +245,13 @@ export const useMasterpassAccount = () => {
|
|
|
230
245
|
};
|
|
231
246
|
} else if (result.requires3D && result.redirectUrl) {
|
|
232
247
|
dispatch(setShowOTPModal(false));
|
|
248
|
+
|
|
249
|
+
// Leaving the tab for the bank restarts the wait, so refresh the record
|
|
250
|
+
// without losing the order number it was opened with.
|
|
251
|
+
if (isPaymentContext) {
|
|
252
|
+
setPaymentPending(getPaymentPending()?.orderNo ?? null);
|
|
253
|
+
}
|
|
254
|
+
|
|
233
255
|
window.location.href = result.redirectUrl;
|
|
234
256
|
return {
|
|
235
257
|
success: false,
|
|
@@ -257,7 +279,13 @@ export const useMasterpassAccount = () => {
|
|
|
257
279
|
};
|
|
258
280
|
}
|
|
259
281
|
},
|
|
260
|
-
[
|
|
282
|
+
[
|
|
283
|
+
dispatch,
|
|
284
|
+
refreshAccountData,
|
|
285
|
+
token,
|
|
286
|
+
tokenData?.MerchantId,
|
|
287
|
+
modalState.otpContext
|
|
288
|
+
]
|
|
261
289
|
);
|
|
262
290
|
|
|
263
291
|
const handleRemoveCard = useCallback(
|
|
@@ -368,6 +396,7 @@ export const useMasterpassAccount = () => {
|
|
|
368
396
|
dispatch(
|
|
369
397
|
updateModalState({
|
|
370
398
|
otpType: result.otpType,
|
|
399
|
+
otpContext: 'account',
|
|
371
400
|
verificationData: result.data,
|
|
372
401
|
showLinkModal: false,
|
|
373
402
|
showOTPModal: true
|
|
@@ -28,7 +28,9 @@ import { PaymentService } from '../services/payment';
|
|
|
28
28
|
import {
|
|
29
29
|
createPaymentRequest,
|
|
30
30
|
createDirectPaymentRequest,
|
|
31
|
-
getTransactionType
|
|
31
|
+
getTransactionType,
|
|
32
|
+
setPaymentPending,
|
|
33
|
+
clearPaymentPending
|
|
32
34
|
} from '../utils/payment-utils';
|
|
33
35
|
import { handleMasterpassResponse } from '../utils/response-handler';
|
|
34
36
|
import type { Installment, RewardItem } from '../types/payment.types';
|
|
@@ -248,13 +250,20 @@ export const useMasterpassPayment = (
|
|
|
248
250
|
|
|
249
251
|
dispatch(setOrderData(prepareResult));
|
|
250
252
|
|
|
253
|
+
const orderNo = prepareResult.context_list?.[0]?.page_context?.order_no;
|
|
254
|
+
|
|
255
|
+
// From here on the order number is bound to a Masterpass transaction.
|
|
256
|
+
// Regenerating it before that transaction settles makes the backend
|
|
257
|
+
// reject the completion with ORDER_NO_MISMATCH.
|
|
258
|
+
setPaymentPending(orderNo);
|
|
259
|
+
|
|
251
260
|
const paymentService = new PaymentService(activeToken, activeMerchantId);
|
|
252
261
|
const paymentRequest = createPaymentRequest({
|
|
253
262
|
cvc,
|
|
254
263
|
cardAlias: selectedCard.cardAlias,
|
|
255
264
|
accountKey: activeTokenData.AccountKey,
|
|
256
265
|
amount: prepareResult.pre_order?.total_amount_with_interest,
|
|
257
|
-
orderNo
|
|
266
|
+
orderNo,
|
|
258
267
|
currencyCode: currency,
|
|
259
268
|
installmentCount: selectedInstallment.installment_count,
|
|
260
269
|
authenticationMethod: activeTokenData.AuthenticationMethod,
|
|
@@ -284,6 +293,7 @@ export const useMasterpassPayment = (
|
|
|
284
293
|
}).unwrap();
|
|
285
294
|
|
|
286
295
|
dispatch(setOrderData(finalizeResult));
|
|
296
|
+
clearPaymentPending();
|
|
287
297
|
|
|
288
298
|
if (finalizeResult.errors) {
|
|
289
299
|
return { success: false, message: finalizeResult.errors };
|
|
@@ -293,6 +303,10 @@ export const useMasterpassPayment = (
|
|
|
293
303
|
return { success: true };
|
|
294
304
|
}
|
|
295
305
|
|
|
306
|
+
if (!response.requiresOTP) {
|
|
307
|
+
clearPaymentPending();
|
|
308
|
+
}
|
|
309
|
+
|
|
296
310
|
return {
|
|
297
311
|
success: response.success,
|
|
298
312
|
requiresOTP: response.requiresOTP,
|
|
@@ -300,6 +314,7 @@ export const useMasterpassPayment = (
|
|
|
300
314
|
message: response.message
|
|
301
315
|
};
|
|
302
316
|
} catch (error) {
|
|
317
|
+
clearPaymentPending();
|
|
303
318
|
return {
|
|
304
319
|
success: false,
|
|
305
320
|
message:
|
|
@@ -357,6 +372,13 @@ export const useMasterpassPayment = (
|
|
|
357
372
|
|
|
358
373
|
dispatch(setOrderData(prepareResult));
|
|
359
374
|
|
|
375
|
+
const orderNo = prepareResult.context_list?.[0]?.page_context?.order_no;
|
|
376
|
+
|
|
377
|
+
// From here on the order number is bound to a Masterpass transaction.
|
|
378
|
+
// Regenerating it before that transaction settles makes the backend
|
|
379
|
+
// reject the completion with ORDER_NO_MISMATCH.
|
|
380
|
+
setPaymentPending(orderNo);
|
|
381
|
+
|
|
360
382
|
const paymentService = new PaymentService(activeToken, activeMerchantId);
|
|
361
383
|
const directPaymentRequest = createDirectPaymentRequest({
|
|
362
384
|
cvc: newCardFormData.cvv,
|
|
@@ -366,7 +388,7 @@ export const useMasterpassPayment = (
|
|
|
366
388
|
expiryDate: newCardFormData.expiryDate,
|
|
367
389
|
accountKey: activeTokenData.AccountKey,
|
|
368
390
|
amount: prepareResult.pre_order?.total_amount_with_interest,
|
|
369
|
-
orderNo
|
|
391
|
+
orderNo,
|
|
370
392
|
currencyCode: currency,
|
|
371
393
|
installmentCount: selectedInstallment.installment_count,
|
|
372
394
|
authenticationMethod: activeTokenData.AuthenticationMethod,
|
|
@@ -398,6 +420,7 @@ export const useMasterpassPayment = (
|
|
|
398
420
|
}).unwrap();
|
|
399
421
|
|
|
400
422
|
dispatch(setOrderData(finalizeResult));
|
|
423
|
+
clearPaymentPending();
|
|
401
424
|
|
|
402
425
|
if (finalizeResult.errors) {
|
|
403
426
|
return { success: false, message: finalizeResult.errors };
|
|
@@ -407,6 +430,10 @@ export const useMasterpassPayment = (
|
|
|
407
430
|
return { success: true };
|
|
408
431
|
}
|
|
409
432
|
|
|
433
|
+
if (!response.requiresOTP) {
|
|
434
|
+
clearPaymentPending();
|
|
435
|
+
}
|
|
436
|
+
|
|
410
437
|
return {
|
|
411
438
|
success: response.success,
|
|
412
439
|
requiresOTP: response.requiresOTP,
|
|
@@ -414,6 +441,7 @@ export const useMasterpassPayment = (
|
|
|
414
441
|
message: response.message
|
|
415
442
|
};
|
|
416
443
|
} catch (error) {
|
|
444
|
+
clearPaymentPending();
|
|
417
445
|
return {
|
|
418
446
|
success: false,
|
|
419
447
|
message:
|
|
@@ -435,6 +463,41 @@ export const useMasterpassPayment = (
|
|
|
435
463
|
dispatch
|
|
436
464
|
]);
|
|
437
465
|
|
|
466
|
+
const finalizePaymentAfterVerification = useCallback(
|
|
467
|
+
async (verificationResponse: any): Promise<PaymentResult> => {
|
|
468
|
+
const { useThreeD } = paymentState;
|
|
469
|
+
|
|
470
|
+
try {
|
|
471
|
+
const finalizeResult = await finalizeMasterpassOrder({
|
|
472
|
+
responseCode: verificationResponse?.result?.responseCode,
|
|
473
|
+
token: verificationResponse?.result?.token,
|
|
474
|
+
three_d_secure: useThreeD,
|
|
475
|
+
transactionType: getTransactionType(useThreeD)
|
|
476
|
+
}).unwrap();
|
|
477
|
+
|
|
478
|
+
dispatch(setOrderData(finalizeResult));
|
|
479
|
+
clearPaymentPending();
|
|
480
|
+
|
|
481
|
+
if (finalizeResult.errors) {
|
|
482
|
+
return { success: false, message: finalizeResult.errors };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
dispatch(setOrderCompleted(true));
|
|
486
|
+
return { success: true };
|
|
487
|
+
} catch (error) {
|
|
488
|
+
clearPaymentPending();
|
|
489
|
+
return {
|
|
490
|
+
success: false,
|
|
491
|
+
message:
|
|
492
|
+
error instanceof Error
|
|
493
|
+
? error.message
|
|
494
|
+
: 'An unexpected error occurred'
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
[paymentState, finalizeMasterpassOrder, dispatch]
|
|
499
|
+
);
|
|
500
|
+
|
|
438
501
|
return {
|
|
439
502
|
paymentState,
|
|
440
503
|
|
|
@@ -451,6 +514,7 @@ export const useMasterpassPayment = (
|
|
|
451
514
|
handleInstallmentSelect,
|
|
452
515
|
processPayment,
|
|
453
516
|
processDirectPayment,
|
|
517
|
+
finalizePaymentAfterVerification,
|
|
454
518
|
fetchRewardsForCard,
|
|
455
519
|
openRewardModal,
|
|
456
520
|
closeRewardModal,
|
package/src/redux/reducer.ts
CHANGED
|
@@ -82,6 +82,7 @@ const initialState: MasterpassRestState = {
|
|
|
82
82
|
showRewardModal: false,
|
|
83
83
|
informationModalData: null,
|
|
84
84
|
otpType: 'OTP',
|
|
85
|
+
otpContext: 'account',
|
|
85
86
|
verificationData: null,
|
|
86
87
|
cardToDelete: null,
|
|
87
88
|
removingCardId: null
|
|
@@ -270,6 +271,7 @@ const masterpassRestSlice = createSlice({
|
|
|
270
271
|
showRewardModal: false,
|
|
271
272
|
informationModalData: null,
|
|
272
273
|
otpType: 'OTP',
|
|
274
|
+
otpContext: 'account',
|
|
273
275
|
verificationData: null,
|
|
274
276
|
cardToDelete: null,
|
|
275
277
|
removingCardId: null
|
|
@@ -114,6 +114,10 @@ export type MasterpassRestOptionRenderProps = {
|
|
|
114
114
|
cvc: string;
|
|
115
115
|
error: any;
|
|
116
116
|
|
|
117
|
+
/** True while a Masterpass transaction started on this tab has not settled. */
|
|
118
|
+
isPaymentPending: boolean;
|
|
119
|
+
dismissPaymentPending: () => void;
|
|
120
|
+
|
|
117
121
|
accountData: any;
|
|
118
122
|
accountStatus: any;
|
|
119
123
|
modalState: any;
|
|
@@ -102,6 +102,8 @@ export interface InformationModalData {
|
|
|
102
102
|
buttonText: string;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
export type OTPContext = 'account' | 'payment';
|
|
106
|
+
|
|
105
107
|
export interface ModalState {
|
|
106
108
|
showLinkModal: boolean;
|
|
107
109
|
showOTPModal: boolean;
|
|
@@ -110,6 +112,7 @@ export interface ModalState {
|
|
|
110
112
|
showRewardModal: boolean;
|
|
111
113
|
informationModalData: InformationModalData | null;
|
|
112
114
|
otpType: OTPType;
|
|
115
|
+
otpContext: OTPContext;
|
|
113
116
|
verificationData: any | null;
|
|
114
117
|
cardToDelete: any | null;
|
|
115
118
|
removingCardId: string | null;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
setPaymentPending,
|
|
3
|
+
getPaymentPending,
|
|
4
|
+
clearPaymentPending
|
|
5
|
+
} from './payment-utils';
|
|
6
|
+
|
|
7
|
+
const STORAGE_KEY = 'masterpass_rest_payment_pending';
|
|
8
|
+
const TTL_MS = 30 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
describe('payment pending record', () => {
|
|
11
|
+
it('reports nothing pending when no payment has started', () => {
|
|
12
|
+
expect(getPaymentPending()).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('remembers the order number the transaction was opened with', () => {
|
|
16
|
+
setPaymentPending('ORDER-123');
|
|
17
|
+
|
|
18
|
+
expect(getPaymentPending()).toEqual({
|
|
19
|
+
orderNo: 'ORDER-123',
|
|
20
|
+
startedAt: expect.any(Number)
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('still marks a payment pending when no order number is known', () => {
|
|
25
|
+
setPaymentPending(null);
|
|
26
|
+
|
|
27
|
+
const record = getPaymentPending();
|
|
28
|
+
expect(record).not.toBeNull();
|
|
29
|
+
expect(record?.orderNo).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('stops reporting pending once cleared', () => {
|
|
33
|
+
setPaymentPending('ORDER-123');
|
|
34
|
+
clearPaymentPending();
|
|
35
|
+
|
|
36
|
+
expect(getPaymentPending()).toBeNull();
|
|
37
|
+
expect(window.sessionStorage.getItem(STORAGE_KEY)).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('keeps a record that is still inside the ttl', () => {
|
|
41
|
+
window.sessionStorage.setItem(
|
|
42
|
+
STORAGE_KEY,
|
|
43
|
+
JSON.stringify({ orderNo: 'ORDER-123', startedAt: Date.now() - TTL_MS + 60_000 })
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(getPaymentPending()?.orderNo).toBe('ORDER-123');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('expires and purges a record past the ttl so an abandoned tab recovers', () => {
|
|
50
|
+
window.sessionStorage.setItem(
|
|
51
|
+
STORAGE_KEY,
|
|
52
|
+
JSON.stringify({ orderNo: 'ORDER-123', startedAt: Date.now() - TTL_MS - 1 })
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(getPaymentPending()).toBeNull();
|
|
56
|
+
expect(window.sessionStorage.getItem(STORAGE_KEY)).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('treats an unparseable record as not pending', () => {
|
|
60
|
+
window.sessionStorage.setItem(STORAGE_KEY, 'not-json');
|
|
61
|
+
|
|
62
|
+
expect(getPaymentPending()).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('treats a record without a timestamp as not pending', () => {
|
|
66
|
+
window.sessionStorage.setItem(
|
|
67
|
+
STORAGE_KEY,
|
|
68
|
+
JSON.stringify({ orderNo: 'ORDER-123' })
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(getPaymentPending()).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('degrades quietly when session storage is unavailable', () => {
|
|
75
|
+
const proto = Object.getPrototypeOf(window.sessionStorage);
|
|
76
|
+
const boom = () => {
|
|
77
|
+
throw new Error('storage disabled');
|
|
78
|
+
};
|
|
79
|
+
jest.spyOn(proto, 'setItem').mockImplementation(boom);
|
|
80
|
+
jest.spyOn(proto, 'getItem').mockImplementation(boom);
|
|
81
|
+
jest.spyOn(proto, 'removeItem').mockImplementation(boom);
|
|
82
|
+
|
|
83
|
+
expect(() => setPaymentPending('ORDER-123')).not.toThrow();
|
|
84
|
+
expect(getPaymentPending()).toBeNull();
|
|
85
|
+
expect(() => clearPaymentPending()).not.toThrow();
|
|
86
|
+
|
|
87
|
+
jest.restoreAllMocks();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -15,6 +15,53 @@ export const isTokenExpired = (
|
|
|
15
15
|
return Date.now() >= tokenData.exp * 1000 - bufferMs;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
const PAYMENT_PENDING_KEY = 'masterpass_rest_payment_pending';
|
|
19
|
+
const PAYMENT_PENDING_TTL_MS = 30 * 60 * 1000;
|
|
20
|
+
|
|
21
|
+
export interface PaymentPendingRecord {
|
|
22
|
+
orderNo: string | null;
|
|
23
|
+
startedAt: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const setPaymentPending = (orderNo?: string | null): void => {
|
|
27
|
+
try {
|
|
28
|
+
window.sessionStorage.setItem(
|
|
29
|
+
PAYMENT_PENDING_KEY,
|
|
30
|
+
JSON.stringify({ orderNo: orderNo || null, startedAt: Date.now() })
|
|
31
|
+
);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// sessionStorage may be unavailable (private mode, blocked storage)
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const getPaymentPending = (): PaymentPendingRecord | null => {
|
|
38
|
+
try {
|
|
39
|
+
const raw = window.sessionStorage.getItem(PAYMENT_PENDING_KEY);
|
|
40
|
+
if (!raw) return null;
|
|
41
|
+
|
|
42
|
+
const parsed = JSON.parse(raw) as PaymentPendingRecord;
|
|
43
|
+
if (
|
|
44
|
+
!parsed?.startedAt ||
|
|
45
|
+
Date.now() - parsed.startedAt > PAYMENT_PENDING_TTL_MS
|
|
46
|
+
) {
|
|
47
|
+
window.sessionStorage.removeItem(PAYMENT_PENDING_KEY);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return parsed;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const clearPaymentPending = (): void => {
|
|
58
|
+
try {
|
|
59
|
+
window.sessionStorage.removeItem(PAYMENT_PENDING_KEY);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// sessionStorage may be unavailable (private mode, blocked storage)
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
18
65
|
export const enhance3DSecureUrl = (originalUrl: string): string => {
|
|
19
66
|
try {
|
|
20
67
|
const baseUrl = window.location.origin;
|
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
setLocale,
|
|
16
16
|
setCurrency
|
|
17
17
|
} from '../redux/reducer';
|
|
18
|
+
import {
|
|
19
|
+
getPaymentPending,
|
|
20
|
+
clearPaymentPending
|
|
21
|
+
} from '../utils/payment-utils';
|
|
18
22
|
import type { CreditCardFormData } from '../utils/validation-schemas';
|
|
19
23
|
import type {
|
|
20
24
|
MasterpassRestOptionCustomRender,
|
|
@@ -223,6 +227,17 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
223
227
|
const [cvc, setCvc] = useState<string>('');
|
|
224
228
|
const [isProcessingPayment, setIsProcessingPayment] = useState(false);
|
|
225
229
|
|
|
230
|
+
// Initialized during render (not in an effect) so consumers see the real
|
|
231
|
+
// value before their own mount effects run.
|
|
232
|
+
const [isPaymentPending, setIsPaymentPending] = useState<boolean>(() =>
|
|
233
|
+
typeof window === 'undefined' ? false : Boolean(getPaymentPending())
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const dismissPaymentPending = useCallback(() => {
|
|
237
|
+
clearPaymentPending();
|
|
238
|
+
setIsPaymentPending(false);
|
|
239
|
+
}, []);
|
|
240
|
+
|
|
226
241
|
const [setMasterpassRestBinNumber] = useSetMasterpassRestBinNumberMutation();
|
|
227
242
|
|
|
228
243
|
const {
|
|
@@ -237,6 +252,7 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
237
252
|
handleInstallmentSelect,
|
|
238
253
|
processPayment,
|
|
239
254
|
processDirectPayment,
|
|
255
|
+
finalizePaymentAfterVerification,
|
|
240
256
|
openRewardModal,
|
|
241
257
|
closeRewardModal,
|
|
242
258
|
confirmRewards,
|
|
@@ -334,15 +350,20 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
334
350
|
|
|
335
351
|
useEffect(() => {
|
|
336
352
|
const error = getPosError();
|
|
353
|
+
|
|
337
354
|
if (error && Object.keys(error).length > 0) {
|
|
338
355
|
dispatch(setError(error));
|
|
356
|
+
|
|
357
|
+
// The completion request already ran and failed, so the order number it
|
|
358
|
+
// was bound to is settled and the flow may restart from scratch.
|
|
359
|
+
dismissPaymentPending();
|
|
339
360
|
}
|
|
340
361
|
|
|
341
362
|
return () => {
|
|
342
363
|
resetData();
|
|
343
364
|
dispatch(clearAllErrors());
|
|
344
365
|
};
|
|
345
|
-
}, [dispatch, resetData]);
|
|
366
|
+
}, [dispatch, resetData, dismissPaymentPending]);
|
|
346
367
|
|
|
347
368
|
const getErrorInfo = (error: any) => {
|
|
348
369
|
if (error && typeof error === 'object') {
|
|
@@ -427,7 +448,8 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
427
448
|
if (result.requiresOTP && result.otpType) {
|
|
428
449
|
updateModalState({
|
|
429
450
|
showOTPModal: true,
|
|
430
|
-
otpType: result.otpType
|
|
451
|
+
otpType: result.otpType,
|
|
452
|
+
otpContext: 'payment'
|
|
431
453
|
});
|
|
432
454
|
return;
|
|
433
455
|
}
|
|
@@ -453,12 +475,34 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
453
475
|
|
|
454
476
|
const result = await handleOTPSubmit(otp, mergedTexts);
|
|
455
477
|
|
|
478
|
+
// A payment-context OTP leaves the Masterpass transaction open; the backend
|
|
479
|
+
// only learns about it once the completion request is sent.
|
|
480
|
+
if (result?.success && result?.isPaymentContext) {
|
|
481
|
+
const finalizeResult = await finalizePaymentAfterVerification(
|
|
482
|
+
result.verificationData
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
if (!finalizeResult.success) {
|
|
486
|
+
dispatch(
|
|
487
|
+
setError(finalizeResult.message || mergedTexts.paymentFailedText)
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return finalizeResult;
|
|
492
|
+
}
|
|
493
|
+
|
|
456
494
|
return result;
|
|
457
|
-
}, [
|
|
495
|
+
}, [
|
|
496
|
+
handleOTPSubmit,
|
|
497
|
+
finalizePaymentAfterVerification,
|
|
498
|
+
dispatch,
|
|
499
|
+
mergedTexts
|
|
500
|
+
]);
|
|
458
501
|
|
|
459
502
|
const handleCardSelectWithErrorHandling = useCallback(async (card: any) => {
|
|
460
503
|
try {
|
|
461
504
|
dispatch(clearAllErrors());
|
|
505
|
+
dismissPaymentPending();
|
|
462
506
|
await handleCardSelect(card);
|
|
463
507
|
} catch (error) {
|
|
464
508
|
const errorMessage =
|
|
@@ -467,11 +511,12 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
467
511
|
: mergedTexts.failedToSelectCardText;
|
|
468
512
|
dispatch(setError(errorMessage));
|
|
469
513
|
}
|
|
470
|
-
}, [dispatch, handleCardSelect, mergedTexts.failedToSelectCardText]);
|
|
514
|
+
}, [dispatch, dismissPaymentPending, handleCardSelect, mergedTexts.failedToSelectCardText]);
|
|
471
515
|
|
|
472
516
|
const handleInstallmentSelectWithErrorHandling = useCallback(async (installment: any) => {
|
|
473
517
|
try {
|
|
474
518
|
dispatch(clearAllErrors());
|
|
519
|
+
dismissPaymentPending();
|
|
475
520
|
await handleInstallmentSelect(installment);
|
|
476
521
|
} catch (error) {
|
|
477
522
|
const errorMessage =
|
|
@@ -480,7 +525,7 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
480
525
|
: mergedTexts.failedToSelectInstallmentText;
|
|
481
526
|
dispatch(setError(errorMessage));
|
|
482
527
|
}
|
|
483
|
-
}, [dispatch, handleInstallmentSelect, mergedTexts.failedToSelectInstallmentText]);
|
|
528
|
+
}, [dispatch, dismissPaymentPending, handleInstallmentSelect, mergedTexts.failedToSelectInstallmentText]);
|
|
484
529
|
|
|
485
530
|
const handleConfirmRewards = useCallback(
|
|
486
531
|
async (selected: RewardItem[]) => {
|
|
@@ -500,6 +545,7 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
500
545
|
}, [updatePaymentState]);
|
|
501
546
|
|
|
502
547
|
const handlePaymentMethodChange = useCallback((method: 'stored_card' | 'new_card') => {
|
|
548
|
+
dismissPaymentPending();
|
|
503
549
|
setPaymentMethod(method);
|
|
504
550
|
updatePaymentState({
|
|
505
551
|
selectedCard: null,
|
|
@@ -511,7 +557,7 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
511
557
|
setCvc('');
|
|
512
558
|
dispatch(setLastCheckedBin(''));
|
|
513
559
|
dispatch(clearNewCardFormData());
|
|
514
|
-
}, [dispatch, updatePaymentState]);
|
|
560
|
+
}, [dispatch, dismissPaymentPending, updatePaymentState]);
|
|
515
561
|
|
|
516
562
|
const customRenderRef = useRef(customRender);
|
|
517
563
|
customRenderRef.current = customRender;
|
|
@@ -564,6 +610,8 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
564
610
|
shouldShowDirectForm,
|
|
565
611
|
cvc,
|
|
566
612
|
error,
|
|
613
|
+
isPaymentPending,
|
|
614
|
+
dismissPaymentPending,
|
|
567
615
|
|
|
568
616
|
accountData,
|
|
569
617
|
accountStatus,
|
|
@@ -606,6 +654,8 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
606
654
|
shouldShowDirectForm,
|
|
607
655
|
cvc,
|
|
608
656
|
error,
|
|
657
|
+
isPaymentPending,
|
|
658
|
+
dismissPaymentPending,
|
|
609
659
|
accountData,
|
|
610
660
|
accountStatus,
|
|
611
661
|
modalState,
|