@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.
@@ -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
+ };