@akinon/pz-masterpass-rest 2.0.56-rc.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 +2 -6
- 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 +5 -2
- package/src/components/credit-card-form.tsx +1 -1
- package/src/components/installment-list.tsx +1 -1
- package/src/components/otp-modal.tsx +4 -12
- 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 +36 -27
- package/src/hooks/useMasterpassPayment.ts +75 -11
- package/src/hooks/useMasterpassToken.ts +13 -50
- package/src/index.ts +0 -1
- 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/card-utils.ts +1 -1
- 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
- package/src/utils/wait-for-utils-ready.ts +0 -64
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { renderHook, act } from '@testing-library/react';
|
|
3
|
+
import { Provider } from 'react-redux';
|
|
4
|
+
import { configureStore } from '@reduxjs/toolkit';
|
|
5
|
+
|
|
6
|
+
import masterpassRestReducer from '../redux/reducer';
|
|
7
|
+
import { getPaymentPending, setPaymentPending } from '../utils/payment-utils';
|
|
8
|
+
|
|
9
|
+
const prepareMock = jest.fn();
|
|
10
|
+
const finalizeMock = jest.fn();
|
|
11
|
+
const binNumberMock = jest.fn();
|
|
12
|
+
const installmentMock = jest.fn();
|
|
13
|
+
const processPaymentMock = jest.fn();
|
|
14
|
+
const directPaymentMock = jest.fn();
|
|
15
|
+
const handleResponseMock = jest.fn();
|
|
16
|
+
|
|
17
|
+
const asMutation = (fn: jest.Mock) => () =>
|
|
18
|
+
[(arg: unknown) => ({ unwrap: () => fn(arg) }), { isLoading: false }];
|
|
19
|
+
|
|
20
|
+
jest.mock('../redux/api', () => ({
|
|
21
|
+
usePrepareMasterpassOrderMutation: asMutation(prepareMock),
|
|
22
|
+
useFinalizeMasterpassOrderMutation: asMutation(finalizeMock),
|
|
23
|
+
useSetMasterpassRestBinNumberMutation: asMutation(binNumberMock),
|
|
24
|
+
useCheckoutMasterpassInstallmentMutation: asMutation(installmentMock),
|
|
25
|
+
useQueryMasterpassRewardsMutation: asMutation(jest.fn()),
|
|
26
|
+
useSelectMasterpassRewardsMutation: asMutation(jest.fn())
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
jest.mock('../services/payment', () => ({
|
|
30
|
+
PaymentService: class {
|
|
31
|
+
processPayment = processPaymentMock;
|
|
32
|
+
directPayment = directPaymentMock;
|
|
33
|
+
}
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
jest.mock('../utils/response-handler', () => ({
|
|
37
|
+
handleMasterpassResponse: (...args: unknown[]) => handleResponseMock(...args)
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
jest.mock('./useMasterpassToken', () => ({
|
|
41
|
+
useMasterpassToken: () => ({
|
|
42
|
+
refetch: jest.fn(async () => ({ data: { token: 'fresh-token' } }))
|
|
43
|
+
})
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// Imported after the mocks so the hook picks them up.
|
|
47
|
+
import { useMasterpassPayment } from './useMasterpassPayment';
|
|
48
|
+
|
|
49
|
+
const PREPARED_ORDER = {
|
|
50
|
+
pre_order: { total_amount_with_interest: '100.00' },
|
|
51
|
+
context_list: [{ page_context: { order_no: 'ORDER-1', extras: {} } }]
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const SELECTED_CARD = { cardAlias: 'my card', cardBin: '454671' };
|
|
55
|
+
const SELECTED_INSTALLMENT = { pk: 1, installment_count: 1 };
|
|
56
|
+
|
|
57
|
+
const NEW_CARD_FORM = {
|
|
58
|
+
cardNumber: '4546 7112 3456 7890',
|
|
59
|
+
cardholderName: 'Test User',
|
|
60
|
+
expiryDate: '12/30',
|
|
61
|
+
cvv: '123'
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const renderPaymentHook = ({ withNewCardForm = false } = {}) => {
|
|
65
|
+
const store = configureStore({
|
|
66
|
+
reducer: { masterpassRest: masterpassRestReducer },
|
|
67
|
+
middleware: (getDefault) => getDefault({ serializableCheck: false })
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
store.dispatch({
|
|
71
|
+
type: 'masterpassRest/updatePaymentState',
|
|
72
|
+
payload: {
|
|
73
|
+
selectedCard: SELECTED_CARD,
|
|
74
|
+
selectedInstallment: SELECTED_INSTALLMENT,
|
|
75
|
+
cvc: '123',
|
|
76
|
+
useThreeD: true
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (withNewCardForm) {
|
|
81
|
+
store.dispatch({
|
|
82
|
+
type: 'masterpassRest/setNewCardFormData',
|
|
83
|
+
payload: NEW_CARD_FORM
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
88
|
+
<Provider store={store}>{children}</Provider>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return { ...renderHook(() => useMasterpassPayment(), { wrapper }), store };
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
prepareMock.mockResolvedValue(PREPARED_ORDER);
|
|
96
|
+
finalizeMock.mockResolvedValue({ errors: null });
|
|
97
|
+
processPaymentMock.mockResolvedValue({ success: true, data: { result: {} } });
|
|
98
|
+
directPaymentMock.mockResolvedValue({ success: true, data: { result: {} } });
|
|
99
|
+
handleResponseMock.mockResolvedValue({ success: true });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('order number lifecycle during payment', () => {
|
|
103
|
+
it('marks the order number pending as soon as it is bound to a transaction', async () => {
|
|
104
|
+
// The SDK call never resolves, so the assertion sees the state the user is
|
|
105
|
+
// in while the bank page is open.
|
|
106
|
+
processPaymentMock.mockImplementation(() => new Promise(() => {}));
|
|
107
|
+
|
|
108
|
+
const { result } = renderPaymentHook();
|
|
109
|
+
|
|
110
|
+
act(() => {
|
|
111
|
+
void result.current.processPayment();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await act(async () => {
|
|
115
|
+
await Promise.resolve();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(getPaymentPending()?.orderNo).toBe('ORDER-1');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('releases the order number once the backend has completed the order', async () => {
|
|
122
|
+
const { result } = renderPaymentHook();
|
|
123
|
+
|
|
124
|
+
await act(async () => {
|
|
125
|
+
await result.current.processPayment();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(finalizeMock).toHaveBeenCalled();
|
|
129
|
+
expect(getPaymentPending()).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('keeps the order number pending while the shopper is redirected to 3D', async () => {
|
|
133
|
+
handleResponseMock.mockResolvedValue({
|
|
134
|
+
requires3D: true,
|
|
135
|
+
redirectUrl: 'https://bank.example/3d'
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// jsdom refuses real navigation; the assignment itself is not under test.
|
|
139
|
+
const location = window.location;
|
|
140
|
+
delete (window as any).location;
|
|
141
|
+
(window as any).location = { ...location, href: '' };
|
|
142
|
+
|
|
143
|
+
const { result } = renderPaymentHook();
|
|
144
|
+
|
|
145
|
+
let outcome: any;
|
|
146
|
+
await act(async () => {
|
|
147
|
+
outcome = await result.current.processPayment();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(outcome.requiresRedirect).toBe(true);
|
|
151
|
+
expect(getPaymentPending()?.orderNo).toBe('ORDER-1');
|
|
152
|
+
|
|
153
|
+
(window as any).location = location;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('keeps the order number pending while a verification step is open', async () => {
|
|
157
|
+
handleResponseMock.mockResolvedValue({
|
|
158
|
+
success: false,
|
|
159
|
+
requiresOTP: true,
|
|
160
|
+
otpType: 'OTP'
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const { result } = renderPaymentHook();
|
|
164
|
+
|
|
165
|
+
await act(async () => {
|
|
166
|
+
await result.current.processPayment();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(getPaymentPending()?.orderNo).toBe('ORDER-1');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('releases the order number when the payment is rejected outright', async () => {
|
|
173
|
+
handleResponseMock.mockResolvedValue({
|
|
174
|
+
success: false,
|
|
175
|
+
message: 'Insufficient funds'
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const { result } = renderPaymentHook();
|
|
179
|
+
|
|
180
|
+
await act(async () => {
|
|
181
|
+
await result.current.processPayment();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(getPaymentPending()).toBeNull();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('releases the order number when the flow throws', async () => {
|
|
188
|
+
prepareMock.mockRejectedValue(new Error('network down'));
|
|
189
|
+
setPaymentPending('STALE-ORDER');
|
|
190
|
+
|
|
191
|
+
const { result } = renderPaymentHook();
|
|
192
|
+
|
|
193
|
+
await act(async () => {
|
|
194
|
+
await result.current.processPayment();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(getPaymentPending()).toBeNull();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('marks the order number pending on the new-card flow too', async () => {
|
|
201
|
+
directPaymentMock.mockImplementation(() => new Promise(() => {}));
|
|
202
|
+
|
|
203
|
+
const { result } = renderPaymentHook({ withNewCardForm: true });
|
|
204
|
+
|
|
205
|
+
act(() => {
|
|
206
|
+
void result.current.processDirectPayment();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await act(async () => {
|
|
210
|
+
await Promise.resolve();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(getPaymentPending()?.orderNo).toBe('ORDER-1');
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('completing a payment after a verification step', () => {
|
|
218
|
+
it('sends the verification result to the backend and releases the order number', async () => {
|
|
219
|
+
setPaymentPending('ORDER-1');
|
|
220
|
+
|
|
221
|
+
const { result } = renderPaymentHook();
|
|
222
|
+
|
|
223
|
+
let outcome: any;
|
|
224
|
+
await act(async () => {
|
|
225
|
+
outcome = await result.current.finalizePaymentAfterVerification({
|
|
226
|
+
result: { responseCode: '0000', token: 'verified-token' }
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
expect(finalizeMock).toHaveBeenCalledWith({
|
|
231
|
+
responseCode: '0000',
|
|
232
|
+
token: 'verified-token',
|
|
233
|
+
three_d_secure: true,
|
|
234
|
+
transactionType: 'PURCHASE_3D'
|
|
235
|
+
});
|
|
236
|
+
expect(outcome.success).toBe(true);
|
|
237
|
+
expect(getPaymentPending()).toBeNull();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('surfaces backend errors instead of reporting success', async () => {
|
|
241
|
+
finalizeMock.mockResolvedValue({ errors: { non_field_errors: 'ORDER_NO_MISMATCH' } });
|
|
242
|
+
|
|
243
|
+
const { result } = renderPaymentHook();
|
|
244
|
+
|
|
245
|
+
let outcome: any;
|
|
246
|
+
await act(async () => {
|
|
247
|
+
outcome = await result.current.finalizePaymentAfterVerification({
|
|
248
|
+
result: { responseCode: '0000', token: 'verified-token' }
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(outcome.success).toBe(false);
|
|
253
|
+
expect(outcome.message).toEqual({ non_field_errors: 'ORDER_NO_MISMATCH' });
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -19,12 +19,12 @@ 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
|
|
25
26
|
} from '../types/account.types';
|
|
26
27
|
import { useMasterpassToken } from './useMasterpassToken';
|
|
27
|
-
import { decodeMasterpassToken } from '../utils/wait-for-utils-ready';
|
|
28
28
|
|
|
29
29
|
export const useMasterpassAccount = () => {
|
|
30
30
|
const dispatch = useDispatch();
|
|
@@ -41,16 +41,7 @@ export const useMasterpassAccount = () => {
|
|
|
41
41
|
|
|
42
42
|
const initializeAccount = useCallback(
|
|
43
43
|
async (newTokenData: any, rawToken?: string) => {
|
|
44
|
-
if (!newTokenData?.AccountKey)
|
|
45
|
-
dispatch(
|
|
46
|
-
setAccountStatus({
|
|
47
|
-
isAccountNotFound: false,
|
|
48
|
-
isAccountNotLinked: true,
|
|
49
|
-
shouldShowDirectForm: true
|
|
50
|
-
})
|
|
51
|
-
);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
44
|
+
if (!newTokenData?.AccountKey) return;
|
|
54
45
|
|
|
55
46
|
const accountService = new AccountService(rawToken || token, newTokenData?.MerchantId);
|
|
56
47
|
|
|
@@ -136,19 +127,13 @@ export const useMasterpassAccount = () => {
|
|
|
136
127
|
dispatch(resetState());
|
|
137
128
|
}, [dispatch]);
|
|
138
129
|
|
|
139
|
-
const refreshToken = async (options?: {
|
|
140
|
-
three_d?: boolean;
|
|
141
|
-
skipReset?: boolean;
|
|
142
|
-
}) => {
|
|
130
|
+
const refreshToken = async (options?: { three_d?: boolean; skipReset?: boolean }) => {
|
|
143
131
|
if (options?.three_d !== false && !options?.skipReset) {
|
|
144
132
|
resetData();
|
|
145
133
|
}
|
|
146
134
|
const result = await refetch(options);
|
|
147
135
|
if (result?.data?.token) {
|
|
148
|
-
const decodedToken =
|
|
149
|
-
if (!decodedToken) {
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
136
|
+
const decodedToken = window.Utils.decodeJwt(result.data.token);
|
|
152
137
|
return {
|
|
153
138
|
tokenData: decodedToken,
|
|
154
139
|
token: result.data.token,
|
|
@@ -202,6 +187,7 @@ export const useMasterpassAccount = () => {
|
|
|
202
187
|
dispatch(
|
|
203
188
|
updateModalState({
|
|
204
189
|
otpType: result.otpType,
|
|
190
|
+
otpContext: 'account',
|
|
205
191
|
verificationData: result.data,
|
|
206
192
|
showLinkModal: false,
|
|
207
193
|
showOTPModal: true
|
|
@@ -221,9 +207,22 @@ export const useMasterpassAccount = () => {
|
|
|
221
207
|
const handleOTPSubmit = useCallback(
|
|
222
208
|
async (otp: string, texts?: any) => {
|
|
223
209
|
const result = await handleVerification(otp, token, tokenData?.MerchantId);
|
|
210
|
+
const isPaymentContext = modalState.otpContext === 'payment';
|
|
224
211
|
|
|
225
212
|
if (result.success) {
|
|
226
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
|
+
|
|
227
226
|
await refreshAccountData();
|
|
228
227
|
return { success: true };
|
|
229
228
|
} else if (result.sessionExpired) {
|
|
@@ -234,12 +233,8 @@ export const useMasterpassAccount = () => {
|
|
|
234
233
|
informationModalData: {
|
|
235
234
|
type: 'warning',
|
|
236
235
|
title: texts?.sessionExpiredTitle || 'Session Expired',
|
|
237
|
-
message:
|
|
238
|
-
|
|
239
|
-
'Your session has expired due to inactivity. Please restart the process to continue.',
|
|
240
|
-
secondaryMessage:
|
|
241
|
-
texts?.sessionExpiredSecondaryMessage ||
|
|
242
|
-
'For security reasons, verification codes are only valid for a limited time.',
|
|
236
|
+
message: texts?.sessionExpiredMessage || 'Your session has expired due to inactivity. Please restart the process to continue.',
|
|
237
|
+
secondaryMessage: texts?.sessionExpiredSecondaryMessage || 'For security reasons, verification codes are only valid for a limited time.',
|
|
243
238
|
buttonText: texts?.sessionExpiredButton || 'Start Again'
|
|
244
239
|
}
|
|
245
240
|
})
|
|
@@ -250,6 +245,13 @@ export const useMasterpassAccount = () => {
|
|
|
250
245
|
};
|
|
251
246
|
} else if (result.requires3D && result.redirectUrl) {
|
|
252
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
|
+
|
|
253
255
|
window.location.href = result.redirectUrl;
|
|
254
256
|
return {
|
|
255
257
|
success: false,
|
|
@@ -277,7 +279,13 @@ export const useMasterpassAccount = () => {
|
|
|
277
279
|
};
|
|
278
280
|
}
|
|
279
281
|
},
|
|
280
|
-
[
|
|
282
|
+
[
|
|
283
|
+
dispatch,
|
|
284
|
+
refreshAccountData,
|
|
285
|
+
token,
|
|
286
|
+
tokenData?.MerchantId,
|
|
287
|
+
modalState.otpContext
|
|
288
|
+
]
|
|
281
289
|
);
|
|
282
290
|
|
|
283
291
|
const handleRemoveCard = useCallback(
|
|
@@ -355,7 +363,7 @@ export const useMasterpassAccount = () => {
|
|
|
355
363
|
const activeMerchantId = freshResult?.merchantId || tokenData?.MerchantId;
|
|
356
364
|
|
|
357
365
|
if (!activeTokenData?.AccountKey) {
|
|
358
|
-
|
|
366
|
+
throw new Error('Token data not available');
|
|
359
367
|
}
|
|
360
368
|
|
|
361
369
|
const accountService = new AccountService(activeToken, activeMerchantId);
|
|
@@ -388,6 +396,7 @@ export const useMasterpassAccount = () => {
|
|
|
388
396
|
dispatch(
|
|
389
397
|
updateModalState({
|
|
390
398
|
otpType: result.otpType,
|
|
399
|
+
otpContext: 'account',
|
|
391
400
|
verificationData: result.data,
|
|
392
401
|
showLinkModal: false,
|
|
393
402
|
showOTPModal: true
|
|
@@ -25,11 +25,12 @@ import {
|
|
|
25
25
|
} from '../redux/reducer';
|
|
26
26
|
import { useMasterpassToken } from './useMasterpassToken';
|
|
27
27
|
import { PaymentService } from '../services/payment';
|
|
28
|
-
import { decodeMasterpassToken } from '../utils/wait-for-utils-ready';
|
|
29
28
|
import {
|
|
30
29
|
createPaymentRequest,
|
|
31
30
|
createDirectPaymentRequest,
|
|
32
|
-
getTransactionType
|
|
31
|
+
getTransactionType,
|
|
32
|
+
setPaymentPending,
|
|
33
|
+
clearPaymentPending
|
|
33
34
|
} from '../utils/payment-utils';
|
|
34
35
|
import { handleMasterpassResponse } from '../utils/response-handler';
|
|
35
36
|
import type { Installment, RewardItem } from '../types/payment.types';
|
|
@@ -94,10 +95,7 @@ export const useMasterpassPayment = (
|
|
|
94
95
|
const refreshTokenForPayment = useCallback(async () => {
|
|
95
96
|
const result = await refetchToken({ skipReset: true });
|
|
96
97
|
if (result?.data?.token) {
|
|
97
|
-
const decodedToken =
|
|
98
|
-
if (!decodedToken) {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
98
|
+
const decodedToken = window.Utils.decodeJwt(result.data.token);
|
|
101
99
|
return {
|
|
102
100
|
tokenData: decodedToken,
|
|
103
101
|
token: result.data.token,
|
|
@@ -252,13 +250,20 @@ export const useMasterpassPayment = (
|
|
|
252
250
|
|
|
253
251
|
dispatch(setOrderData(prepareResult));
|
|
254
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
|
+
|
|
255
260
|
const paymentService = new PaymentService(activeToken, activeMerchantId);
|
|
256
261
|
const paymentRequest = createPaymentRequest({
|
|
257
262
|
cvc,
|
|
258
263
|
cardAlias: selectedCard.cardAlias,
|
|
259
264
|
accountKey: activeTokenData.AccountKey,
|
|
260
265
|
amount: prepareResult.pre_order?.total_amount_with_interest,
|
|
261
|
-
orderNo
|
|
266
|
+
orderNo,
|
|
262
267
|
currencyCode: currency,
|
|
263
268
|
installmentCount: selectedInstallment.installment_count,
|
|
264
269
|
authenticationMethod: activeTokenData.AuthenticationMethod,
|
|
@@ -288,6 +293,7 @@ export const useMasterpassPayment = (
|
|
|
288
293
|
}).unwrap();
|
|
289
294
|
|
|
290
295
|
dispatch(setOrderData(finalizeResult));
|
|
296
|
+
clearPaymentPending();
|
|
291
297
|
|
|
292
298
|
if (finalizeResult.errors) {
|
|
293
299
|
return { success: false, message: finalizeResult.errors };
|
|
@@ -297,6 +303,10 @@ export const useMasterpassPayment = (
|
|
|
297
303
|
return { success: true };
|
|
298
304
|
}
|
|
299
305
|
|
|
306
|
+
if (!response.requiresOTP) {
|
|
307
|
+
clearPaymentPending();
|
|
308
|
+
}
|
|
309
|
+
|
|
300
310
|
return {
|
|
301
311
|
success: response.success,
|
|
302
312
|
requiresOTP: response.requiresOTP,
|
|
@@ -304,6 +314,7 @@ export const useMasterpassPayment = (
|
|
|
304
314
|
message: response.message
|
|
305
315
|
};
|
|
306
316
|
} catch (error) {
|
|
317
|
+
clearPaymentPending();
|
|
307
318
|
return {
|
|
308
319
|
success: false,
|
|
309
320
|
message:
|
|
@@ -351,12 +362,23 @@ export const useMasterpassPayment = (
|
|
|
351
362
|
const activeToken = freshResult?.token;
|
|
352
363
|
const activeMerchantId = freshResult?.merchantId || tokenData?.MerchantId;
|
|
353
364
|
|
|
365
|
+
if (!activeTokenData?.AccountKey) {
|
|
366
|
+
return { success: false, message: 'Token data not available' };
|
|
367
|
+
}
|
|
368
|
+
|
|
354
369
|
const prepareResult = await prepareMasterpassOrder({
|
|
355
370
|
use_three_d: paymentState.useThreeD
|
|
356
371
|
}).unwrap();
|
|
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,
|
|
@@ -364,13 +386,13 @@ export const useMasterpassPayment = (
|
|
|
364
386
|
cardHolderName: newCardFormData.cardholderName || '',
|
|
365
387
|
cardAlias: newCardFormData.cardAlias || '',
|
|
366
388
|
expiryDate: newCardFormData.expiryDate,
|
|
367
|
-
accountKey: activeTokenData
|
|
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
|
-
authenticationMethod: activeTokenData
|
|
373
|
-
secure3DModel: activeTokenData
|
|
394
|
+
authenticationMethod: activeTokenData.AuthenticationMethod,
|
|
395
|
+
secure3DModel: activeTokenData.Secure3dType,
|
|
374
396
|
terminalGroupId,
|
|
375
397
|
acquirerIcaNumber: bankIca,
|
|
376
398
|
additionalFields:
|
|
@@ -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,
|
|
@@ -14,10 +14,6 @@ import {
|
|
|
14
14
|
updatePaymentState
|
|
15
15
|
} from '../redux/reducer';
|
|
16
16
|
import { isTokenExpired } from '../utils/payment-utils';
|
|
17
|
-
import {
|
|
18
|
-
waitForUtilsReady,
|
|
19
|
-
decodeMasterpassToken
|
|
20
|
-
} from '../utils/wait-for-utils-ready';
|
|
21
17
|
|
|
22
18
|
interface UseMasterpassTokenProps {
|
|
23
19
|
useThreeD: boolean;
|
|
@@ -69,37 +65,12 @@ export const useMasterpassToken = ({
|
|
|
69
65
|
return;
|
|
70
66
|
}
|
|
71
67
|
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const token = data.token;
|
|
77
|
-
const extras = data.extras;
|
|
78
|
-
let cancelled = false;
|
|
79
|
-
|
|
80
|
-
// The token request can resolve before the Masterpass SDK script has
|
|
81
|
-
// loaded (common on slow/3G networks). Wait for `window.Utils.decodeJwt`
|
|
82
|
-
// to exist before decoding, otherwise the checkout crashes with
|
|
83
|
-
// `Cannot read properties of undefined (reading 'decodeJwt')`.
|
|
84
|
-
//
|
|
85
|
-
// Redux writes are deferred until after the wait resolves: dispatching
|
|
86
|
-
// `setMasterpassRestToken` synchronously would change the `reduxToken`
|
|
87
|
-
// dependency, re-run this effect and cancel the in-flight decode.
|
|
88
|
-
(async () => {
|
|
89
|
-
const ready = await waitForUtilsReady();
|
|
90
|
-
if (cancelled) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
dispatch(setMasterpassRestToken(token));
|
|
95
|
-
dispatch(setMasterpassTerminalGroupId(extras.terminal_group_id));
|
|
96
|
-
dispatch(setMasterpassBankIca(extras.bank_ica));
|
|
97
|
-
|
|
98
|
-
const decodedToken = ready ? decodeMasterpassToken(token) : undefined;
|
|
99
|
-
if (!decodedToken) {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
68
|
+
if (data?.token && data.token !== reduxToken) {
|
|
69
|
+
dispatch(setMasterpassRestToken(data.token));
|
|
70
|
+
dispatch(setMasterpassTerminalGroupId(data.extras.terminal_group_id));
|
|
71
|
+
dispatch(setMasterpassBankIca(data.extras.bank_ica));
|
|
102
72
|
|
|
73
|
+
const decodedToken = window.Utils.decodeJwt(data.token);
|
|
103
74
|
dispatch(setMasterpassRestTokenData(decodedToken));
|
|
104
75
|
|
|
105
76
|
if (decodedToken?.AuthenticationMethod?.toLowerCase() === 'none') {
|
|
@@ -107,11 +78,7 @@ export const useMasterpassToken = ({
|
|
|
107
78
|
}
|
|
108
79
|
|
|
109
80
|
stableOnTokenReady(decodedToken);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return () => {
|
|
113
|
-
cancelled = true;
|
|
114
|
-
};
|
|
81
|
+
}
|
|
115
82
|
}, [data?.token, reduxToken, dispatch, stableOnTokenReady]);
|
|
116
83
|
|
|
117
84
|
const refetchToken = useCallback(
|
|
@@ -127,19 +94,15 @@ export const useMasterpassToken = ({
|
|
|
127
94
|
);
|
|
128
95
|
dispatch(setMasterpassBankIca(result.data.extras.bank_ica));
|
|
129
96
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (decodedToken) {
|
|
134
|
-
dispatch(setMasterpassRestTokenData(decodedToken));
|
|
97
|
+
const decodedToken = window.Utils.decodeJwt(result.data.token);
|
|
98
|
+
dispatch(setMasterpassRestTokenData(decodedToken));
|
|
135
99
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
100
|
+
if (decodedToken?.AuthenticationMethod?.toLowerCase() === 'none') {
|
|
101
|
+
dispatch(updatePaymentState({ useThreeD: false }));
|
|
102
|
+
}
|
|
139
103
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
104
|
+
if (options?.three_d !== false && !options?.skipReset) {
|
|
105
|
+
stableOnTokenReady(decodedToken);
|
|
143
106
|
}
|
|
144
107
|
}
|
|
145
108
|
|
package/src/index.ts
CHANGED
|
@@ -19,7 +19,6 @@ export * from './utils/reward-utils';
|
|
|
19
19
|
export * from './utils/card-utils';
|
|
20
20
|
export * from './utils/validation-schemas';
|
|
21
21
|
export * from './utils/masterpass-sdk';
|
|
22
|
-
export * from './utils/wait-for-utils-ready';
|
|
23
22
|
export type { MasterpassEnvironment } from './utils/constants';
|
|
24
23
|
|
|
25
24
|
export * from './types/payment.types';
|