@akinon/pz-masterpass-rest 2.0.116 → 2.0.117-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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # @akinon/pz-masterpass-rest
2
2
 
3
+ ## 2.0.117-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.116
4
14
 
5
15
  ## 2.0.115
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinon/pz-masterpass-rest",
3
- "version": "2.0.116",
3
+ "version": "2.0.117-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={23}
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">{getTitle()}</h3>
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">{getDescription()}</p>
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(e.target.value.replace(/\D/g, '').slice(0, getMaxLength()));
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">{getHelperText()}</p>
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) return;
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?: { three_d?: boolean; skipReset?: boolean }) => {
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 = window.Utils.decodeJwt(result.data.token);
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: 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.',
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
- throw new Error('Token data not available');
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 = window.Utils.decodeJwt(result.data.token);
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.AccountKey,
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.AuthenticationMethod,
395
- secure3DModel: activeTokenData.Secure3dType,
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
- dispatch(setMasterpassRestToken(data.token));
70
- dispatch(setMasterpassTerminalGroupId(data.extras.terminal_group_id));
71
- dispatch(setMasterpassBankIca(data.extras.bank_ica));
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
- const decodedToken = window.Utils.decodeJwt(result.data.token);
98
- dispatch(setMasterpassRestTokenData(decodedToken));
130
+ await waitForUtilsReady();
131
+ const decodedToken = decodeMasterpassToken(result.data.token);
99
132
 
100
- if (decodedToken?.AuthenticationMethod?.toLowerCase() === 'none') {
101
- dispatch(updatePaymentState({ useThreeD: false }));
102
- }
133
+ if (decodedToken) {
134
+ dispatch(setMasterpassRestTokenData(decodedToken));
135
+
136
+ if (decodedToken?.AuthenticationMethod?.toLowerCase() === 'none') {
137
+ dispatch(updatePaymentState({ useThreeD: false }));
138
+ }
103
139
 
104
- if (options?.three_d !== false && !options?.skipReset) {
105
- stableOnTokenReady(decodedToken);
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';
@@ -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, 23);
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
+ };