Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@tanstack/react-query": "^5.100.9",
"axios": "^1.16.0",
"dayjs": "^1.11.20",
"html5-qrcode": "^2.3.8",
"i18next": "^26.0.1",
"lucide-react": "^1.7.0",
"qrcode.react": "^4.2.0",
Expand Down
2,982 changes: 2,212 additions & 770 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions src/components/NavBar/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ export const NAV_LINKS = [
{ id: 'lifestyle', href: '/lifeStyle', icon: Activity, key: 'nav.lifestyle' },
{ id: 'metrics', href: '/dailyReports', icon: ChartColumn, key: 'nav.metrics' },
]

export const NAV_ROUTES = {
metrics: '/dailyReports',
home: '/home',
} as const

export const CANT_GET_TOKEN = 'cant get token'
59 changes: 44 additions & 15 deletions src/pages/login/LoginPatient/LoginPatient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,65 @@ import { LoginForm } from '../LoginForm/LoginForm'
import { QuickLoginQRButton } from './QuickLoginQRButton/QuickLoginQRButton'
import { LoginSupportInfo } from './LoginSupportInfo/LoginSupportInfo'
import * as styles from './styles'
import { useNavigate } from 'react-router-dom'
import { QRScanner } from '../QRLogin/QRScanner'
import { QRGeneration } from '../QRLogin/QRGeneration'
import { SGLTypography } from '@/components/UI/Typography/SGLTypography'
import { SGLButton } from '@/components/UI/Button/SGLButton'
import { useQrLogin } from './useQrLogin'

export const LoginPatient = () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we extract all of the logic into custom hook?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please address @Tamir198 comment

const { t } = useTranslation()
const navigate = useNavigate()
const {
scannerOpen,
scannerError,
qrToken,
openScanner,
closeScanner,
generateQrToken,
handleQrSuccess,
handleBackToLogin,
} = useQrLogin()

const loginSchema = createLoginSchema(t)
const methods = useForm<LoginFormSchema>({
resolver: zodResolver(loginSchema),
})

const onSubmit = (data: LoginFormSchema) => {
console.log('Final Number:', data.serializedNumber)
navigate('/home')
generateQrToken(data.serializedNumber)
methods.reset()
}
Comment thread
beny25585 marked this conversation as resolved.

return (
<div style={styles.loginPatientStyles}>
<FormProvider {...methods}>
<LoginForm
title={t('login.enterPatientNumber')}
inputText={t('login.example')}
buttonText={t('login.sendCode')}
onSubmit={methods.handleSubmit(onSubmit)}
/>
</FormProvider>
<SGLDividerWithText text={t('login.quickLogin')} />
<QuickLoginQRButton buttonText={t('login.scanQr')} />
<LoginSupportInfo text={t('login.supportInfo')} />
{scannerOpen ? (
<QRScanner onSuccess={handleQrSuccess} onClose={closeScanner} />
) : !qrToken ? (
<>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need this fragment?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please address @Tamir198 comment

d5d27ff#r3370058583

<FormProvider {...methods}>
<LoginForm
title={t('login.enterPatientNumber')}
inputText={t('login.example')}
buttonText={t('login.sendCode')}
onSubmit={methods.handleSubmit(onSubmit)}
/>
</FormProvider>
<SGLDividerWithText text={t('login.quickLogin')} />
<QuickLoginQRButton buttonText={t('login.scanQr')} onClick={openScanner} />
{scannerError && (
<SGLTypography styles={styles.errorText} variant="smallText">
{scannerError}
</SGLTypography>
)}
<LoginSupportInfo text={t('login.supportInfo')} />
</>
) : (
<div style={styles.qrCardContainer}>
<SGLTypography variant="mediumTitle">{t('login.scanQrCode')}</SGLTypography>
<QRGeneration token={qrToken} />
<SGLButton onClick={handleBackToLogin}>{t('login.back')}</SGLButton>
</div>
)}
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { SGLQrIcon } from '@/components/UI/Icons/QR/SGLQrIcon'
import * as styles from '../styles'
interface QuickLoginQRButtonProps {
buttonText: string
onClick: () => void
}
export const QuickLoginQRButton = ({ buttonText }: QuickLoginQRButtonProps) => {
export const QuickLoginQRButton = ({ buttonText, onClick }: QuickLoginQRButtonProps) => {
return (
<SGLButton variant="outlined" onClick={() => {}} styles={styles.qrButton}>
<SGLButton variant="outlined" onClick={onClick} styles={styles.qrButton}>
<div style={styles.qrButtonContent}>
<SGLQrIcon />
<span>{buttonText}</span>
Expand Down
16 changes: 16 additions & 0 deletions src/pages/login/LoginPatient/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,19 @@ export const infoBox: CSSProperties = {
borderRadius: '1rem',
padding: '0.5rem',
}

export const qrCardContainer: CSSProperties = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '1.5rem',
padding: '2rem',
backgroundColor: theme.palette.lightGrey.main,
borderRadius: '1rem',
}

export const errorText: CSSProperties = {
color: theme.palette.error.main,
textAlign: 'center',
padding: '0.5rem',
}
60 changes: 60 additions & 0 deletions src/pages/login/LoginPatient/useQrLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useState, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import { createToken, verifyToken } from '@/services/qrService'
import { extractQrToken } from '@/utils/qrUtils'
import { CANT_GET_TOKEN, NAV_ROUTES } from '@/components/NavBar/constants'

export const useQrLogin = () => {
const { t } = useTranslation()
const navigate = useNavigate()
const [scannerOpen, setScannerOpen] = useState(false)
const [scannerError, setScannerError] = useState<string | null>(null)
const [qrToken, setQrToken] = useState<string>()

const openScanner = useCallback(() => {
setScannerError(null)
setScannerOpen(true)
}, [])

const closeScanner = useCallback(() => {
setScannerOpen(false)
}, [])

const generateQrToken = useCallback((userId: string) => {
const token = createToken(userId)
setQrToken(token)
}, [])

const handleQrSuccess = useCallback(
async (result: string) => {
try {
const token = extractQrToken(result)
if (!token) {
throw new Error(CANT_GET_TOKEN)
}
await verifyToken(token)
navigate(NAV_ROUTES.home)
} catch {
setScannerError(t('login.qrLoginFailed'))
setScannerOpen(false)
}
},
[navigate, t],
)

const handleBackToLogin = useCallback(() => {
setQrToken(undefined)
}, [])

return {
scannerOpen,
scannerError,
qrToken,
openScanner,
closeScanner,
generateQrToken,
handleQrSuccess,
handleBackToLogin,
}
}
65 changes: 65 additions & 0 deletions src/pages/login/QRLogin/QRScanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Html5Qrcode } from 'html5-qrcode'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import * as styles from './styles'
import { SGLButton } from '@/components/UI/Button/SGLButton'

interface QRScannerProps {
onSuccess: (value: string) => void
onClose?: () => void
}

export const QRScanner = ({ onSuccess, onClose }: QRScannerProps) => {
const { t } = useTranslation()
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)

useEffect(() => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need this useEffect? you have no dependency array making this re render every single time

Also why are you declaring functions inside this useEffect only to use them later outside of it?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const qr = new Html5Qrcode('reader')
let stopped = false
let started = false

const handleDecode = async (decodedText: string) => {
if (stopped) return
stopped = true
await qr.stop().catch(() => undefined)
onSuccess(decodedText)
}

const startScanner = async () => {
try {
await qr.start(
{ facingMode: 'environment' },
{ fps: 20, qrbox: 250 },
handleDecode,
() => {},
)
started = true
setLoading(false)
} catch (err) {
setLoading(false)
setError((err as Error)?.message || t('login.cameraPermission'))
}
}
startScanner()

return () => {
if (started && !stopped) {
qr.stop().catch(() => undefined)
}
}
}, [])

return (
<div style={styles.scannerContainer}>
<div id="reader" style={styles.scannerReader} />
{onClose && (
<SGLButton onClick={onClose} styles={styles.closeButton}>
X

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be constant

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

</SGLButton>
)}
{loading && !error && <div style={styles.loadingOverlay}>{t('login.cameraPermission')}</div>}
{error && <div style={styles.loadingOverlay}>{error}</div>}
</div>
)
}
2 changes: 2 additions & 0 deletions src/pages/login/QRLogin/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export const QR_TOKEN_PARAM = 'token'

export const buildQrLoginUrl = (token: string) =>
`${window.location.origin}/qr-login?token=${encodeURIComponent(token)}`
42 changes: 42 additions & 0 deletions src/pages/login/QRLogin/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { CSSProperties } from '@mui/material'
import { theme } from '@/theme'

export const scannerContainer: CSSProperties = {
position: 'fixed',
inset: 0,
zIndex: 2,
backgroundColor: theme.palette.common.black,
}

export const scannerReader: CSSProperties = {
width: '100%',
height: '100%',
}

export const loadingOverlay: CSSProperties = {
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: theme.palette.common.white,
zIndex: 2,
}

export const closeButton: CSSProperties = {
position: 'absolute',
top: 16,
right: 16,
zIndex: 2,
background: theme.palette.common.black,
color: theme.palette.common.white,
border: 'none',
borderRadius: '50%',
width: 40,
height: 40,
fontSize: 24,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}
16 changes: 16 additions & 0 deletions src/services/qrService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const tokenStore = new Map<string, string>()

export const createToken = (userId: string): string => {
const token = crypto.randomUUID()
tokenStore.set(token, userId)
return token
}

export const verifyToken = async (token: string): Promise<{ userId: string }> => {
const userId = tokenStore.get(token)
tokenStore.delete(token)
if (!userId) {
throw new Error('invalid token')
}
return { userId }
}
Comment thread
GilHeller marked this conversation as resolved.
6 changes: 6 additions & 0 deletions src/utils/qrUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { QR_TOKEN_PARAM } from '@/pages/login/QRLogin/constants'

export const extractQrToken = (qrContent: string): string | null => {
const url = new URL(qrContent)
return url.searchParams.get(QR_TOKEN_PARAM)
}