@akinon/pz-masterpass 1.19.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/.prettierrc +13 -0
- package/CHANGELOG.md +7 -0
- package/README.md +122 -0
- package/assets/img/mp_amex.jpg +0 -0
- package/assets/img/mp_mastercard.png +0 -0
- package/assets/img/mp_masterpass-logo.png +0 -0
- package/assets/img/mp_other.png +0 -0
- package/assets/img/mp_troy.png +0 -0
- package/assets/img/mp_visa.png +0 -0
- package/assets/mfs-client.min.js +3 -0
- package/assets/zepto.min.js +2 -0
- package/package.json +18 -0
- package/src/hooks/use-cards.ts +51 -0
- package/src/hooks/use-countdown.ts +28 -0
- package/src/hooks/use-delete-card.tsx +88 -0
- package/src/index.d.ts +3 -0
- package/src/index.ts +17 -0
- package/src/masterpass-provider.tsx +141 -0
- package/src/redux/reducer.ts +126 -0
- package/src/types/index.ts +111 -0
- package/src/utils/check-masterpass.ts +66 -0
- package/src/utils/get-masterpass-cards.ts +32 -0
- package/src/utils/index.ts +177 -0
- package/src/utils/init.ts +17 -0
- package/src/views/card-list/index.tsx +166 -0
- package/src/views/card-registration/index.tsx +250 -0
- package/src/views/countdown-timer/countdown-timer.tsx +43 -0
- package/src/views/delete-confirmation-modal/index.tsx +64 -0
- package/src/views/info-modal/index.tsx +102 -0
- package/src/views/link-modal/index.tsx +135 -0
- package/src/views/otp-modal/index.tsx +148 -0
- package/src/views/otp-modal/otp-form.tsx +86 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
4
|
+
import { Button, LoaderSpinner, Modal } from 'components';
|
|
5
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
6
|
+
import masterpassLogo from '../../../assets/img/mp_masterpass-logo.png';
|
|
7
|
+
import {
|
|
8
|
+
setAccountStatus,
|
|
9
|
+
setOtpModalVisible,
|
|
10
|
+
setOtpResponse
|
|
11
|
+
} from '../../redux/reducer';
|
|
12
|
+
import { MasterpassStatus } from '../../types';
|
|
13
|
+
import { formCreator } from '../../utils';
|
|
14
|
+
import { Image } from '@akinon/next/components/image';
|
|
15
|
+
|
|
16
|
+
const defaultTranslations = {
|
|
17
|
+
use_masterpass_cards:
|
|
18
|
+
'You have cards registered to your Masterpass account. Would you like to use your cards?',
|
|
19
|
+
use: 'Use'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface MasterpassLinkModalProps {
|
|
23
|
+
translations?: typeof defaultTranslations;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const MasterpassLinkModal = ({
|
|
27
|
+
translations
|
|
28
|
+
}: MasterpassLinkModalProps) => {
|
|
29
|
+
const { msisdn, token, accountStatus, otp, language } = useAppSelector(
|
|
30
|
+
(state) => state.masterpass
|
|
31
|
+
);
|
|
32
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
33
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
34
|
+
const [error, setError] = useState<string | null>(null);
|
|
35
|
+
const dispatch = useAppDispatch();
|
|
36
|
+
|
|
37
|
+
const onLinkButtonClick = useCallback(() => {
|
|
38
|
+
if (!msisdn || !token) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const fields = [
|
|
43
|
+
{ name: 'msisdn', value: msisdn },
|
|
44
|
+
{ name: 'token', value: token },
|
|
45
|
+
{ name: 'referenceNo', value: '' },
|
|
46
|
+
{ name: 'sendSmsLanguage', value: language },
|
|
47
|
+
{ name: 'sendSms', value: 'N' }
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const form = formCreator({
|
|
51
|
+
id: 'on-link-button-form',
|
|
52
|
+
fields
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
setError(null);
|
|
56
|
+
setIsLoading(true);
|
|
57
|
+
|
|
58
|
+
window.MFS?.linkCardToClient($(form), async (statusCode, response) => {
|
|
59
|
+
setIsLoading(false);
|
|
60
|
+
|
|
61
|
+
dispatch(
|
|
62
|
+
setOtpModalVisible(['5001', '5008'].includes(response.responseCode))
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (response.responseCode === '0000') {
|
|
66
|
+
setIsOpen(false);
|
|
67
|
+
} else {
|
|
68
|
+
setError(response.responseDescription);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}, [msisdn, token]);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (accountStatus === MasterpassStatus.ShowLinkModal) {
|
|
75
|
+
setIsOpen(true);
|
|
76
|
+
}
|
|
77
|
+
}, [accountStatus]);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (otp.isModalVisible) {
|
|
81
|
+
setIsOpen(false);
|
|
82
|
+
}
|
|
83
|
+
}, [otp.isModalVisible]);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (
|
|
87
|
+
otp.response?.responseCode === '0000' ||
|
|
88
|
+
otp.response?.responseCode === '3838' ||
|
|
89
|
+
otp.response?.responseCode === ''
|
|
90
|
+
) {
|
|
91
|
+
dispatch(setAccountStatus(MasterpassStatus.ListCards));
|
|
92
|
+
dispatch(setOtpResponse(undefined));
|
|
93
|
+
}
|
|
94
|
+
}, [otp.response]);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<Modal
|
|
98
|
+
portalId="masterpass-check-user"
|
|
99
|
+
title={
|
|
100
|
+
<Image
|
|
101
|
+
width={120}
|
|
102
|
+
height={25}
|
|
103
|
+
src={masterpassLogo.src}
|
|
104
|
+
alt="Masterpass Logo"
|
|
105
|
+
/>
|
|
106
|
+
}
|
|
107
|
+
className="w-full sm:w-[28rem] max-h-[90vh] overflow-y-auto"
|
|
108
|
+
open={isOpen}
|
|
109
|
+
setOpen={setIsOpen}
|
|
110
|
+
>
|
|
111
|
+
<div className="px-6">
|
|
112
|
+
<h3 className="text-center mt-4 text-base">
|
|
113
|
+
{translations?.use_masterpass_cards ??
|
|
114
|
+
defaultTranslations.use_masterpass_cards}
|
|
115
|
+
</h3>
|
|
116
|
+
<div className="flex flex-col gap-3 p-5 w-3/4 m-auto">
|
|
117
|
+
{isLoading ? (
|
|
118
|
+
<div className="flex items-center justify-center h-10">
|
|
119
|
+
<LoaderSpinner className="w-4 h-4" />
|
|
120
|
+
</div>
|
|
121
|
+
) : (
|
|
122
|
+
<Button
|
|
123
|
+
className="py-3 h-auto"
|
|
124
|
+
onClick={onLinkButtonClick}
|
|
125
|
+
disabled={isLoading}
|
|
126
|
+
>
|
|
127
|
+
{translations?.use ?? defaultTranslations.use}
|
|
128
|
+
</Button>
|
|
129
|
+
)}
|
|
130
|
+
{error && <p className="text-error text-xs text-center">{error}</p>}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</Modal>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Modal } from '@akinon/next/components';
|
|
4
|
+
import masterpassLogo from '../../../assets/img/mp_masterpass-logo.png';
|
|
5
|
+
|
|
6
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { formCreator } from '../../utils';
|
|
9
|
+
|
|
10
|
+
import CountdownTimer from '../countdown-timer/countdown-timer';
|
|
11
|
+
import { OtpForm, OtpFormProps } from './otp-form';
|
|
12
|
+
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
13
|
+
import { setOtpResponse } from '../../redux/reducer';
|
|
14
|
+
import { Image } from '@akinon/next/components/image';
|
|
15
|
+
|
|
16
|
+
export interface MasterpassOtpModalProps {
|
|
17
|
+
translations?: {
|
|
18
|
+
1999?: string;
|
|
19
|
+
5192?: string;
|
|
20
|
+
5001?: string;
|
|
21
|
+
5008?: string;
|
|
22
|
+
} & OtpFormProps['translations'];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const MasterpassOtpModal = ({
|
|
26
|
+
translations
|
|
27
|
+
}: MasterpassOtpModalProps) => {
|
|
28
|
+
const { token, otp, language } = useAppSelector((state) => state.masterpass);
|
|
29
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
30
|
+
const [otpError, setOtpError] = useState<string | null>(null);
|
|
31
|
+
const [modalTitle, setModalTitle] = useState(otp.modalTitle);
|
|
32
|
+
const [isBusy, setIsBusy] = useState(false);
|
|
33
|
+
const [otpTime, setOtpTime] = useState(0);
|
|
34
|
+
const [otpRef, setOtpRef] = useState<string | null>(null);
|
|
35
|
+
const dispatch = useAppDispatch();
|
|
36
|
+
|
|
37
|
+
const onFormSubmit = useCallback(
|
|
38
|
+
(data: { otp_code: string }) => {
|
|
39
|
+
if (!token || !language) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const fields = [
|
|
44
|
+
{ name: 'sendSmsLanguage', value: language },
|
|
45
|
+
{ name: 'token', value: token },
|
|
46
|
+
{ name: 'sendSms', value: 'N' },
|
|
47
|
+
{ name: 'validationCode', value: data.otp_code },
|
|
48
|
+
{ name: 'pinType', value: 'otp' }
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const form = formCreator({
|
|
52
|
+
id: 'otp-masterpass-form',
|
|
53
|
+
fields
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
setOtpError(null);
|
|
57
|
+
setIsBusy(true);
|
|
58
|
+
|
|
59
|
+
window.MFS.validateTransaction($(form), async (statusCode, response) => {
|
|
60
|
+
setIsBusy(false);
|
|
61
|
+
setOtpRef(response.referenceNo);
|
|
62
|
+
|
|
63
|
+
if (['5001', '5008'].includes(response.responseCode)) {
|
|
64
|
+
startOtpCountdown();
|
|
65
|
+
|
|
66
|
+
setModalTitle(
|
|
67
|
+
translations?.[response.responseCode] ||
|
|
68
|
+
response.responseDescription
|
|
69
|
+
);
|
|
70
|
+
} else if (response.responseDescription.length) {
|
|
71
|
+
setOtpError(response.responseDescription);
|
|
72
|
+
} else {
|
|
73
|
+
setIsModalOpen(false);
|
|
74
|
+
dispatch(setOtpResponse(response));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
[token, language]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const resendSms = () => {
|
|
82
|
+
const token = window.MFS.getLastToken();
|
|
83
|
+
|
|
84
|
+
if (!token && isBusy) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setIsBusy(true);
|
|
89
|
+
|
|
90
|
+
window.MFS.resendOtp(token, language, () => {
|
|
91
|
+
startOtpCountdown();
|
|
92
|
+
setIsBusy(false);
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const startOtpCountdown = () => {
|
|
97
|
+
const otpTimeout = 120000;
|
|
98
|
+
const nowInMs = new Date().getTime();
|
|
99
|
+
|
|
100
|
+
setOtpTime(otpTimeout + nowInMs);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
setIsModalOpen(otp.isModalVisible);
|
|
105
|
+
}, [otp.isModalVisible]);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (isModalOpen) {
|
|
109
|
+
startOtpCountdown();
|
|
110
|
+
}
|
|
111
|
+
}, [isModalOpen]);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<Modal
|
|
115
|
+
portalId="otp-masterpass"
|
|
116
|
+
title={
|
|
117
|
+
<Image
|
|
118
|
+
width={120}
|
|
119
|
+
height={21}
|
|
120
|
+
src={masterpassLogo.src}
|
|
121
|
+
alt="Masterpass Logo"
|
|
122
|
+
/>
|
|
123
|
+
}
|
|
124
|
+
open={isModalOpen}
|
|
125
|
+
setOpen={setIsModalOpen}
|
|
126
|
+
className="w-full sm:w-[28rem] max-h-[90vh] overflow-y-auto"
|
|
127
|
+
>
|
|
128
|
+
<div className="px-6 py-4">
|
|
129
|
+
<p className="text-center">{modalTitle}</p>
|
|
130
|
+
|
|
131
|
+
<OtpForm
|
|
132
|
+
formError={otpError}
|
|
133
|
+
onSubmit={onFormSubmit}
|
|
134
|
+
otpRef={otpRef}
|
|
135
|
+
translations={translations}
|
|
136
|
+
/>
|
|
137
|
+
|
|
138
|
+
<div className="mt-2 flex justify-center">
|
|
139
|
+
<CountdownTimer
|
|
140
|
+
resendSmsFetching={isBusy}
|
|
141
|
+
targetDate={otpTime}
|
|
142
|
+
resendSms={resendSms}
|
|
143
|
+
/>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</Modal>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { yupResolver } from '@hookform/resolvers/yup';
|
|
2
|
+
import { Button, Input } from 'components';
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { useForm } from 'react-hook-form';
|
|
5
|
+
import * as yup from 'yup';
|
|
6
|
+
|
|
7
|
+
const defaultTranslations = {
|
|
8
|
+
enter_the_verification_code: 'Enter the verification code',
|
|
9
|
+
sms_code: 'SMS Code',
|
|
10
|
+
verify: 'Verify'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export interface OtpFormProps {
|
|
14
|
+
onSubmit: (data: any) => void;
|
|
15
|
+
formError: string | null;
|
|
16
|
+
otpRef: string | null;
|
|
17
|
+
translations?: typeof defaultTranslations;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const OtpForm = ({
|
|
21
|
+
onSubmit,
|
|
22
|
+
formError,
|
|
23
|
+
otpRef,
|
|
24
|
+
translations
|
|
25
|
+
}: OtpFormProps) => {
|
|
26
|
+
const formSchema = () =>
|
|
27
|
+
yup.object().shape({
|
|
28
|
+
otp_code: yup
|
|
29
|
+
.string()
|
|
30
|
+
.max(
|
|
31
|
+
6,
|
|
32
|
+
translations?.enter_the_verification_code ??
|
|
33
|
+
defaultTranslations.enter_the_verification_code
|
|
34
|
+
)
|
|
35
|
+
.min(
|
|
36
|
+
6,
|
|
37
|
+
translations?.enter_the_verification_code ??
|
|
38
|
+
defaultTranslations.enter_the_verification_code
|
|
39
|
+
)
|
|
40
|
+
.required(
|
|
41
|
+
translations?.enter_the_verification_code ??
|
|
42
|
+
defaultTranslations.enter_the_verification_code
|
|
43
|
+
)
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const {
|
|
47
|
+
register,
|
|
48
|
+
handleSubmit,
|
|
49
|
+
control,
|
|
50
|
+
reset,
|
|
51
|
+
formState: { errors }
|
|
52
|
+
} = useForm({
|
|
53
|
+
resolver: yupResolver(formSchema())
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
reset();
|
|
58
|
+
}, [otpRef]);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<form>
|
|
62
|
+
<div className="mt-2">
|
|
63
|
+
<Input
|
|
64
|
+
autoComplete="off"
|
|
65
|
+
minLength={6}
|
|
66
|
+
maxLength={6}
|
|
67
|
+
max="999999"
|
|
68
|
+
min="000000"
|
|
69
|
+
label={translations?.sms_code ?? defaultTranslations.sms_code}
|
|
70
|
+
control={control}
|
|
71
|
+
{...register('otp_code')}
|
|
72
|
+
error={errors.otp_code}
|
|
73
|
+
/>
|
|
74
|
+
<Button
|
|
75
|
+
onClick={handleSubmit(onSubmit)}
|
|
76
|
+
className="w-full uppercase mt-2"
|
|
77
|
+
>
|
|
78
|
+
{translations?.verify ?? defaultTranslations.verify}
|
|
79
|
+
</Button>
|
|
80
|
+
{formError && (
|
|
81
|
+
<p className="mt-2 text-error text-xs text-center">{formError}</p>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
</form>
|
|
85
|
+
);
|
|
86
|
+
};
|