@akinon/pz-masterpass-rest 1.126.7 → 1.126.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinon/pz-masterpass-rest",
3
- "version": "1.126.7",
3
+ "version": "1.126.8",
4
4
  "main": "src/index.ts",
5
5
  "types": "src/index.d.ts",
6
6
  "description": "Modern React-based Masterpass REST API integration package for ProjectZero e-commerce platform",
@@ -10,6 +10,9 @@
10
10
  "docs",
11
11
  "README.md"
12
12
  ],
13
+ "scripts": {
14
+ "test": "jest"
15
+ },
13
16
  "keywords": [
14
17
  "masterpass",
15
18
  "payment",
@@ -0,0 +1,189 @@
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 handleVerificationMock = jest.fn();
10
+ const accountAccessMock = jest.fn();
11
+
12
+ jest.mock('../utils/response-handler', () => ({
13
+ handleVerification: (...args: unknown[]) => handleVerificationMock(...args),
14
+ handleMasterpassResponse: jest.fn(async (response) => response)
15
+ }));
16
+
17
+ jest.mock('../services/account', () => ({
18
+ AccountService: class {
19
+ accountAccess = accountAccessMock;
20
+ linkToMerchant = jest.fn();
21
+ removeCard = jest.fn();
22
+ addCard = jest.fn();
23
+ }
24
+ }));
25
+
26
+ jest.mock('./useMasterpassToken', () => ({
27
+ useMasterpassToken: () => ({
28
+ refetch: jest.fn(async () => ({ data: { token: 'fresh-token' } }))
29
+ })
30
+ }));
31
+
32
+ // Imported after the mocks so the hook picks them up.
33
+ import { useMasterpassAccount } from './useMasterpassAccount';
34
+
35
+ const renderAccountHook = (otpContext: 'account' | 'payment') => {
36
+ const store = configureStore({
37
+ reducer: { masterpassRest: masterpassRestReducer },
38
+ middleware: (getDefault) => getDefault({ serializableCheck: false })
39
+ });
40
+
41
+ store.dispatch({
42
+ type: 'masterpassRest/setMasterpassRestTokenData',
43
+ payload: {
44
+ MerchantId: 'merchant-1',
45
+ AccountKey: 'account-1',
46
+ UserId: 'user-1'
47
+ }
48
+ });
49
+
50
+ store.dispatch({
51
+ type: 'masterpassRest/updateModalState',
52
+ payload: { otpContext, showOTPModal: true }
53
+ });
54
+
55
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
56
+ <Provider store={store}>{children}</Provider>
57
+ );
58
+
59
+ return { ...renderHook(() => useMasterpassAccount(), { wrapper }), store };
60
+ };
61
+
62
+ beforeEach(() => {
63
+ accountAccessMock.mockResolvedValue({
64
+ statusCode: 200,
65
+ result: { cards: [] }
66
+ });
67
+ });
68
+
69
+ describe('verification during a payment', () => {
70
+ it('hands the verification result back so the caller can complete the order', async () => {
71
+ handleVerificationMock.mockResolvedValue({
72
+ success: true,
73
+ data: { result: { responseCode: '0000', token: 'verified-token' } }
74
+ });
75
+
76
+ const { result } = renderAccountHook('payment');
77
+
78
+ let outcome: any;
79
+ await act(async () => {
80
+ outcome = await result.current.handleOTPSubmit('123456');
81
+ });
82
+
83
+ expect(outcome).toEqual({
84
+ success: true,
85
+ isPaymentContext: true,
86
+ verificationData: { result: { responseCode: '0000', token: 'verified-token' } }
87
+ });
88
+ });
89
+
90
+ it('does not treat a refreshed card list as a completed payment', async () => {
91
+ handleVerificationMock.mockResolvedValue({
92
+ success: true,
93
+ data: { result: { responseCode: '0000', token: 'verified-token' } }
94
+ });
95
+
96
+ const { result } = renderAccountHook('payment');
97
+ accountAccessMock.mockClear();
98
+
99
+ await act(async () => {
100
+ await result.current.handleOTPSubmit('123456');
101
+ });
102
+
103
+ // Refreshing the account here used to be the only thing that happened, which
104
+ // left the order uncompleted on the backend.
105
+ expect(accountAccessMock).not.toHaveBeenCalled();
106
+ });
107
+
108
+ it('keeps the order number pending when verification sends the shopper to 3D', async () => {
109
+ setPaymentPending('ORDER-1');
110
+ handleVerificationMock.mockResolvedValue({
111
+ requires3D: true,
112
+ redirectUrl: 'https://bank.example/3d'
113
+ });
114
+
115
+ const location = window.location;
116
+ delete (window as any).location;
117
+ (window as any).location = { ...location, href: '' };
118
+
119
+ const { result } = renderAccountHook('payment');
120
+
121
+ await act(async () => {
122
+ await result.current.handleOTPSubmit('123456');
123
+ });
124
+
125
+ expect(getPaymentPending()?.orderNo).toBe('ORDER-1');
126
+
127
+ (window as any).location = location;
128
+ });
129
+ });
130
+
131
+ describe('verification during an account operation', () => {
132
+ it('refreshes the saved cards and reports plain success', async () => {
133
+ handleVerificationMock.mockResolvedValue({
134
+ success: true,
135
+ data: { result: { responseCode: '0000' } }
136
+ });
137
+
138
+ const { result } = renderAccountHook('account');
139
+ accountAccessMock.mockClear();
140
+
141
+ let outcome: any;
142
+ await act(async () => {
143
+ outcome = await result.current.handleOTPSubmit('123456');
144
+ });
145
+
146
+ expect(outcome).toEqual({ success: true });
147
+ expect(accountAccessMock).toHaveBeenCalled();
148
+ });
149
+
150
+ it('leaves the pending record untouched on a 3D redirect', async () => {
151
+ setPaymentPending('ORDER-1');
152
+ const before = getPaymentPending()?.startedAt;
153
+
154
+ handleVerificationMock.mockResolvedValue({
155
+ requires3D: true,
156
+ redirectUrl: 'https://bank.example/3d'
157
+ });
158
+
159
+ const location = window.location;
160
+ delete (window as any).location;
161
+ (window as any).location = { ...location, href: '' };
162
+
163
+ const { result } = renderAccountHook('account');
164
+
165
+ await act(async () => {
166
+ await result.current.handleOTPSubmit('123456');
167
+ });
168
+
169
+ expect(getPaymentPending()?.startedAt).toBe(before);
170
+
171
+ (window as any).location = location;
172
+ });
173
+ });
174
+
175
+ describe('the default verification context', () => {
176
+ it('is the account flow, so an unset context never skips the card refresh', async () => {
177
+ handleVerificationMock.mockResolvedValue({
178
+ success: true,
179
+ data: { result: {} }
180
+ });
181
+
182
+ const store = configureStore({
183
+ reducer: { masterpassRest: masterpassRestReducer },
184
+ middleware: (getDefault) => getDefault({ serializableCheck: false })
185
+ });
186
+
187
+ expect(store.getState().masterpassRest.modalState.otpContext).toBe('account');
188
+ });
189
+ });
@@ -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,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
@@ -190,6 +191,7 @@ export const useMasterpassAccount = () => {
190
191
  dispatch(
191
192
  updateModalState({
192
193
  otpType: result.otpType,
194
+ otpContext: 'account',
193
195
  verificationData: result.data,
194
196
  showLinkModal: false,
195
197
  showOTPModal: true
@@ -209,9 +211,22 @@ export const useMasterpassAccount = () => {
209
211
  const handleOTPSubmit = useCallback(
210
212
  async (otp: string, texts?: any) => {
211
213
  const result = await handleVerification(otp, token, tokenData?.MerchantId);
214
+ const isPaymentContext = modalState.otpContext === 'payment';
212
215
 
213
216
  if (result.success) {
214
217
  dispatch(setShowOTPModal(false));
218
+
219
+ // A payment-context OTP still has an open Masterpass transaction. The
220
+ // caller has to complete it against the backend; refreshing the account
221
+ // here would leave the order unfinalized.
222
+ if (isPaymentContext) {
223
+ return {
224
+ success: true,
225
+ isPaymentContext: true,
226
+ verificationData: result.data
227
+ };
228
+ }
229
+
215
230
  await refreshAccountData();
216
231
  return { success: true };
217
232
  } else if (result.sessionExpired) {
@@ -234,6 +249,13 @@ export const useMasterpassAccount = () => {
234
249
  };
235
250
  } else if (result.requires3D && result.redirectUrl) {
236
251
  dispatch(setShowOTPModal(false));
252
+
253
+ // Leaving the tab for the bank restarts the wait, so refresh the record
254
+ // without losing the order number it was opened with.
255
+ if (isPaymentContext) {
256
+ setPaymentPending(getPaymentPending()?.orderNo ?? null);
257
+ }
258
+
237
259
  window.location.href = result.redirectUrl;
238
260
  return {
239
261
  success: false,
@@ -261,7 +283,13 @@ export const useMasterpassAccount = () => {
261
283
  };
262
284
  }
263
285
  },
264
- [dispatch, refreshAccountData, token, tokenData?.MerchantId]
286
+ [
287
+ dispatch,
288
+ refreshAccountData,
289
+ token,
290
+ tokenData?.MerchantId,
291
+ modalState.otpContext
292
+ ]
265
293
  );
266
294
 
267
295
  const handleRemoveCard = useCallback(
@@ -372,6 +400,7 @@ export const useMasterpassAccount = () => {
372
400
  dispatch(
373
401
  updateModalState({
374
402
  otpType: result.otpType,
403
+ otpContext: 'account',
375
404
  verificationData: result.data,
376
405
  showLinkModal: false,
377
406
  showOTPModal: true
@@ -29,7 +29,9 @@ import { decodeMasterpassToken } from '../utils/wait-for-utils-ready';
29
29
  import {
30
30
  createPaymentRequest,
31
31
  createDirectPaymentRequest,
32
- getTransactionType
32
+ getTransactionType,
33
+ setPaymentPending,
34
+ clearPaymentPending
33
35
  } from '../utils/payment-utils';
34
36
  import { handleMasterpassResponse } from '../utils/response-handler';
35
37
  import type { Installment, RewardItem } from '../types/payment.types';
@@ -252,13 +254,20 @@ export const useMasterpassPayment = (
252
254
 
253
255
  dispatch(setOrderData(prepareResult));
254
256
 
257
+ const orderNo = prepareResult.context_list?.[0]?.page_context?.order_no;
258
+
259
+ // From here on the order number is bound to a Masterpass transaction.
260
+ // Regenerating it before that transaction settles makes the backend
261
+ // reject the completion with ORDER_NO_MISMATCH.
262
+ setPaymentPending(orderNo);
263
+
255
264
  const paymentService = new PaymentService(activeToken, activeMerchantId);
256
265
  const paymentRequest = createPaymentRequest({
257
266
  cvc,
258
267
  cardAlias: selectedCard.cardAlias,
259
268
  accountKey: activeTokenData.AccountKey,
260
269
  amount: prepareResult.pre_order?.total_amount_with_interest,
261
- orderNo: prepareResult.context_list?.[0]?.page_context?.order_no,
270
+ orderNo,
262
271
  currencyCode: currency,
263
272
  installmentCount: selectedInstallment.installment_count,
264
273
  authenticationMethod: activeTokenData.AuthenticationMethod,
@@ -288,6 +297,7 @@ export const useMasterpassPayment = (
288
297
  }).unwrap();
289
298
 
290
299
  dispatch(setOrderData(finalizeResult));
300
+ clearPaymentPending();
291
301
 
292
302
  if (finalizeResult.errors) {
293
303
  return { success: false, message: finalizeResult.errors };
@@ -297,6 +307,10 @@ export const useMasterpassPayment = (
297
307
  return { success: true };
298
308
  }
299
309
 
310
+ if (!response.requiresOTP) {
311
+ clearPaymentPending();
312
+ }
313
+
300
314
  return {
301
315
  success: response.success,
302
316
  requiresOTP: response.requiresOTP,
@@ -304,6 +318,7 @@ export const useMasterpassPayment = (
304
318
  message: response.message
305
319
  };
306
320
  } catch (error) {
321
+ clearPaymentPending();
307
322
  return {
308
323
  success: false,
309
324
  message:
@@ -361,6 +376,13 @@ export const useMasterpassPayment = (
361
376
 
362
377
  dispatch(setOrderData(prepareResult));
363
378
 
379
+ const orderNo = prepareResult.context_list?.[0]?.page_context?.order_no;
380
+
381
+ // From here on the order number is bound to a Masterpass transaction.
382
+ // Regenerating it before that transaction settles makes the backend
383
+ // reject the completion with ORDER_NO_MISMATCH.
384
+ setPaymentPending(orderNo);
385
+
364
386
  const paymentService = new PaymentService(activeToken, activeMerchantId);
365
387
  const directPaymentRequest = createDirectPaymentRequest({
366
388
  cvc: newCardFormData.cvv,
@@ -370,7 +392,7 @@ export const useMasterpassPayment = (
370
392
  expiryDate: newCardFormData.expiryDate,
371
393
  accountKey: activeTokenData.AccountKey,
372
394
  amount: prepareResult.pre_order?.total_amount_with_interest,
373
- orderNo: prepareResult.context_list?.[0]?.page_context?.order_no,
395
+ orderNo,
374
396
  currencyCode: currency,
375
397
  installmentCount: selectedInstallment.installment_count,
376
398
  authenticationMethod: activeTokenData.AuthenticationMethod,
@@ -402,6 +424,7 @@ export const useMasterpassPayment = (
402
424
  }).unwrap();
403
425
 
404
426
  dispatch(setOrderData(finalizeResult));
427
+ clearPaymentPending();
405
428
 
406
429
  if (finalizeResult.errors) {
407
430
  return { success: false, message: finalizeResult.errors };
@@ -411,6 +434,10 @@ export const useMasterpassPayment = (
411
434
  return { success: true };
412
435
  }
413
436
 
437
+ if (!response.requiresOTP) {
438
+ clearPaymentPending();
439
+ }
440
+
414
441
  return {
415
442
  success: response.success,
416
443
  requiresOTP: response.requiresOTP,
@@ -418,6 +445,7 @@ export const useMasterpassPayment = (
418
445
  message: response.message
419
446
  };
420
447
  } catch (error) {
448
+ clearPaymentPending();
421
449
  return {
422
450
  success: false,
423
451
  message:
@@ -439,6 +467,41 @@ export const useMasterpassPayment = (
439
467
  dispatch
440
468
  ]);
441
469
 
470
+ const finalizePaymentAfterVerification = useCallback(
471
+ async (verificationResponse: any): Promise<PaymentResult> => {
472
+ const { useThreeD } = paymentState;
473
+
474
+ try {
475
+ const finalizeResult = await finalizeMasterpassOrder({
476
+ responseCode: verificationResponse?.result?.responseCode,
477
+ token: verificationResponse?.result?.token,
478
+ three_d_secure: useThreeD,
479
+ transactionType: getTransactionType(useThreeD)
480
+ }).unwrap();
481
+
482
+ dispatch(setOrderData(finalizeResult));
483
+ clearPaymentPending();
484
+
485
+ if (finalizeResult.errors) {
486
+ return { success: false, message: finalizeResult.errors };
487
+ }
488
+
489
+ dispatch(setOrderCompleted(true));
490
+ return { success: true };
491
+ } catch (error) {
492
+ clearPaymentPending();
493
+ return {
494
+ success: false,
495
+ message:
496
+ error instanceof Error
497
+ ? error.message
498
+ : 'An unexpected error occurred'
499
+ };
500
+ }
501
+ },
502
+ [paymentState, finalizeMasterpassOrder, dispatch]
503
+ );
504
+
442
505
  return {
443
506
  paymentState,
444
507
 
@@ -455,6 +518,7 @@ export const useMasterpassPayment = (
455
518
  handleInstallmentSelect,
456
519
  processPayment,
457
520
  processDirectPayment,
521
+ finalizePaymentAfterVerification,
458
522
  fetchRewardsForCard,
459
523
  openRewardModal,
460
524
  closeRewardModal,
@@ -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