@akinon/pz-masterpass-rest 2.0.0-beta.12 → 2.0.0-beta.13

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": "2.0.0-beta.12",
3
+ "version": "2.0.0-beta.13",
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",
@@ -23,8 +23,8 @@
23
23
  "yup": "0.32.11"
24
24
  },
25
25
  "peerDependencies": {
26
- "react": "^18.2.0",
27
- "react-dom": "^18.2.0",
26
+ "react": "^18.2.0 || ^19.0.0",
27
+ "react-dom": "^18.2.0 || ^19.0.0",
28
28
  "react-redux": "^8.1.3",
29
29
  "@reduxjs/toolkit": "^1.9.7"
30
30
  },
@@ -50,7 +50,7 @@ const CreditCardForm: React.FC<CreditCardFormProps> = ({
50
50
  setValue,
51
51
  formState: { errors, isValid }
52
52
  } = useForm<CreditCardFormData>({
53
- resolver: yupResolver(validationSchema),
53
+ resolver: yupResolver(validationSchema) as any,
54
54
  defaultValues: {
55
55
  cardNumber: '',
56
56
  expiryDate: '',
@@ -0,0 +1,139 @@
1
+ import React from 'react';
2
+ import { Modal, Button } from '@akinon/next/components';
3
+ import mpBlackLogo from '../assets/img/masterpass-black-logo.png';
4
+ import type { InformationModalType, InformationModalData } from '../types/payment.types';
5
+
6
+ export interface InformationModalProps {
7
+ open: boolean;
8
+ onClose: () => void;
9
+ data: InformationModalData | null;
10
+ onButtonClick?: () => void;
11
+ }
12
+
13
+ const iconMap: Record<InformationModalType, React.ReactNode> = {
14
+ warning: (
15
+ <svg
16
+ className="w-8 h-8 text-amber-600"
17
+ fill="none"
18
+ stroke="currentColor"
19
+ viewBox="0 0 24 24"
20
+ >
21
+ <path
22
+ strokeLinecap="round"
23
+ strokeLinejoin="round"
24
+ strokeWidth={2}
25
+ d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
26
+ />
27
+ </svg>
28
+ ),
29
+ error: (
30
+ <svg
31
+ className="w-8 h-8 text-red-600"
32
+ fill="none"
33
+ stroke="currentColor"
34
+ viewBox="0 0 24 24"
35
+ >
36
+ <path
37
+ strokeLinecap="round"
38
+ strokeLinejoin="round"
39
+ strokeWidth={2}
40
+ d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
41
+ />
42
+ </svg>
43
+ ),
44
+ success: (
45
+ <svg
46
+ className="w-8 h-8 text-green-600"
47
+ fill="none"
48
+ stroke="currentColor"
49
+ viewBox="0 0 24 24"
50
+ >
51
+ <path
52
+ strokeLinecap="round"
53
+ strokeLinejoin="round"
54
+ strokeWidth={2}
55
+ d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
56
+ />
57
+ </svg>
58
+ ),
59
+ info: (
60
+ <svg
61
+ className="w-8 h-8 text-blue-600"
62
+ fill="none"
63
+ stroke="currentColor"
64
+ viewBox="0 0 24 24"
65
+ >
66
+ <path
67
+ strokeLinecap="round"
68
+ strokeLinejoin="round"
69
+ strokeWidth={2}
70
+ d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
71
+ />
72
+ </svg>
73
+ )
74
+ };
75
+
76
+ const bgColorMap: Record<InformationModalType, string> = {
77
+ warning: 'bg-amber-100',
78
+ error: 'bg-red-100',
79
+ success: 'bg-green-100',
80
+ info: 'bg-blue-100'
81
+ };
82
+
83
+ const InformationModal: React.FC<InformationModalProps> = ({
84
+ open,
85
+ onClose,
86
+ data,
87
+ onButtonClick
88
+ }) => {
89
+ const handleButtonClick = () => {
90
+ if (onButtonClick) {
91
+ onButtonClick();
92
+ } else {
93
+ onClose();
94
+ }
95
+ };
96
+
97
+ if (!data) return null;
98
+
99
+ const { type, title, message, secondaryMessage, buttonText } = data;
100
+
101
+ return (
102
+ <Modal
103
+ portalId="masterpass-information-modal"
104
+ open={open}
105
+ setOpen={onClose}
106
+ title={
107
+ <img src={mpBlackLogo.src} alt="Masterpass" className="w-36 h-auto" />
108
+ }
109
+ className="w-full sm:w-[28rem] max-h-[90vh] overflow-y-auto"
110
+ >
111
+ <div className="px-6 py-4">
112
+ <div className="flex flex-col items-center text-center">
113
+ <div className={`w-16 h-16 rounded-full ${bgColorMap[type]} flex items-center justify-center mb-4`}>
114
+ {iconMap[type]}
115
+ </div>
116
+ <h3 className="text-lg font-semibold text-gray-900 mb-2">
117
+ {title}
118
+ </h3>
119
+ <p className="text-sm text-gray-600 mb-3">
120
+ {message}
121
+ </p>
122
+ {secondaryMessage && (
123
+ <p className="text-xs text-gray-500 mb-6">
124
+ {secondaryMessage}
125
+ </p>
126
+ )}
127
+ <Button
128
+ className="w-full py-3 h-auto"
129
+ onClick={handleButtonClick}
130
+ >
131
+ {buttonText}
132
+ </Button>
133
+ </div>
134
+ </div>
135
+ </Modal>
136
+ );
137
+ };
138
+
139
+ export default InformationModal;
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { Modal, Button, Input } from '@akinon/next/components';
3
3
  import { LoaderSpinner } from '@akinon/next/components';
4
+ import { useAppSelector } from '@akinon/next/redux/hooks';
4
5
  import type { OTPModalProps } from '../types/custom-render.types';
5
6
  import mpBlackLogo from '../assets/img/masterpass-black-logo.png';
6
7
  import { handleResendOtp } from '../utils/response-handler';
@@ -11,9 +12,11 @@ const OTPModal: React.FC<OTPModalProps> = ({
11
12
  onClose,
12
13
  onSubmit,
13
14
  type,
15
+ responseCode,
14
16
  description,
15
17
  texts
16
18
  }) => {
19
+ const { token, tokenData } = useAppSelector((state) => state.masterpassRest);
17
20
  const [otp, setOtp] = useState('');
18
21
  const [isLoading, setIsLoading] = useState(false);
19
22
  const [error, setError] = useState<string | null>(null);
@@ -21,6 +24,13 @@ const OTPModal: React.FC<OTPModalProps> = ({
21
24
  const [canResend, setCanResend] = useState(false);
22
25
  const [remainingTime, setRemainingTime] = useState(90);
23
26
 
27
+ useEffect(() => {
28
+ if (!open) {
29
+ setError(null);
30
+ setOtp('');
31
+ }
32
+ }, [open]);
33
+
24
34
  useEffect(() => {
25
35
  if (open && type === 'OTP') {
26
36
  setRemainingTime(90);
@@ -38,14 +48,16 @@ const OTPModal: React.FC<OTPModalProps> = ({
38
48
 
39
49
  return () => clearInterval(timer);
40
50
  }
41
- }, [open, type]);
51
+ }, [open, type, responseCode]);
42
52
 
43
53
  const handleSubmit = async () => {
44
54
  setError(null);
45
55
  setIsLoading(true);
46
56
  try {
47
57
  const result = await onSubmit(otp);
48
- if (result?.message) {
58
+ if (result?.requiresOTP) {
59
+ setOtp('');
60
+ } else if (result?.message) {
49
61
  setError(result.message);
50
62
  }
51
63
  } catch (error) {
@@ -59,7 +71,7 @@ const OTPModal: React.FC<OTPModalProps> = ({
59
71
  setError(null);
60
72
  setIsResending(true);
61
73
  try {
62
- const result = await handleResendOtp();
74
+ const result = await handleResendOtp(token, tokenData?.MerchantId);
63
75
  if (result.success) {
64
76
  setRemainingTime(90);
65
77
  setCanResend(false);
@@ -90,7 +102,47 @@ const OTPModal: React.FC<OTPModalProps> = ({
90
102
  return `${minutes}:${secs.toString().padStart(2, '0')}`;
91
103
  };
92
104
 
105
+ const getContentByResponseCode = () => {
106
+ switch (responseCode) {
107
+ case '5000':
108
+ return {
109
+ title: texts.rtaVerificationTitle,
110
+ description: texts.rtaVerificationDescription,
111
+ placeholder: texts.rtaVerificationPlaceholder,
112
+ helperText: null
113
+ };
114
+ case '5001':
115
+ return {
116
+ title: texts.bankOtpVerificationTitle,
117
+ description: texts.bankOtpVerificationDescription,
118
+ placeholder: texts.bankOtpVerificationPlaceholder,
119
+ helperText: null
120
+ };
121
+ case '5008':
122
+ return {
123
+ title: texts.phoneVerificationTitle,
124
+ description: texts.phoneVerificationDescription,
125
+ placeholder: texts.phoneVerificationPlaceholder,
126
+ helperText: null
127
+ };
128
+ case '5013':
129
+ return {
130
+ title: texts.cvvVerificationTitle,
131
+ description: texts.cvvVerificationDescription,
132
+ placeholder: texts.cvvVerificationPlaceholder,
133
+ helperText: texts.cvvVerificationHelperText
134
+ };
135
+ default:
136
+ return null;
137
+ }
138
+ };
139
+
93
140
  const getDescription = () => {
141
+ const responseCodeContent = getContentByResponseCode();
142
+ if (responseCodeContent) {
143
+ return responseCodeContent.description;
144
+ }
145
+
94
146
  if (description) {
95
147
  return description;
96
148
  }
@@ -104,15 +156,40 @@ const OTPModal: React.FC<OTPModalProps> = ({
104
156
  }
105
157
  };
106
158
 
107
- const getMaxLength = () => {
159
+ const getTitle = () => {
160
+ const responseCodeContent = getContentByResponseCode();
161
+ if (responseCodeContent) {
162
+ return responseCodeContent.title;
163
+ }
164
+
108
165
  switch (type) {
109
166
  case 'RTA':
110
- return 3;
167
+ return texts.rtaVerificationTitle;
111
168
  case 'CVV':
112
- return 3;
169
+ return texts.cvvVerificationTitle;
113
170
  default:
114
- return 6;
171
+ return null;
172
+ }
173
+ };
174
+
175
+ const getPlaceholder = () => {
176
+ const responseCodeContent = getContentByResponseCode();
177
+ if (responseCodeContent) {
178
+ return responseCodeContent.placeholder;
115
179
  }
180
+ return '';
181
+ };
182
+
183
+ const getHelperText = () => {
184
+ const responseCodeContent = getContentByResponseCode();
185
+ return responseCodeContent?.helperText || null;
186
+ };
187
+
188
+ const getMaxLength = () => {
189
+ if (responseCode === '5013' || type === 'CVV') {
190
+ return 4;
191
+ }
192
+ return 6;
116
193
  };
117
194
 
118
195
  return (
@@ -126,27 +203,32 @@ const OTPModal: React.FC<OTPModalProps> = ({
126
203
  className="w-full sm:w-[28rem] max-h-[90vh] overflow-y-auto"
127
204
  >
128
205
  <div className="px-6">
129
- <p className="text-center mt-4 text-lg">{getDescription()}</p>
206
+ {getTitle() && (
207
+ <h3 className="text-center mt-4 text-lg font-semibold">{getTitle()}</h3>
208
+ )}
209
+ <p className="text-center mt-2 text-sm text-gray-600">{getDescription()}</p>
130
210
  <div className="flex flex-col gap-3 p-5 w-3/4 m-auto">
131
211
  <Input
132
212
  type="text"
133
213
  value={otp}
134
214
  onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
135
- setOtp(
136
- e.target.value.replace(/\D/g, '').slice(0, getMaxLength())
137
- );
215
+ setOtp(e.target.value.replace(/\D/g, '').slice(0, getMaxLength()));
138
216
  setError(null);
139
217
  }}
140
218
  maxLength={getMaxLength()}
141
219
  pattern="[0-9]*"
142
220
  inputMode="numeric"
143
221
  className="text-center"
222
+ placeholder={getPlaceholder()}
144
223
  />
224
+ {getHelperText() && (
225
+ <p className="text-xs text-gray-500 text-center">{getHelperText()}</p>
226
+ )}
145
227
  {error && <p className="text-error text-xs text-center">{error}</p>}
146
228
  <Button
147
229
  className="py-3 h-auto"
148
230
  onClick={handleSubmit}
149
- disabled={isLoading || otp.length !== getMaxLength()}
231
+ disabled={isLoading || otp.length === 0}
150
232
  >
151
233
  {isLoading ? (
152
234
  <LoaderSpinner className="w-4 h-4" />
@@ -39,16 +39,16 @@ export const useMasterpassAccount = () => {
39
39
  } = useAppSelector((state) => state.masterpassRest);
40
40
 
41
41
  const initializeAccount = useCallback(
42
- async (tokenData: any) => {
43
- if (!tokenData?.AccountKey) return;
42
+ async (newTokenData: any, rawToken?: string) => {
43
+ if (!newTokenData?.AccountKey) return;
44
44
 
45
- const accountService = new AccountService();
45
+ const accountService = new AccountService(rawToken || token, newTokenData?.MerchantId);
46
46
 
47
47
  try {
48
48
  const response = await accountService.accountAccess({
49
- accountKey: tokenData.AccountKey,
49
+ accountKey: newTokenData.AccountKey,
50
50
  accountKeyType: 'Msisdn',
51
- userId: tokenData.UserId
51
+ userId: newTokenData.UserId
52
52
  });
53
53
 
54
54
  if (response.statusCode !== 200 && response.exception) {
@@ -99,12 +99,12 @@ export const useMasterpassAccount = () => {
99
99
  );
100
100
  }
101
101
  },
102
- [dispatch]
102
+ [dispatch, token]
103
103
  );
104
104
 
105
105
  const onTokenReady = useCallback(
106
- (newTokenData: any) => {
107
- initializeAccount(newTokenData);
106
+ (newTokenData: any, rawToken?: string) => {
107
+ initializeAccount(newTokenData, rawToken);
108
108
  },
109
109
  [initializeAccount]
110
110
  );
@@ -126,15 +126,26 @@ export const useMasterpassAccount = () => {
126
126
  dispatch(resetState());
127
127
  }, [dispatch]);
128
128
 
129
- const refreshToken = async () => {
130
- resetData();
131
- await refetch();
129
+ const refreshToken = async (options?: { three_d?: boolean; skipReset?: boolean }) => {
130
+ if (options?.three_d !== false && !options?.skipReset) {
131
+ resetData();
132
+ }
133
+ const result = await refetch(options);
134
+ if (result?.data?.token) {
135
+ const decodedToken = window.Utils.decodeJwt(result.data.token);
136
+ return {
137
+ tokenData: decodedToken,
138
+ token: result.data.token,
139
+ merchantId: decodedToken?.MerchantId
140
+ };
141
+ }
142
+ return null;
132
143
  };
133
144
 
134
145
  const refreshAccountData = useCallback(async () => {
135
146
  if (!tokenData?.AccountKey) return;
136
147
 
137
- const accountService = new AccountService();
148
+ const accountService = new AccountService(token, tokenData?.MerchantId);
138
149
  const response = await accountService.accountAccess({
139
150
  accountKey: tokenData.AccountKey,
140
151
  accountKeyType: 'Msisdn',
@@ -153,15 +164,20 @@ export const useMasterpassAccount = () => {
153
164
  })
154
165
  );
155
166
  }
156
- }, [tokenData?.AccountKey, tokenData?.UserId, dispatch]);
167
+ }, [tokenData?.AccountKey, tokenData?.UserId, tokenData?.MerchantId, token, dispatch]);
157
168
 
158
169
  const handleLinkConfirm = useCallback(async () => {
159
- if (!tokenData?.AccountKey) return;
160
-
161
170
  try {
162
- const accountService = new AccountService();
171
+ const freshResult = await refreshToken({ three_d: false });
172
+ const activeTokenData = freshResult?.tokenData || tokenData;
173
+ const activeToken = freshResult?.token || token;
174
+ const activeMerchantId = freshResult?.merchantId || tokenData?.MerchantId;
175
+
176
+ if (!activeTokenData?.AccountKey) return;
177
+
178
+ const accountService = new AccountService(activeToken, activeMerchantId);
163
179
  const response = await accountService.linkToMerchant({
164
- accountKey: tokenData.AccountKey
180
+ accountKey: activeTokenData.AccountKey
165
181
  });
166
182
 
167
183
  const result = await handleMasterpassResponse(response);
@@ -177,7 +193,6 @@ export const useMasterpassAccount = () => {
177
193
  );
178
194
  } else if (result.success) {
179
195
  dispatch(setShowLinkModal(false));
180
- await refreshToken();
181
196
  await refreshAccountData();
182
197
  } else {
183
198
  dispatch(setShowLinkModal(false));
@@ -185,17 +200,34 @@ export const useMasterpassAccount = () => {
185
200
  } catch (error) {
186
201
  dispatch(setShowLinkModal(false));
187
202
  }
188
- }, [tokenData?.AccountKey, dispatch, refreshToken, refreshAccountData]);
203
+ }, [tokenData, token, dispatch, refreshToken, refreshAccountData]);
189
204
 
190
205
  const handleOTPSubmit = useCallback(
191
- async (otp: string) => {
192
- const result = await handleVerification(otp);
206
+ async (otp: string, texts?: any) => {
207
+ const result = await handleVerification(otp, token, tokenData?.MerchantId);
193
208
 
194
209
  if (result.success) {
195
210
  dispatch(setShowOTPModal(false));
196
- await refreshToken();
197
211
  await refreshAccountData();
198
212
  return { success: true };
213
+ } else if (result.sessionExpired) {
214
+ dispatch(setShowOTPModal(false));
215
+ dispatch(
216
+ updateModalState({
217
+ showInformationModal: true,
218
+ informationModalData: {
219
+ type: 'warning',
220
+ title: texts?.sessionExpiredTitle || 'Session Expired',
221
+ message: texts?.sessionExpiredMessage || 'Your session has expired due to inactivity. Please restart the process to continue.',
222
+ secondaryMessage: texts?.sessionExpiredSecondaryMessage || 'For security reasons, verification codes are only valid for a limited time.',
223
+ buttonText: texts?.sessionExpiredButton || 'Start Again'
224
+ }
225
+ })
226
+ );
227
+ return {
228
+ success: false,
229
+ sessionExpired: true
230
+ };
199
231
  } else if (result.requires3D && result.redirectUrl) {
200
232
  dispatch(setShowOTPModal(false));
201
233
  window.location.href = result.redirectUrl;
@@ -216,10 +248,7 @@ export const useMasterpassAccount = () => {
216
248
  return {
217
249
  success: false,
218
250
  requiresOTP: true,
219
- otpType: result.otpType,
220
- message:
221
- result.message ||
222
- 'Additional verification required. Please enter the new OTP code.'
251
+ otpType: result.otpType
223
252
  };
224
253
  } else {
225
254
  return {
@@ -228,7 +257,7 @@ export const useMasterpassAccount = () => {
228
257
  };
229
258
  }
230
259
  },
231
- [dispatch, refreshAccountData, refreshToken]
260
+ [dispatch, refreshAccountData, token, tokenData?.MerchantId]
232
261
  );
233
262
 
234
263
  const handleRemoveCard = useCallback(
@@ -245,7 +274,7 @@ export const useMasterpassAccount = () => {
245
274
  dispatch(setRemovingCardId(cardToDelete.uniqueCardNumber));
246
275
 
247
276
  try {
248
- const accountService = new AccountService();
277
+ const accountService = new AccountService(token, tokenData?.MerchantId);
249
278
  const response = await accountService.removeCard({
250
279
  accountKey: tokenData.AccountKey,
251
280
  cardAlias: cardToDelete.cardAlias
@@ -276,6 +305,8 @@ export const useMasterpassAccount = () => {
276
305
  }, [
277
306
  modalState.cardToDelete,
278
307
  tokenData?.AccountKey,
308
+ tokenData?.MerchantId,
309
+ token,
279
310
  dispatch,
280
311
  refreshAccountData
281
312
  ]);
@@ -288,10 +319,6 @@ export const useMasterpassAccount = () => {
288
319
  cvv: string;
289
320
  cardAlias: string;
290
321
  }) => {
291
- if (!tokenData?.AccountKey) {
292
- throw new Error('Account key not available');
293
- }
294
-
295
322
  const finalCardData = cardData || {
296
323
  cardNumber: newCardFormData.cardNumber || '',
297
324
  cardholderName: newCardFormData.cardholderName || '',
@@ -301,7 +328,17 @@ export const useMasterpassAccount = () => {
301
328
  };
302
329
 
303
330
  try {
304
- const accountService = new AccountService();
331
+ const freshResult = await refreshToken({ three_d: false });
332
+
333
+ const activeTokenData = freshResult?.tokenData || tokenData;
334
+ const activeToken = freshResult?.token || token;
335
+ const activeMerchantId = freshResult?.merchantId || tokenData?.MerchantId;
336
+
337
+ if (!activeTokenData?.AccountKey) {
338
+ throw new Error('Token data not available');
339
+ }
340
+
341
+ const accountService = new AccountService(activeToken, activeMerchantId);
305
342
 
306
343
  const timestamp = Date.now().toString().slice(-10);
307
344
  const random = Math.floor(Math.random() * 1000)
@@ -314,16 +351,16 @@ export const useMasterpassAccount = () => {
314
351
 
315
352
  const response = await accountService.addCard({
316
353
  requestReferenceNumber,
317
- accountKey: tokenData.AccountKey,
354
+ accountKey: activeTokenData.AccountKey,
318
355
  accountKeyType: 'Msisdn',
319
356
  cardNumber: finalCardData.cardNumber.replace(/\D/g, ''),
320
357
  cardHolderName: finalCardData.cardholderName,
321
358
  expiryDate: formattedExpiryDate,
322
359
  cvv: finalCardData.cvv,
323
360
  accountAliasName: finalCardData.cardAlias,
324
- userId: tokenData.UserId,
325
- isMsisdnValidatedByMerchant: tokenData.IsMsisdnValidated === 'True',
326
- authenticationMethod: tokenData.AuthenticationMethod
361
+ userId: activeTokenData.UserId,
362
+ isMsisdnValidatedByMerchant: activeTokenData.IsMsisdnValidated === 'True',
363
+ authenticationMethod: activeTokenData.AuthenticationMethod
327
364
  });
328
365
 
329
366
  const result = await handleMasterpassResponse(response);
@@ -344,6 +381,7 @@ export const useMasterpassAccount = () => {
344
381
 
345
382
  return result;
346
383
  } else if (result.success) {
384
+ await new Promise((resolve) => setTimeout(resolve, 1000));
347
385
  await refreshAccountData();
348
386
  return { success: true, data: result.data };
349
387
  }
@@ -372,6 +410,7 @@ export const useMasterpassAccount = () => {
372
410
  tokenData?.AuthenticationMethod,
373
411
  newCardFormData,
374
412
  refreshAccountData,
413
+ refreshToken,
375
414
  dispatch
376
415
  ]
377
416
  );
@@ -388,6 +427,7 @@ export const useMasterpassAccount = () => {
388
427
  updateModalState: updateModalStateAction,
389
428
  initializeAccount,
390
429
  refreshAccountData,
430
+ refreshToken,
391
431
  handleLinkConfirm,
392
432
  handleOTPSubmit,
393
433
  handleRemoveCard,