feat: add otp api

This commit is contained in:
Koosha Lahouti
2025-08-06 11:16:31 -07:00
parent 3e23fae993
commit fb9d691f6a
3 changed files with 108 additions and 51 deletions

View File

@@ -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

View File

@@ -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}

View File

@@ -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;