@akinon/pz-otp 1.15.0 → 1.16.1
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 +4 -0
- package/package.json +1 -1
- package/readme.md +25 -25
- package/src/views/Otp.tsx +218 -218
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
# pz-otp
|
|
2
|
-
|
|
3
|
-
### Install the npm package
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
# For latest version
|
|
7
|
-
yarn add git+ssh://git@bitbucket.org:akinonteam/pz-otp.git
|
|
8
|
-
|
|
9
|
-
# For specific version
|
|
10
|
-
yarn add git+ssh://git@bitbucket.org:akinonteam/pz-otp.git#eX4mPl3
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
### Example Usage
|
|
14
|
-
##### File Path: src/views/register/index.tsx
|
|
15
|
-
|
|
16
|
-
```javascript
|
|
17
|
-
import { Otp } from 'pz-otp';
|
|
18
|
-
|
|
19
|
-
export default function Register() {
|
|
20
|
-
return (
|
|
21
|
-
// ...
|
|
22
|
-
<Otp />
|
|
23
|
-
//
|
|
24
|
-
);
|
|
25
|
-
}
|
|
1
|
+
# pz-otp
|
|
2
|
+
|
|
3
|
+
### Install the npm package
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
# For latest version
|
|
7
|
+
yarn add git+ssh://git@bitbucket.org:akinonteam/pz-otp.git
|
|
8
|
+
|
|
9
|
+
# For specific version
|
|
10
|
+
yarn add git+ssh://git@bitbucket.org:akinonteam/pz-otp.git#eX4mPl3
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Example Usage
|
|
14
|
+
##### File Path: src/views/register/index.tsx
|
|
15
|
+
|
|
16
|
+
```javascript
|
|
17
|
+
import { Otp } from 'pz-otp';
|
|
18
|
+
|
|
19
|
+
export default function Register() {
|
|
20
|
+
return (
|
|
21
|
+
// ...
|
|
22
|
+
<Otp />
|
|
23
|
+
//
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
26
|
```
|
package/src/views/Otp.tsx
CHANGED
|
@@ -1,218 +1,218 @@
|
|
|
1
|
-
import { FormEvent, ReactNode, useEffect, useState } from 'react';
|
|
2
|
-
import { useRouter } from '@akinon/next/hooks';
|
|
3
|
-
import { Button } from '@akinon/next/components/button';
|
|
4
|
-
import OtpInput from 'react-otp-input';
|
|
5
|
-
import { SubmitHandler } from 'react-hook-form';
|
|
6
|
-
import { twMerge } from 'tailwind-merge';
|
|
7
|
-
|
|
8
|
-
enum Component {
|
|
9
|
-
Title = 'title',
|
|
10
|
-
Description = 'description',
|
|
11
|
-
SubmitButton = 'submitButton',
|
|
12
|
-
ResendText = 'resendText',
|
|
13
|
-
ResendButton = 'resendButton',
|
|
14
|
-
Input = 'input'
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
enum ComponentWrapper {
|
|
18
|
-
Title = 'title',
|
|
19
|
-
Form = 'form',
|
|
20
|
-
Resend = 'resend',
|
|
21
|
-
Otp = 'otp'
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
type ComponentClassKey = (typeof Component)[keyof typeof Component];
|
|
25
|
-
type ComponentWrapperClassKey =
|
|
26
|
-
`${(typeof ComponentWrapper)[keyof typeof ComponentWrapper]}Wrapper`;
|
|
27
|
-
type ComponentClasses = ComponentClassKey | ComponentWrapperClassKey;
|
|
28
|
-
|
|
29
|
-
type OtpProps = {
|
|
30
|
-
codeLength?: number;
|
|
31
|
-
timer?: number;
|
|
32
|
-
setShowPopup: any;
|
|
33
|
-
submitAction: SubmitHandler<{
|
|
34
|
-
[key: string]: any;
|
|
35
|
-
}>;
|
|
36
|
-
data: {
|
|
37
|
-
[key: string]: any;
|
|
38
|
-
};
|
|
39
|
-
texts?: {
|
|
40
|
-
[c in Component]?: string | ReactNode;
|
|
41
|
-
};
|
|
42
|
-
classes?: {
|
|
43
|
-
[c in ComponentClasses]?: string;
|
|
44
|
-
};
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export const Otp = ({
|
|
48
|
-
submitAction,
|
|
49
|
-
data,
|
|
50
|
-
codeLength = 6,
|
|
51
|
-
timer = 60,
|
|
52
|
-
setShowPopup,
|
|
53
|
-
texts,
|
|
54
|
-
classes
|
|
55
|
-
}: OtpProps) => {
|
|
56
|
-
const router = useRouter();
|
|
57
|
-
|
|
58
|
-
const [otp, setOtp] = useState('');
|
|
59
|
-
const [canResend, setCanResend] = useState(false);
|
|
60
|
-
const [time, setTime] = useState(timer);
|
|
61
|
-
const [hasError, setHasError] = useState(false);
|
|
62
|
-
|
|
63
|
-
const resetTimer = () => {
|
|
64
|
-
setTime(timer);
|
|
65
|
-
setCanResend(false);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
|
69
|
-
event.preventDefault();
|
|
70
|
-
|
|
71
|
-
if (submitAction) {
|
|
72
|
-
data.code = otp;
|
|
73
|
-
const res = await submitAction(data);
|
|
74
|
-
|
|
75
|
-
if (res.status !== 200) {
|
|
76
|
-
setHasError(true);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const closeHandler = () => {
|
|
82
|
-
if (setShowPopup) {
|
|
83
|
-
setShowPopup(false);
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const resendHandler = async () => {
|
|
88
|
-
if (!canResend) return;
|
|
89
|
-
|
|
90
|
-
if (submitAction) {
|
|
91
|
-
resetTimer();
|
|
92
|
-
data.resend = true;
|
|
93
|
-
await submitAction(data);
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const onTimerEnd = () => {
|
|
98
|
-
setCanResend(true);
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
useEffect(() => {
|
|
102
|
-
if (time > 0) {
|
|
103
|
-
const timerId = setInterval(() => {
|
|
104
|
-
setTime(time - 1);
|
|
105
|
-
}, 1000);
|
|
106
|
-
|
|
107
|
-
return () => clearInterval(timerId);
|
|
108
|
-
} else {
|
|
109
|
-
onTimerEnd();
|
|
110
|
-
}
|
|
111
|
-
}, [time, onTimerEnd]);
|
|
112
|
-
|
|
113
|
-
useEffect(() => {
|
|
114
|
-
document.body.style.overflow = 'hidden';
|
|
115
|
-
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
116
|
-
|
|
117
|
-
return () => {
|
|
118
|
-
document.body.style.overflow = 'auto';
|
|
119
|
-
};
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
return (
|
|
123
|
-
<div className="fixed left-0 top-0 z-50 flex h-screen w-screen items-end md:items-center md:justify-center md:bg-black/10">
|
|
124
|
-
<div className="h-[calc(100vh-48px)] w-screen flex md:h-auto md:max-w-lg flex-col items-center rounded-sm bg-white p-8 shadow-xl">
|
|
125
|
-
<div className="w-full flex items-center justify-end">
|
|
126
|
-
<div className="cursor-pointer" onClick={closeHandler}>
|
|
127
|
-
<svg
|
|
128
|
-
width="14"
|
|
129
|
-
height="14"
|
|
130
|
-
viewBox="0 0 14 14"
|
|
131
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
132
|
-
>
|
|
133
|
-
<g fill="#000" fill-rule="nonzero">
|
|
134
|
-
<path d="M.684 14A.684.684 0 0 1 .2 12.833L12.833.2a.684.684 0 1 1 .967.967L1.167 13.8a.682.682 0 0 1-.483.2z" />
|
|
135
|
-
<path d="M13.316 14a.682.682 0 0 1-.483-.2L.2 1.167A.684.684 0 0 1 1.167.2L13.8 12.833A.684.684 0 0 1 13.316 14z" />
|
|
136
|
-
</g>
|
|
137
|
-
</svg>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
<div
|
|
141
|
-
className={twMerge(
|
|
142
|
-
'flex flex-col items-center px-14 mt-5',
|
|
143
|
-
classes?.titleWrapper
|
|
144
|
-
)}
|
|
145
|
-
>
|
|
146
|
-
<div className={twMerge('text-2xl font-medium', classes?.title)}>
|
|
147
|
-
{texts?.title ?? 'OTP Verification'}
|
|
148
|
-
</div>
|
|
149
|
-
<div
|
|
150
|
-
className={twMerge(
|
|
151
|
-
'mt-2.5 text-center text-gray-700',
|
|
152
|
-
classes?.description
|
|
153
|
-
)}
|
|
154
|
-
>
|
|
155
|
-
{texts?.description ??
|
|
156
|
-
`Please enter the 4-digit sms code sent to your registered number and email address`}
|
|
157
|
-
</div>
|
|
158
|
-
</div>
|
|
159
|
-
<form
|
|
160
|
-
onSubmit={onSubmit}
|
|
161
|
-
className={twMerge(
|
|
162
|
-
'flex flex-col items-center w-full',
|
|
163
|
-
classes?.formWrapper
|
|
164
|
-
)}
|
|
165
|
-
>
|
|
166
|
-
<OtpInput
|
|
167
|
-
value={otp}
|
|
168
|
-
onChange={(otp) => {
|
|
169
|
-
setOtp(otp);
|
|
170
|
-
setHasError(false);
|
|
171
|
-
}}
|
|
172
|
-
numInputs={codeLength}
|
|
173
|
-
containerStyle={twMerge(
|
|
174
|
-
'mt-12 gap-2 flex-wrap',
|
|
175
|
-
classes?.otpWrapper
|
|
176
|
-
)}
|
|
177
|
-
inputStyle={twMerge(
|
|
178
|
-
'h-12 w-8 md:h-16 md:w-12 rounded-md border border-gray-600 text-center text-lg',
|
|
179
|
-
hasError && 'border-error',
|
|
180
|
-
classes?.input
|
|
181
|
-
)}
|
|
182
|
-
renderInput={({ style, ...props }) => <input {...props} />}
|
|
183
|
-
skipDefaultStyles={true}
|
|
184
|
-
/>
|
|
185
|
-
<Button
|
|
186
|
-
type="submit"
|
|
187
|
-
className={twMerge(
|
|
188
|
-
'mt-5 h-auto w-full py-4 text-lg font-medium uppercase',
|
|
189
|
-
classes?.submitButton
|
|
190
|
-
)}
|
|
191
|
-
>
|
|
192
|
-
{texts?.submitButton ?? 'Verify'}
|
|
193
|
-
</Button>
|
|
194
|
-
</form>
|
|
195
|
-
<div
|
|
196
|
-
className={twMerge(
|
|
197
|
-
'mt-6 flex flex-col items-center',
|
|
198
|
-
classes?.resendWrapper
|
|
199
|
-
)}
|
|
200
|
-
>
|
|
201
|
-
<span className={twMerge('text-gray-700', classes?.resendText)}>
|
|
202
|
-
{texts?.resendText ?? 'I didn`t receive a code'}
|
|
203
|
-
</span>
|
|
204
|
-
<div
|
|
205
|
-
className={twMerge(
|
|
206
|
-
' font-medium underline cursor-pointer',
|
|
207
|
-
!canResend && 'cursor-not-allowed text-gray-700',
|
|
208
|
-
classes?.resendButton
|
|
209
|
-
)}
|
|
210
|
-
onClick={resendHandler}
|
|
211
|
-
>
|
|
212
|
-
{texts?.resendButton ?? 'RESEND'}
|
|
213
|
-
</div>
|
|
214
|
-
</div>
|
|
215
|
-
</div>
|
|
216
|
-
</div>
|
|
217
|
-
);
|
|
218
|
-
};
|
|
1
|
+
import { FormEvent, ReactNode, useEffect, useState } from 'react';
|
|
2
|
+
import { useRouter } from '@akinon/next/hooks';
|
|
3
|
+
import { Button } from '@akinon/next/components/button';
|
|
4
|
+
import OtpInput from 'react-otp-input';
|
|
5
|
+
import { SubmitHandler } from 'react-hook-form';
|
|
6
|
+
import { twMerge } from 'tailwind-merge';
|
|
7
|
+
|
|
8
|
+
enum Component {
|
|
9
|
+
Title = 'title',
|
|
10
|
+
Description = 'description',
|
|
11
|
+
SubmitButton = 'submitButton',
|
|
12
|
+
ResendText = 'resendText',
|
|
13
|
+
ResendButton = 'resendButton',
|
|
14
|
+
Input = 'input'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
enum ComponentWrapper {
|
|
18
|
+
Title = 'title',
|
|
19
|
+
Form = 'form',
|
|
20
|
+
Resend = 'resend',
|
|
21
|
+
Otp = 'otp'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type ComponentClassKey = (typeof Component)[keyof typeof Component];
|
|
25
|
+
type ComponentWrapperClassKey =
|
|
26
|
+
`${(typeof ComponentWrapper)[keyof typeof ComponentWrapper]}Wrapper`;
|
|
27
|
+
type ComponentClasses = ComponentClassKey | ComponentWrapperClassKey;
|
|
28
|
+
|
|
29
|
+
type OtpProps = {
|
|
30
|
+
codeLength?: number;
|
|
31
|
+
timer?: number;
|
|
32
|
+
setShowPopup: any;
|
|
33
|
+
submitAction: SubmitHandler<{
|
|
34
|
+
[key: string]: any;
|
|
35
|
+
}>;
|
|
36
|
+
data: {
|
|
37
|
+
[key: string]: any;
|
|
38
|
+
};
|
|
39
|
+
texts?: {
|
|
40
|
+
[c in Component]?: string | ReactNode;
|
|
41
|
+
};
|
|
42
|
+
classes?: {
|
|
43
|
+
[c in ComponentClasses]?: string;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const Otp = ({
|
|
48
|
+
submitAction,
|
|
49
|
+
data,
|
|
50
|
+
codeLength = 6,
|
|
51
|
+
timer = 60,
|
|
52
|
+
setShowPopup,
|
|
53
|
+
texts,
|
|
54
|
+
classes
|
|
55
|
+
}: OtpProps) => {
|
|
56
|
+
const router = useRouter();
|
|
57
|
+
|
|
58
|
+
const [otp, setOtp] = useState('');
|
|
59
|
+
const [canResend, setCanResend] = useState(false);
|
|
60
|
+
const [time, setTime] = useState(timer);
|
|
61
|
+
const [hasError, setHasError] = useState(false);
|
|
62
|
+
|
|
63
|
+
const resetTimer = () => {
|
|
64
|
+
setTime(timer);
|
|
65
|
+
setCanResend(false);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
|
|
71
|
+
if (submitAction) {
|
|
72
|
+
data.code = otp;
|
|
73
|
+
const res = await submitAction(data);
|
|
74
|
+
|
|
75
|
+
if (res.status !== 200) {
|
|
76
|
+
setHasError(true);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const closeHandler = () => {
|
|
82
|
+
if (setShowPopup) {
|
|
83
|
+
setShowPopup(false);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const resendHandler = async () => {
|
|
88
|
+
if (!canResend) return;
|
|
89
|
+
|
|
90
|
+
if (submitAction) {
|
|
91
|
+
resetTimer();
|
|
92
|
+
data.resend = true;
|
|
93
|
+
await submitAction(data);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const onTimerEnd = () => {
|
|
98
|
+
setCanResend(true);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (time > 0) {
|
|
103
|
+
const timerId = setInterval(() => {
|
|
104
|
+
setTime(time - 1);
|
|
105
|
+
}, 1000);
|
|
106
|
+
|
|
107
|
+
return () => clearInterval(timerId);
|
|
108
|
+
} else {
|
|
109
|
+
onTimerEnd();
|
|
110
|
+
}
|
|
111
|
+
}, [time, onTimerEnd]);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
document.body.style.overflow = 'hidden';
|
|
115
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
document.body.style.overflow = 'auto';
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div className="fixed left-0 top-0 z-50 flex h-screen w-screen items-end md:items-center md:justify-center md:bg-black/10">
|
|
124
|
+
<div className="h-[calc(100vh-48px)] w-screen flex md:h-auto md:max-w-lg flex-col items-center rounded-sm bg-white p-8 shadow-xl">
|
|
125
|
+
<div className="w-full flex items-center justify-end">
|
|
126
|
+
<div className="cursor-pointer" onClick={closeHandler}>
|
|
127
|
+
<svg
|
|
128
|
+
width="14"
|
|
129
|
+
height="14"
|
|
130
|
+
viewBox="0 0 14 14"
|
|
131
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
132
|
+
>
|
|
133
|
+
<g fill="#000" fill-rule="nonzero">
|
|
134
|
+
<path d="M.684 14A.684.684 0 0 1 .2 12.833L12.833.2a.684.684 0 1 1 .967.967L1.167 13.8a.682.682 0 0 1-.483.2z" />
|
|
135
|
+
<path d="M13.316 14a.682.682 0 0 1-.483-.2L.2 1.167A.684.684 0 0 1 1.167.2L13.8 12.833A.684.684 0 0 1 13.316 14z" />
|
|
136
|
+
</g>
|
|
137
|
+
</svg>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
<div
|
|
141
|
+
className={twMerge(
|
|
142
|
+
'flex flex-col items-center px-14 mt-5',
|
|
143
|
+
classes?.titleWrapper
|
|
144
|
+
)}
|
|
145
|
+
>
|
|
146
|
+
<div className={twMerge('text-2xl font-medium', classes?.title)}>
|
|
147
|
+
{texts?.title ?? 'OTP Verification'}
|
|
148
|
+
</div>
|
|
149
|
+
<div
|
|
150
|
+
className={twMerge(
|
|
151
|
+
'mt-2.5 text-center text-gray-700',
|
|
152
|
+
classes?.description
|
|
153
|
+
)}
|
|
154
|
+
>
|
|
155
|
+
{texts?.description ??
|
|
156
|
+
`Please enter the 4-digit sms code sent to your registered number and email address`}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
<form
|
|
160
|
+
onSubmit={onSubmit}
|
|
161
|
+
className={twMerge(
|
|
162
|
+
'flex flex-col items-center w-full',
|
|
163
|
+
classes?.formWrapper
|
|
164
|
+
)}
|
|
165
|
+
>
|
|
166
|
+
<OtpInput
|
|
167
|
+
value={otp}
|
|
168
|
+
onChange={(otp) => {
|
|
169
|
+
setOtp(otp);
|
|
170
|
+
setHasError(false);
|
|
171
|
+
}}
|
|
172
|
+
numInputs={codeLength}
|
|
173
|
+
containerStyle={twMerge(
|
|
174
|
+
'mt-12 gap-2 flex-wrap',
|
|
175
|
+
classes?.otpWrapper
|
|
176
|
+
)}
|
|
177
|
+
inputStyle={twMerge(
|
|
178
|
+
'h-12 w-8 md:h-16 md:w-12 rounded-md border border-gray-600 text-center text-lg',
|
|
179
|
+
hasError && 'border-error',
|
|
180
|
+
classes?.input
|
|
181
|
+
)}
|
|
182
|
+
renderInput={({ style, ...props }) => <input {...props} />}
|
|
183
|
+
skipDefaultStyles={true}
|
|
184
|
+
/>
|
|
185
|
+
<Button
|
|
186
|
+
type="submit"
|
|
187
|
+
className={twMerge(
|
|
188
|
+
'mt-5 h-auto w-full py-4 text-lg font-medium uppercase',
|
|
189
|
+
classes?.submitButton
|
|
190
|
+
)}
|
|
191
|
+
>
|
|
192
|
+
{texts?.submitButton ?? 'Verify'}
|
|
193
|
+
</Button>
|
|
194
|
+
</form>
|
|
195
|
+
<div
|
|
196
|
+
className={twMerge(
|
|
197
|
+
'mt-6 flex flex-col items-center',
|
|
198
|
+
classes?.resendWrapper
|
|
199
|
+
)}
|
|
200
|
+
>
|
|
201
|
+
<span className={twMerge('text-gray-700', classes?.resendText)}>
|
|
202
|
+
{texts?.resendText ?? 'I didn`t receive a code'}
|
|
203
|
+
</span>
|
|
204
|
+
<div
|
|
205
|
+
className={twMerge(
|
|
206
|
+
' font-medium underline cursor-pointer',
|
|
207
|
+
!canResend && 'cursor-not-allowed text-gray-700',
|
|
208
|
+
classes?.resendButton
|
|
209
|
+
)}
|
|
210
|
+
onClick={resendHandler}
|
|
211
|
+
>
|
|
212
|
+
{texts?.resendButton ?? 'RESEND'}
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
};
|