feat: add otp api
This commit is contained in:
@@ -83,7 +83,15 @@ export function SubmitSection({ onSubmit, loading, error, success }: Props) {
|
|||||||
>
|
>
|
||||||
{t('completion.registerButton')}
|
{t('completion.registerButton')}
|
||||||
</Button> */}
|
</Button> */}
|
||||||
<Button variant="contained" onClick={onSubmit} disabled={loading}>
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={onSubmit}
|
||||||
|
disabled={loading}
|
||||||
|
sx={{
|
||||||
|
width: { xs: '100%', sm: '247px' },
|
||||||
|
alignSelf: { xs: 'stretch', sm: 'center' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
{loading
|
{loading
|
||||||
? t('completion.submitting')
|
? t('completion.submitting')
|
||||||
: success
|
: success
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Box, Typography } from '@mui/material';
|
import { Box, Typography, Button } from '@mui/material';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Logo from '@/components/Logo';
|
import Logo from '@/components/Logo';
|
||||||
import { PersonalInfoFields } from './PersonalInfoFields';
|
import { PersonalInfoFields } from './PersonalInfoFields';
|
||||||
@@ -7,11 +7,10 @@ import { PasswordSection } from './PasswordSection';
|
|||||||
import { EmailSection } from './EmailSection';
|
import { EmailSection } from './EmailSection';
|
||||||
import { SubmitSection } from './SubmitSection';
|
import { SubmitSection } from './SubmitSection';
|
||||||
import apiClient from '@/lib/apiClient';
|
import apiClient from '@/lib/apiClient';
|
||||||
import { loginWithPassword } from '@/lib/authToken';
|
import { sendEmailOtp, fetchAuthToken } from '@/lib/authToken';
|
||||||
|
|
||||||
export function UserCompletionForm() {
|
export function UserCompletionForm() {
|
||||||
const { t } = useTranslation('completionForm');
|
const { t } = useTranslation('completionForm');
|
||||||
const USERNAME = '+989353989651';
|
|
||||||
|
|
||||||
const [firstName, setFirstName] = useState('');
|
const [firstName, setFirstName] = useState('');
|
||||||
const [lastName, setLastName] = useState('');
|
const [lastName, setLastName] = useState('');
|
||||||
@@ -28,12 +27,13 @@ export function UserCompletionForm() {
|
|||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [codeSent, setCodeSent] = useState(false);
|
const [codeSent, setCodeSent] = useState(false);
|
||||||
const [verificationCode, setVerificationCode] = useState('');
|
const [verificationCode, setVerificationCode] = useState('');
|
||||||
const [buttonState, setButtonState] = useState<
|
const [buttonState, setButtonState] = useState<'default' | 'counting'>(
|
||||||
'default' | 'counting' | 'sent'
|
'default',
|
||||||
>('default');
|
);
|
||||||
const [countdown, setCountdown] = useState(60);
|
const [countdown, setCountdown] = useState(0);
|
||||||
const [emailVerified, setEmailVerified] = useState(false);
|
const [emailVerified, setEmailVerified] = useState(false);
|
||||||
const [isVerifyingCode, setIsVerifyingCode] = useState(false);
|
const [isVerifyingCode, setIsVerifyingCode] = useState(false);
|
||||||
|
|
||||||
const correctEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
const correctEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||||
|
|
||||||
const matchPassword = password === confirmPassword;
|
const matchPassword = password === confirmPassword;
|
||||||
@@ -65,10 +65,18 @@ export function UserCompletionForm() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timer: NodeJS.Timeout;
|
let timer: NodeJS.Timeout;
|
||||||
if (buttonState === 'counting' && countdown > 0) {
|
if (buttonState === 'counting' && countdown > 0) {
|
||||||
timer = setInterval(() => setCountdown((prev) => prev - 1), 1000);
|
timer = setInterval(
|
||||||
}
|
() =>
|
||||||
if (countdown === 0) {
|
setCountdown((prev) => {
|
||||||
setButtonState('default');
|
if (prev <= 1) {
|
||||||
|
setButtonState('default');
|
||||||
|
clearInterval(timer);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
}),
|
||||||
|
1000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, [buttonState, countdown]);
|
}, [buttonState, countdown]);
|
||||||
@@ -77,46 +85,58 @@ export function UserCompletionForm() {
|
|||||||
str.replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[parseInt(d, 10)]);
|
str.replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[parseInt(d, 10)]);
|
||||||
|
|
||||||
const getButtonLabel = () => {
|
const getButtonLabel = () => {
|
||||||
if (buttonState === 'sent') return t('completion.sent');
|
|
||||||
if (buttonState === 'counting') {
|
if (buttonState === 'counting') {
|
||||||
const minutes = String(Math.floor(countdown / 60)).padStart(2, '0');
|
const m = String(Math.floor(countdown / 60)).padStart(2, '0');
|
||||||
const seconds = String(countdown % 60).padStart(2, '0');
|
const s = String(countdown % 60).padStart(2, '0');
|
||||||
return toPersianDigits(`${minutes}:${seconds}`);
|
return toPersianDigits(`${m}:${s}`);
|
||||||
}
|
}
|
||||||
return t('completion.vericationCodeButton');
|
return t('completion.vericationCodeButton');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendCode = () => {
|
const handleSendCode = async () => {
|
||||||
setCodeSent(true);
|
setError(null);
|
||||||
setButtonState('sent');
|
setLoading(true);
|
||||||
setTimeout(() => {
|
setSuccess(false);
|
||||||
setButtonState('counting');
|
|
||||||
setCountdown(60);
|
try {
|
||||||
}, 500);
|
await sendEmailOtp(EMAIL);
|
||||||
|
setSuccess(true);
|
||||||
|
} catch {
|
||||||
|
setError('Failed to send OTP');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVerifyCode = () => {
|
const handleVerifyCode = async () => {
|
||||||
|
if (!verificationCode) return;
|
||||||
setIsVerifyingCode(true);
|
setIsVerifyingCode(true);
|
||||||
setTimeout(() => {
|
setError(null);
|
||||||
setIsVerifyingCode(false);
|
try {
|
||||||
|
const tokenRes = await fetchAuthToken(email, verificationCode);
|
||||||
|
localStorage.setItem('authToken', tokenRes.access_token);
|
||||||
|
apiClient.defaults.headers.common['Authorization'] =
|
||||||
|
`Bearer ${tokenRes.access_token}`;
|
||||||
setEmailVerified(true);
|
setEmailVerified(true);
|
||||||
}, 500);
|
} catch {
|
||||||
|
setError('Invalid verification code');
|
||||||
|
} finally {
|
||||||
|
setIsVerifyingCode(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditEmail = () => {
|
const handleEditEmail = () => {
|
||||||
setButtonState('default');
|
setButtonState('default');
|
||||||
setCodeSent(false);
|
setCodeSent(false);
|
||||||
setEmailVerified(false);
|
setEmailVerified(false);
|
||||||
|
setVerificationCode('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setSuccess(false);
|
setSuccess(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await loginWithPassword(USERNAME, password);
|
|
||||||
|
|
||||||
const { data } = await apiClient.post<{
|
const { data } = await apiClient.post<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
errorCode: number;
|
errorCode: number;
|
||||||
@@ -132,20 +152,24 @@ export function UserCompletionForm() {
|
|||||||
saveEmail: showEmail,
|
saveEmail: showEmail,
|
||||||
email: showEmail ? email : undefined,
|
email: showEmail ? email : undefined,
|
||||||
birthDate,
|
birthDate,
|
||||||
|
country,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
} else {
|
} else {
|
||||||
setError(data.message || 'Validation error');
|
setError(data.message || 'Validation error');
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || 'An error occurred');
|
setError(
|
||||||
|
err.response?.data?.message || err.message || 'An error occurred',
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const EMAIL = 'klahouti81@gmail.com';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -232,6 +256,10 @@ export function UserCompletionForm() {
|
|||||||
handleEditEmail={handleEditEmail}
|
handleEditEmail={handleEditEmail}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Button onClick={handleSendCode} variant="contained" disabled={loading}>
|
||||||
|
{loading ? 'Sending…' : 'Send OTP'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<SubmitSection
|
<SubmitSection
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
|||||||
@@ -1,33 +1,54 @@
|
|||||||
// src/lib/authService.ts
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export interface SendEmailOtpResponse {
|
||||||
|
success: boolean;
|
||||||
|
errorCode: number;
|
||||||
|
message: string;
|
||||||
|
validations: {
|
||||||
|
message: string;
|
||||||
|
code: number;
|
||||||
|
property: string;
|
||||||
|
severity: number;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface TokenResponse {
|
export interface TokenResponse {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
expires_in: number;
|
expires_in: number;
|
||||||
refresh_token: string;
|
refresh_token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authClient = axios.create({
|
const SEND_EMAIL_OTP_URL =
|
||||||
baseURL: 'https://account.business-harmony.com',
|
'https://account.business-harmony.com/api/User/SendEmailOtp';
|
||||||
timeout: 10000,
|
const TOKEN_URL = 'https://account.business-harmony.com/connect/token';
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function loginWithPassword(
|
export async function sendEmailOtp(
|
||||||
username: string,
|
email: string,
|
||||||
password: string,
|
): Promise<SendEmailOtpResponse> {
|
||||||
|
const { data } = await axios.post<SendEmailOtpResponse>(SEND_EMAIL_OTP_URL, {
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAuthToken(
|
||||||
|
email: string,
|
||||||
|
otpCode: string,
|
||||||
): Promise<TokenResponse> {
|
): Promise<TokenResponse> {
|
||||||
const body = new URLSearchParams();
|
await sendEmailOtp(email);
|
||||||
body.set('grant_type', 'password');
|
|
||||||
body.set('username', username);
|
|
||||||
body.set('password', password);
|
|
||||||
body.set('client_id', 'harmony_identity');
|
|
||||||
body.set('scope', 'openid harmony_identity profile offline_access');
|
|
||||||
|
|
||||||
const { data } = await authClient.post<TokenResponse>(
|
const body = new URLSearchParams({
|
||||||
'/connect/token',
|
grant_type: 'otp',
|
||||||
body.toString(),
|
client_id: 'harmony_identity',
|
||||||
);
|
phonenumber: '',
|
||||||
|
email,
|
||||||
|
otp_code: otpCode,
|
||||||
|
scope: 'openid profile offline_access harmony_identity',
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
const { data } = await axios.post<TokenResponse>(TOKEN_URL, body, {
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
});
|
||||||
|
|
||||||
localStorage.setItem('authToken', data.access_token);
|
localStorage.setItem('authToken', data.access_token);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
Reference in New Issue
Block a user