@akinon/pz-masterpass-rest 2.0.56-rc.0 → 2.0.57-beta.0

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,14 +1,12 @@
1
1
  # @akinon/pz-masterpass-rest
2
2
 
3
- ## 2.0.56-rc.0
3
+ ## 2.0.57-beta.0
4
+
5
+ ## 2.0.56
4
6
 
5
7
  ### Patch Changes
6
8
 
7
- - 119f0c11: ZERO-4629: Refactor token decoding to use decodeMasterpassToken utility and add wait-for-utils-ready functionality
8
- - 823a4449a: ZERO-4193: Upgrade TypeScript to version 5.2.2 across multiple packages
9
- - 5da13fff: ZERO-4358: fix masterpass-rest guest checkout flow
10
- - fb4aa9b4: ZERO-4333: update maxLength for cardNumber input and enhance button styling in installment list
11
- - 63a720003: ZERO-3895: enhance payment processing with additional fields support
9
+ - 6a61e19: ZERO-4788:Implement payment context handling and improve OTP flow in Masterpass integration
12
10
 
13
11
  ## 2.0.55
14
12
 
@@ -0,0 +1,26 @@
1
+ const React = require('react');
2
+
3
+ // Minimal stand-ins: tests assert on this package's behavior, not on how the
4
+ // design system renders.
5
+ const passthrough = (tag) => (props) => {
6
+ const { children, ...rest } = props || {};
7
+ return React.createElement(tag, rest, children);
8
+ };
9
+
10
+ const Modal = ({ open, children }) =>
11
+ open ? React.createElement('div', { 'data-testid': 'modal' }, children) : null;
12
+
13
+ module.exports = {
14
+ Icon: (props) => React.createElement('i', { 'data-icon': props?.name }),
15
+ Button: passthrough('button'),
16
+ Input: (props) => {
17
+ const { error, label, ...rest } = props || {};
18
+ return React.createElement('input', rest);
19
+ },
20
+ LoaderSpinner: () => React.createElement('div', { role: 'status' }),
21
+ Modal,
22
+ Price: (props) => React.createElement('span', null, String(props?.value ?? '')),
23
+ Radio: passthrough('input'),
24
+ Checkbox: passthrough('input'),
25
+ Image: (props) => React.createElement('img', { alt: props?.alt, src: props?.src })
26
+ };
@@ -0,0 +1,6 @@
1
+ const { useSelector } = require('react-redux');
2
+
3
+ module.exports = {
4
+ useAppSelector: useSelector,
5
+ useAppDispatch: () => require('react-redux').useDispatch()
6
+ };
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ api: { injectEndpoints: () => ({ endpoints: {} }) },
3
+ setPaymentStepBusy: () => ({ type: 'checkout/setPaymentStepBusy' })
4
+ };
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ getPosError: jest.fn(() => null),
3
+ buildClientRequestUrl: (url) => url
4
+ };
@@ -0,0 +1,6 @@
1
+ // Ships ESM-only builds; the form validation path is not what these tests cover.
2
+ module.exports = {
3
+ yupResolver: () => async () => ({ values: {}, errors: {} }),
4
+ toNestError: (errors) => errors,
5
+ validateFieldsNatively: () => {}
6
+ };
@@ -0,0 +1,16 @@
1
+ // The package reads the host app's store directly in its service layer. Tests
2
+ // only need the masterpassRest slice the services touch.
3
+ const state = {
4
+ masterpassRest: {
5
+ token: 'test-token',
6
+ tokenData: { MerchantId: 'merchant-1' },
7
+ modalState: { verificationData: null }
8
+ }
9
+ };
10
+
11
+ module.exports = {
12
+ store: {
13
+ getState: () => state,
14
+ dispatch: jest.fn()
15
+ }
16
+ };
@@ -0,0 +1 @@
1
+ module.exports = { src: '/test-asset.png', height: 1, width: 1 };
package/jest/setup.ts ADDED
@@ -0,0 +1,37 @@
1
+ import '@testing-library/jest-dom';
2
+
3
+ // React needs this flag to accept act(...) outside of a render call.
4
+ (global as any).IS_REACT_ACT_ENVIRONMENT = true;
5
+
6
+ // The SDK is injected by useMasterpassScript at runtime. Tests never exercise
7
+ // the real script, but service constructors call these setters eagerly.
8
+ beforeEach(() => {
9
+ (window as any).Masterpass = {
10
+ setToken: jest.fn(),
11
+ setMerchantId: jest.fn(),
12
+ paymentService: {
13
+ payment: jest.fn(),
14
+ directPayment: jest.fn()
15
+ },
16
+ verifyService: {
17
+ verifyOtp: jest.fn(),
18
+ resendOtp: jest.fn()
19
+ }
20
+ };
21
+
22
+ (window as any).Utils = {
23
+ decodeJwt: jest.fn(() => ({
24
+ MerchantId: 'merchant-1',
25
+ AccountKey: 'account-1',
26
+ AuthenticationMethod: '3DS',
27
+ Secure3dType: '3D',
28
+ UserId: 'user-1'
29
+ }))
30
+ };
31
+
32
+ window.sessionStorage.clear();
33
+ });
34
+
35
+ afterEach(() => {
36
+ jest.clearAllMocks();
37
+ });
@@ -0,0 +1 @@
1
+ module.exports = {};
package/jest.config.js ADDED
@@ -0,0 +1,46 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} */
2
+ module.exports = {
3
+ preset: 'ts-jest',
4
+ testEnvironment: 'jsdom',
5
+ roots: ['<rootDir>/src'],
6
+ testMatch: ['**/*.test.ts', '**/*.test.tsx'],
7
+ setupFilesAfterEnv: ['<rootDir>/jest/setup.ts'],
8
+ // The hooks and the option view pull in app-coupled modules (@akinon/next
9
+ // components, the host app's redux store, image assets). Each is mapped to a
10
+ // small stub so tests exercise this package's own logic.
11
+ moduleNameMapper: {
12
+ '\\.(css|less|scss|sass)$': '<rootDir>/jest/style-mock.js',
13
+ '\\.(png|jpg|jpeg|gif|svg|webp)$': '<rootDir>/jest/file-mock.js',
14
+ '^redux/store$': '<rootDir>/jest/__mocks__/host-store.js',
15
+ '^@hookform/resolvers(/.*)?$': '<rootDir>/jest/__mocks__/hookform-resolvers.js',
16
+ '^@akinon/next/redux/hooks$':
17
+ '<rootDir>/jest/__mocks__/akinon-next-redux-hooks.js',
18
+ '^@akinon/next/components$':
19
+ '<rootDir>/jest/__mocks__/akinon-next-components.js',
20
+ '^@akinon/next/utils$': '<rootDir>/jest/__mocks__/akinon-next-utils.js',
21
+ '^@akinon/next/data/client/(.*)$': '<rootDir>/jest/__mocks__/akinon-next-rtk.js'
22
+ },
23
+ transform: {
24
+ '^.+\\.(ts|tsx)$': [
25
+ 'ts-jest',
26
+ {
27
+ // Transpile-only: the package is consumed by brands through SWC and is
28
+ // never type-checked at build time, so tests assert runtime behavior.
29
+ diagnostics: false,
30
+ tsconfig: {
31
+ isolatedModules: true,
32
+ jsx: 'react-jsx',
33
+ esModuleInterop: true,
34
+ allowSyntheticDefaultImports: true,
35
+ module: 'commonjs',
36
+ target: 'es2020',
37
+ lib: ['es2020', 'dom', 'dom.iterable'],
38
+ moduleResolution: 'node',
39
+ resolveJsonModule: true,
40
+ skipLibCheck: true
41
+ }
42
+ }
43
+ ]
44
+ },
45
+ transformIgnorePatterns: ['node_modules/(?!(@akinon)/)']
46
+ };
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@akinon/pz-masterpass-rest",
3
- "version": "2.0.56-rc.0",
3
+ "version": "2.0.57-beta.0",
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",
7
+ "scripts": {
8
+ "test": "jest"
9
+ },
7
10
  "keywords": [
8
11
  "masterpass",
9
12
  "payment",
@@ -31,6 +34,6 @@
31
34
  "devDependencies": {
32
35
  "@types/react": "18.2.27",
33
36
  "@types/react-dom": "18.2.12",
34
- "typescript": "^5.2.2"
37
+ "typescript": "5.2.2"
35
38
  }
36
39
  }
@@ -184,7 +184,7 @@ const CreditCardForm: React.FC<CreditCardFormProps> = ({
184
184
  setValue('cardNumber', formatted, { shouldValidate: true });
185
185
  }}
186
186
  error={errors.cardNumber}
187
- maxLength={19}
187
+ maxLength={23}
188
188
  className="pr-20"
189
189
  />
190
190
  {cardType !== 'unknown' && (
@@ -118,7 +118,7 @@ const InstallmentList: React.FC<InstallmentListProps> = ({
118
118
  <Button
119
119
  onClick={onProceedToPayment}
120
120
  disabled={isLoading || paymentLoading}
121
- className="w-full cursor-pointer bg-[#000000] text-[#ffffff] hover:bg-[#ffffff] hover:text-[#000000] border border-solid border-[#000000] disabled:opacity-50 disabled:cursor-not-allowed"
121
+ className="w-full cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
122
122
  >
123
123
  {paymentLoading ? (
124
124
  <div className="flex items-center justify-center">
@@ -204,21 +204,15 @@ const OTPModal: React.FC<OTPModalProps> = ({
204
204
  >
205
205
  <div className="px-6">
206
206
  {getTitle() && (
207
- <h3 className="text-center mt-4 text-lg font-semibold">
208
- {getTitle()}
209
- </h3>
207
+ <h3 className="text-center mt-4 text-lg font-semibold">{getTitle()}</h3>
210
208
  )}
211
- <p className="text-center mt-2 text-sm text-gray-600">
212
- {getDescription()}
213
- </p>
209
+ <p className="text-center mt-2 text-sm text-gray-600">{getDescription()}</p>
214
210
  <div className="flex flex-col gap-3 p-5 w-3/4 m-auto">
215
211
  <Input
216
212
  type="text"
217
213
  value={otp}
218
214
  onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
219
- setOtp(
220
- e.target.value.replace(/\D/g, '').slice(0, getMaxLength())
221
- );
215
+ setOtp(e.target.value.replace(/\D/g, '').slice(0, getMaxLength()));
222
216
  setError(null);
223
217
  }}
224
218
  maxLength={getMaxLength()}
@@ -228,9 +222,7 @@ const OTPModal: React.FC<OTPModalProps> = ({
228
222
  placeholder={getPlaceholder()}
229
223
  />
230
224
  {getHelperText() && (
231
- <p className="text-xs text-gray-500 text-center">
232
- {getHelperText()}
233
- </p>
225
+ <p className="text-xs text-gray-500 text-center">{getHelperText()}</p>
234
226
  )}
235
227
  {error && (
236
228
  <p className="text-[#d72b01] text-xs text-center">{error}</p>
@@ -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
+ });