@akinon/pz-masterpass-rest 2.0.121 → 2.0.122-rc.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 +10 -0
- package/package.json +2 -2
- package/src/components/credit-card-form.tsx +1 -1
- package/src/components/installment-list.tsx +1 -1
- package/src/components/otp-modal.tsx +12 -4
- package/src/hooks/useMasterpassAccount.ts +26 -6
- package/src/hooks/useMasterpassPayment.ts +8 -8
- package/src/hooks/useMasterpassToken.ts +50 -13
- package/src/index.ts +1 -0
- package/src/utils/card-utils.ts +1 -1
- package/src/utils/wait-for-utils-ready.ts +64 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @akinon/pz-masterpass-rest
|
|
2
2
|
|
|
3
|
+
## 2.0.122-rc.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 119f0c110: 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
|
+
- 5da13fffc: ZERO-4358: fix masterpass-rest guest checkout flow
|
|
10
|
+
- fb4aa9b47: 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
|
|
12
|
+
|
|
3
13
|
## 2.0.121
|
|
4
14
|
|
|
5
15
|
## 2.0.120
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-masterpass-rest",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.122-rc.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",
|
|
@@ -34,6 +34,6 @@
|
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/react": "18.2.27",
|
|
36
36
|
"@types/react-dom": "18.2.12",
|
|
37
|
-
"typescript": "5.2.2"
|
|
37
|
+
"typescript": "^5.2.2"
|
|
38
38
|
}
|
|
39
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={
|
|
187
|
+
maxLength={19}
|
|
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 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
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"
|
|
122
122
|
>
|
|
123
123
|
{paymentLoading ? (
|
|
124
124
|
<div className="flex items-center justify-center">
|
|
@@ -204,15 +204,21 @@ 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">
|
|
207
|
+
<h3 className="text-center mt-4 text-lg font-semibold">
|
|
208
|
+
{getTitle()}
|
|
209
|
+
</h3>
|
|
208
210
|
)}
|
|
209
|
-
<p className="text-center mt-2 text-sm text-gray-600">
|
|
211
|
+
<p className="text-center mt-2 text-sm text-gray-600">
|
|
212
|
+
{getDescription()}
|
|
213
|
+
</p>
|
|
210
214
|
<div className="flex flex-col gap-3 p-5 w-3/4 m-auto">
|
|
211
215
|
<Input
|
|
212
216
|
type="text"
|
|
213
217
|
value={otp}
|
|
214
218
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
215
|
-
setOtp(
|
|
219
|
+
setOtp(
|
|
220
|
+
e.target.value.replace(/\D/g, '').slice(0, getMaxLength())
|
|
221
|
+
);
|
|
216
222
|
setError(null);
|
|
217
223
|
}}
|
|
218
224
|
maxLength={getMaxLength()}
|
|
@@ -222,7 +228,9 @@ const OTPModal: React.FC<OTPModalProps> = ({
|
|
|
222
228
|
placeholder={getPlaceholder()}
|
|
223
229
|
/>
|
|
224
230
|
{getHelperText() && (
|
|
225
|
-
<p className="text-xs text-gray-500 text-center">
|
|
231
|
+
<p className="text-xs text-gray-500 text-center">
|
|
232
|
+
{getHelperText()}
|
|
233
|
+
</p>
|
|
226
234
|
)}
|
|
227
235
|
{error && (
|
|
228
236
|
<p className="text-[#d72b01] text-xs text-center">{error}</p>
|
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
CardModel
|
|
26
26
|
} from '../types/account.types';
|
|
27
27
|
import { useMasterpassToken } from './useMasterpassToken';
|
|
28
|
+
import { decodeMasterpassToken } from '../utils/wait-for-utils-ready';
|
|
28
29
|
|
|
29
30
|
export const useMasterpassAccount = () => {
|
|
30
31
|
const dispatch = useDispatch();
|
|
@@ -41,7 +42,16 @@ export const useMasterpassAccount = () => {
|
|
|
41
42
|
|
|
42
43
|
const initializeAccount = useCallback(
|
|
43
44
|
async (newTokenData: any, rawToken?: string) => {
|
|
44
|
-
if (!newTokenData?.AccountKey)
|
|
45
|
+
if (!newTokenData?.AccountKey) {
|
|
46
|
+
dispatch(
|
|
47
|
+
setAccountStatus({
|
|
48
|
+
isAccountNotFound: false,
|
|
49
|
+
isAccountNotLinked: true,
|
|
50
|
+
shouldShowDirectForm: true
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
45
55
|
|
|
46
56
|
const accountService = new AccountService(rawToken || token, newTokenData?.MerchantId);
|
|
47
57
|
|
|
@@ -127,13 +137,19 @@ export const useMasterpassAccount = () => {
|
|
|
127
137
|
dispatch(resetState());
|
|
128
138
|
}, [dispatch]);
|
|
129
139
|
|
|
130
|
-
const refreshToken = async (options?: {
|
|
140
|
+
const refreshToken = async (options?: {
|
|
141
|
+
three_d?: boolean;
|
|
142
|
+
skipReset?: boolean;
|
|
143
|
+
}) => {
|
|
131
144
|
if (options?.three_d !== false && !options?.skipReset) {
|
|
132
145
|
resetData();
|
|
133
146
|
}
|
|
134
147
|
const result = await refetch(options);
|
|
135
148
|
if (result?.data?.token) {
|
|
136
|
-
const decodedToken =
|
|
149
|
+
const decodedToken = decodeMasterpassToken(result.data.token);
|
|
150
|
+
if (!decodedToken) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
137
153
|
return {
|
|
138
154
|
tokenData: decodedToken,
|
|
139
155
|
token: result.data.token,
|
|
@@ -233,8 +249,12 @@ export const useMasterpassAccount = () => {
|
|
|
233
249
|
informationModalData: {
|
|
234
250
|
type: 'warning',
|
|
235
251
|
title: texts?.sessionExpiredTitle || 'Session Expired',
|
|
236
|
-
message:
|
|
237
|
-
|
|
252
|
+
message:
|
|
253
|
+
texts?.sessionExpiredMessage ||
|
|
254
|
+
'Your session has expired due to inactivity. Please restart the process to continue.',
|
|
255
|
+
secondaryMessage:
|
|
256
|
+
texts?.sessionExpiredSecondaryMessage ||
|
|
257
|
+
'For security reasons, verification codes are only valid for a limited time.',
|
|
238
258
|
buttonText: texts?.sessionExpiredButton || 'Start Again'
|
|
239
259
|
}
|
|
240
260
|
})
|
|
@@ -363,7 +383,7 @@ export const useMasterpassAccount = () => {
|
|
|
363
383
|
const activeMerchantId = freshResult?.merchantId || tokenData?.MerchantId;
|
|
364
384
|
|
|
365
385
|
if (!activeTokenData?.AccountKey) {
|
|
366
|
-
|
|
386
|
+
return { success: false, message: '' };
|
|
367
387
|
}
|
|
368
388
|
|
|
369
389
|
const accountService = new AccountService(activeToken, activeMerchantId);
|
|
@@ -25,6 +25,7 @@ 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';
|
|
28
29
|
import {
|
|
29
30
|
createPaymentRequest,
|
|
30
31
|
createDirectPaymentRequest,
|
|
@@ -95,7 +96,10 @@ export const useMasterpassPayment = (
|
|
|
95
96
|
const refreshTokenForPayment = useCallback(async () => {
|
|
96
97
|
const result = await refetchToken({ skipReset: true });
|
|
97
98
|
if (result?.data?.token) {
|
|
98
|
-
const decodedToken =
|
|
99
|
+
const decodedToken = decodeMasterpassToken(result.data.token);
|
|
100
|
+
if (!decodedToken) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
99
103
|
return {
|
|
100
104
|
tokenData: decodedToken,
|
|
101
105
|
token: result.data.token,
|
|
@@ -362,10 +366,6 @@ export const useMasterpassPayment = (
|
|
|
362
366
|
const activeToken = freshResult?.token;
|
|
363
367
|
const activeMerchantId = freshResult?.merchantId || tokenData?.MerchantId;
|
|
364
368
|
|
|
365
|
-
if (!activeTokenData?.AccountKey) {
|
|
366
|
-
return { success: false, message: 'Token data not available' };
|
|
367
|
-
}
|
|
368
|
-
|
|
369
369
|
const prepareResult = await prepareMasterpassOrder({
|
|
370
370
|
use_three_d: paymentState.useThreeD
|
|
371
371
|
}).unwrap();
|
|
@@ -386,13 +386,13 @@ export const useMasterpassPayment = (
|
|
|
386
386
|
cardHolderName: newCardFormData.cardholderName || '',
|
|
387
387
|
cardAlias: newCardFormData.cardAlias || '',
|
|
388
388
|
expiryDate: newCardFormData.expiryDate,
|
|
389
|
-
accountKey: activeTokenData
|
|
389
|
+
accountKey: activeTokenData?.AccountKey || '',
|
|
390
390
|
amount: prepareResult.pre_order?.total_amount_with_interest,
|
|
391
391
|
orderNo,
|
|
392
392
|
currencyCode: currency,
|
|
393
393
|
installmentCount: selectedInstallment.installment_count,
|
|
394
|
-
authenticationMethod: activeTokenData
|
|
395
|
-
secure3DModel: activeTokenData
|
|
394
|
+
authenticationMethod: activeTokenData?.AuthenticationMethod,
|
|
395
|
+
secure3DModel: activeTokenData?.Secure3dType,
|
|
396
396
|
terminalGroupId,
|
|
397
397
|
acquirerIcaNumber: bankIca,
|
|
398
398
|
additionalFields:
|
|
@@ -14,6 +14,10 @@ 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';
|
|
17
21
|
|
|
18
22
|
interface UseMasterpassTokenProps {
|
|
19
23
|
useThreeD: boolean;
|
|
@@ -65,12 +69,37 @@ export const useMasterpassToken = ({
|
|
|
65
69
|
return;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
if (data?.token && data.token !== reduxToken) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
if (!(data?.token && data.token !== reduxToken)) {
|
|
73
|
+
return;
|
|
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
|
+
}
|
|
72
102
|
|
|
73
|
-
const decodedToken = window.Utils.decodeJwt(data.token);
|
|
74
103
|
dispatch(setMasterpassRestTokenData(decodedToken));
|
|
75
104
|
|
|
76
105
|
if (decodedToken?.AuthenticationMethod?.toLowerCase() === 'none') {
|
|
@@ -78,7 +107,11 @@ export const useMasterpassToken = ({
|
|
|
78
107
|
}
|
|
79
108
|
|
|
80
109
|
stableOnTokenReady(decodedToken);
|
|
81
|
-
}
|
|
110
|
+
})();
|
|
111
|
+
|
|
112
|
+
return () => {
|
|
113
|
+
cancelled = true;
|
|
114
|
+
};
|
|
82
115
|
}, [data?.token, reduxToken, dispatch, stableOnTokenReady]);
|
|
83
116
|
|
|
84
117
|
const refetchToken = useCallback(
|
|
@@ -94,15 +127,19 @@ export const useMasterpassToken = ({
|
|
|
94
127
|
);
|
|
95
128
|
dispatch(setMasterpassBankIca(result.data.extras.bank_ica));
|
|
96
129
|
|
|
97
|
-
|
|
98
|
-
|
|
130
|
+
await waitForUtilsReady();
|
|
131
|
+
const decodedToken = decodeMasterpassToken(result.data.token);
|
|
99
132
|
|
|
100
|
-
if (decodedToken
|
|
101
|
-
dispatch(
|
|
102
|
-
|
|
133
|
+
if (decodedToken) {
|
|
134
|
+
dispatch(setMasterpassRestTokenData(decodedToken));
|
|
135
|
+
|
|
136
|
+
if (decodedToken?.AuthenticationMethod?.toLowerCase() === 'none') {
|
|
137
|
+
dispatch(updatePaymentState({ useThreeD: false }));
|
|
138
|
+
}
|
|
103
139
|
|
|
104
|
-
|
|
105
|
-
|
|
140
|
+
if (options?.three_d !== false && !options?.skipReset) {
|
|
141
|
+
stableOnTokenReady(decodedToken);
|
|
142
|
+
}
|
|
106
143
|
}
|
|
107
144
|
}
|
|
108
145
|
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ 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';
|
|
22
23
|
export type { MasterpassEnvironment } from './utils/constants';
|
|
23
24
|
|
|
24
25
|
export * from './types/payment.types';
|
package/src/utils/card-utils.ts
CHANGED
|
@@ -9,7 +9,7 @@ export const formatCardNumber = (value: string): string => {
|
|
|
9
9
|
|
|
10
10
|
const formatted = cleanValue.replace(/(\d{4})(?=\d)/g, '$1 ');
|
|
11
11
|
|
|
12
|
-
return formatted.substring(0,
|
|
12
|
+
return formatted.substring(0, 19);
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export const formatExpiryDate = (value: string): string => {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
2
|
+
const POLL_INTERVAL_MS = 100;
|
|
3
|
+
|
|
4
|
+
interface MasterpassUtils {
|
|
5
|
+
decodeJwt: (token: string) => any;
|
|
6
|
+
[key: string]: any;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// `window.Utils` is attached by the Masterpass REST SDK and is undefined until
|
|
10
|
+
// that script finishes loading, so it is resolved through this guarded getter
|
|
11
|
+
// rather than accessed directly.
|
|
12
|
+
const getMasterpassUtils = (): MasterpassUtils | undefined =>
|
|
13
|
+
(window as unknown as { Utils?: MasterpassUtils }).Utils;
|
|
14
|
+
|
|
15
|
+
// The Masterpass REST SDK (`/masterpass-javascript-sdk-web.min.js`) attaches
|
|
16
|
+
// both `window.Masterpass` and `window.Utils` when it finishes loading. That
|
|
17
|
+
// script is loaded asynchronously (see `useMasterpassScript`), so on slow
|
|
18
|
+
// networks the `/orders/masterpass-token` response can arrive before the SDK
|
|
19
|
+
// is ready. Calling `window.Utils.decodeJwt(...)` at that point crashes the
|
|
20
|
+
// checkout with `TypeError: Cannot read properties of undefined (reading
|
|
21
|
+
// 'decodeJwt')` (ZERO-4362 — same class of bug already fixed in the non-rest
|
|
22
|
+
// `@akinon/pz-masterpass` package).
|
|
23
|
+
//
|
|
24
|
+
// This helper polls until the SDK has exposed `window.Utils.decodeJwt`, so
|
|
25
|
+
// callers can await readiness before decoding instead of crashing.
|
|
26
|
+
export const waitForUtilsReady = (
|
|
27
|
+
timeoutMs: number = DEFAULT_TIMEOUT_MS
|
|
28
|
+
): Promise<boolean> =>
|
|
29
|
+
new Promise((resolve) => {
|
|
30
|
+
const start = Date.now();
|
|
31
|
+
|
|
32
|
+
const check = () => {
|
|
33
|
+
if (typeof getMasterpassUtils()?.decodeJwt === 'function') {
|
|
34
|
+
resolve(true);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (Date.now() - start >= timeoutMs) {
|
|
39
|
+
resolve(false);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setTimeout(check, POLL_INTERVAL_MS);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
check();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Guarded `decodeJwt` wrapper. Returns `undefined` (instead of throwing) when
|
|
50
|
+
// the SDK is not yet available, so every callsite degrades gracefully.
|
|
51
|
+
export const decodeMasterpassToken = <T = any>(
|
|
52
|
+
token: string
|
|
53
|
+
): T | undefined => {
|
|
54
|
+
const utils = getMasterpassUtils();
|
|
55
|
+
|
|
56
|
+
if (typeof utils?.decodeJwt !== 'function') {
|
|
57
|
+
console.error(
|
|
58
|
+
'[pz-masterpass-rest] window.Utils.decodeJwt is not available'
|
|
59
|
+
);
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return utils.decodeJwt(token);
|
|
64
|
+
};
|