chore: auth verification (#2867) [skip e2e]

This commit is contained in:
Mo
2024-04-08 10:52:56 -05:00
committed by GitHub
parent a37e095907
commit b6eda707bd
30 changed files with 516 additions and 205 deletions

View File

@@ -16,6 +16,8 @@ import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput'
import Icon from '@/Components/Icon/Icon'
import IconButton from '@/Components/Button/IconButton'
import { useApplication } from '../ApplicationProvider'
import { useCaptcha } from '@/Hooks/useCaptcha'
import { isErrorResponse } from '@standardnotes/snjs'
type Props = {
setMenuPane: (pane: AccountMenuPane) => void
@@ -33,6 +35,39 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
const [shouldMergeLocal, setShouldMergeLocal] = useState(true)
const [error, setError] = useState('')
const [hvmToken, setHVMToken] = useState('')
const [captchaURL, setCaptchaURL] = useState('')
const register = useCallback(() => {
setIsRegistering(true)
application
.register(email, password, hvmToken, isEphemeral, shouldMergeLocal)
.then(() => {
application.accountMenuController.closeAccountMenu()
application.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu)
})
.catch((err) => {
console.error(err)
setError(err.message)
})
.finally(() => {
setIsRegistering(false)
})
}, [application, email, hvmToken, isEphemeral, password, shouldMergeLocal])
const captchaIframe = useCaptcha(captchaURL, (token) => {
setHVMToken(token)
setCaptchaURL('')
})
useEffect(() => {
if (!hvmToken) {
return
}
register()
}, [hvmToken, register])
const passwordInputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
@@ -51,6 +86,28 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
setShouldMergeLocal(!shouldMergeLocal)
}, [shouldMergeLocal])
const checkIfCaptchaRequiredAndRegister = useCallback(() => {
application
.getCaptchaUrl()
.then((response) => {
if (isErrorResponse(response)) {
throw new Error()
}
const { captchaUIUrl } = response.data
if (captchaUIUrl) {
setCaptchaURL(captchaUIUrl)
} else {
setCaptchaURL('')
register()
}
})
.catch((error) => {
console.error(error)
setCaptchaURL('')
register()
})
}, [application, register])
const handleConfirmFormSubmit: FormEventHandler = useCallback(
(e) => {
e.preventDefault()
@@ -60,28 +117,16 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
return
}
if (password === confirmPassword) {
setIsRegistering(true)
application
.register(email, password, isEphemeral, shouldMergeLocal)
.then(() => {
application.accountMenuController.closeAccountMenu()
application.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu)
})
.catch((err) => {
console.error(err)
setError(err.message)
})
.finally(() => {
setIsRegistering(false)
})
} else {
if (password !== confirmPassword) {
setError(STRING_NON_MATCHING_PASSWORDS)
setConfirmPassword('')
passwordInputRef.current?.focus()
return
}
checkIfCaptchaRequiredAndRegister()
},
[application, confirmPassword, email, isEphemeral, password, shouldMergeLocal],
[checkIfCaptchaRequiredAndRegister, confirmPassword, password],
)
const handleKeyDown: KeyboardEventHandler = useCallback(
@@ -100,35 +145,26 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
setMenuPane(AccountMenuPane.Register)
}, [setMenuPane])
return (
const confirmPasswordForm = (
<>
<div className="mb-3 mt-1 flex items-center px-3">
<IconButton
icon="arrow-left"
title="Go back"
className="mr-2 flex p-0 text-neutral"
onClick={handleGoBack}
focusable={true}
disabled={isRegistering}
/>
<div className="text-base font-bold">Confirm password</div>
</div>
<div className="mb-3 px-3 text-sm">
Because your notes are encrypted using your password,{' '}
<span className="text-danger">Standard Notes does not have a password reset option</span>. If you forget your
password, you will permanently lose access to your data.
</div>
<form onSubmit={handleConfirmFormSubmit} className="mb-1 px-3">
<DecoratedPasswordInput
className={{ container: 'mb-2' }}
disabled={isRegistering}
left={[<Icon type="password" className="text-neutral" />]}
onChange={handlePasswordChange}
onKeyDown={handleKeyDown}
placeholder="Confirm password"
ref={passwordInputRef}
value={confirmPassword}
/>
{!isRegistering && (
<DecoratedPasswordInput
className={{ container: 'mb-2' }}
disabled={isRegistering}
left={[<Icon type="password" className="text-neutral" />]}
onChange={handlePasswordChange}
onKeyDown={handleKeyDown}
placeholder="Confirm password"
ref={passwordInputRef}
value={confirmPassword}
/>
)}
{error ? <div className="my-2 text-danger">{error}</div> : null}
<Button
primary
@@ -157,6 +193,23 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
</form>
</>
)
return (
<>
<div className="mb-3 mt-1 flex items-center px-3">
<IconButton
icon="arrow-left"
title="Go back"
className="mr-2 flex p-0 text-neutral"
onClick={handleGoBack}
focusable={true}
disabled={isRegistering}
/>
<div className="text-base font-bold">{captchaURL ? 'Human verification' : 'Confirm password'}</div>
</div>
{captchaURL ? <div className="p-[10px]">{captchaIframe}</div> : confirmPasswordForm}
</>
)
}
export default observer(ConfirmPassword)

View File

@@ -10,8 +10,9 @@ import Icon from '@/Components/Icon/Icon'
import IconButton from '@/Components/Button/IconButton'
import AdvancedOptions from './AdvancedOptions'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { getErrorFromErrorResponse, isErrorResponse } from '@standardnotes/snjs'
import { getErrorFromErrorResponse, isErrorResponse, getCaptchaHeader } from '@standardnotes/snjs'
import { useApplication } from '../ApplicationProvider'
import { useCaptcha } from '@/Hooks/useCaptcha'
type Props = {
setMenuPane: (pane: AccountMenuPane) => void
@@ -34,6 +35,15 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
const [isRecoverySignIn, setIsRecoverySignIn] = useState(false)
const [captchaURL, setCaptchaURL] = useState('')
const [showCaptcha, setShowCaptcha] = useState(false)
const [hvmToken, setHVMToken] = useState('')
const captchaIframe = useCaptcha(captchaURL, (token) => {
setHVMToken(token)
setShowCaptcha(false)
setCaptchaURL('')
})
const emailInputRef = useRef<HTMLInputElement>(null)
const passwordInputRef = useRef<HTMLInputElement>(null)
@@ -95,8 +105,12 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
passwordInputRef?.current?.blur()
application
.signIn(email, password, isStrictSignin, isEphemeral, shouldMergeLocal)
.signIn(email, password, isStrictSignin, isEphemeral, shouldMergeLocal, false, hvmToken)
.then((response) => {
const captchaURL = getCaptchaHeader(response)
if (captchaURL) {
setCaptchaURL(captchaURL)
}
if (isErrorResponse(response)) {
throw new Error(getErrorFromErrorResponse(response).message)
}
@@ -106,12 +120,13 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
console.error(err)
setError(err.message ?? err.toString())
setPassword('')
setHVMToken('')
passwordInputRef?.current?.blur()
})
.finally(() => {
setIsSigningIn(false)
})
}, [application, email, isEphemeral, isStrictSignin, password, shouldMergeLocal])
}, [application, email, hvmToken, isEphemeral, isStrictSignin, password, shouldMergeLocal])
const recoverySignIn = useCallback(() => {
setIsSigningIn(true)
@@ -123,10 +138,21 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
recoveryCodes,
username: email,
password: password,
hvmToken,
})
.then((result) => {
if (result.isFailed()) {
throw new Error(result.getError())
const error = result.getError()
try {
const parsed = JSON.parse(error)
if (parsed.captchaURL) {
setCaptchaURL(parsed.captchaURL)
return
}
} catch (e) {
setCaptchaURL('')
}
throw new Error(error)
}
application.accountMenuController.closeAccountMenu()
})
@@ -134,12 +160,13 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
console.error(err)
setError(err.message ?? err.toString())
setPassword('')
setHVMToken('')
passwordInputRef?.current?.blur()
})
.finally(() => {
setIsSigningIn(false)
})
}, [application, email, password, recoveryCodes])
}, [application.accountMenuController, application.signInWithRecoveryCodes, email, hvmToken, password, recoveryCodes])
const onPrivateUsernameChange = useCallback(
(newisPrivateUsername: boolean, privateUsernameIdentifier?: string) => {
@@ -151,28 +178,37 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
[setEmail],
)
const performSignIn = useCallback(() => {
if (!email || email.length === 0) {
emailInputRef?.current?.focus()
return
}
if (!password || password.length === 0) {
passwordInputRef?.current?.focus()
return
}
if (isRecoverySignIn) {
recoverySignIn()
return
}
signIn()
}, [email, isRecoverySignIn, password, recoverySignIn, signIn])
const handleSignInFormSubmit = useCallback(
(e: React.SyntheticEvent) => {
e.preventDefault()
if (!email || email.length === 0) {
emailInputRef?.current?.focus()
if (captchaURL) {
setShowCaptcha(true)
return
}
if (!password || password.length === 0) {
passwordInputRef?.current?.focus()
return
}
if (isRecoverySignIn) {
recoverySignIn()
return
}
signIn()
performSignIn()
},
[email, password, isRecoverySignIn, signIn, recoverySignIn],
[captchaURL, performSignIn],
)
const handleKeyDown: KeyboardEventHandler = useCallback(
@@ -184,19 +220,16 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
[handleSignInFormSubmit],
)
return (
useEffect(() => {
if (!hvmToken) {
return
}
performSignIn()
}, [hvmToken, performSignIn])
const signInForm = (
<>
<div className="mb-3 mt-1 flex items-center px-3">
<IconButton
icon="arrow-left"
title="Go back"
className="mr-2 flex p-0 text-neutral"
onClick={() => setMenuPane(AccountMenuPane.GeneralMenu)}
focusable={true}
disabled={isSigningIn}
/>
<div className="text-base font-bold">Sign in</div>
</div>
<div className="mb-1 px-3">
<DecoratedInput
className={{ container: `mb-2 ${error ? 'border-danger' : null}` }}
@@ -257,6 +290,23 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
/>
</>
)
return (
<>
<div className="mb-3 mt-1 flex items-center px-3">
<IconButton
icon="arrow-left"
title="Go back"
className="mr-2 flex p-0 text-neutral"
onClick={() => setMenuPane(AccountMenuPane.GeneralMenu)}
focusable={true}
disabled={isSigningIn}
/>
<div className="text-base font-bold">{showCaptcha ? 'Human verification' : 'Sign in'}</div>
</div>
{showCaptcha ? <div className="p-[10px]">{captchaIframe}</div> : signInForm}
</>
)
}
export default observer(SignInPane)

View File

@@ -1,6 +1,13 @@
import { WebApplicationGroup } from '@/Application/WebApplicationGroup'
import { getPlatformString } from '@/Utils'
import { ApplicationEvent, Challenge, removeFromArray, WebAppEvent } from '@standardnotes/snjs'
import {
ApplicationEvent,
Challenge,
getErrorMessageFromErrorResponseBody,
HttpErrorResponseBody,
removeFromArray,
WebAppEvent,
} from '@standardnotes/snjs'
import { alertDialog, isIOS, RouteType } from '@standardnotes/ui-services'
import { WebApplication } from '@/Application/WebApplication'
import Footer from '@/Components/Footer/Footer'
@@ -117,7 +124,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
onAppLaunch()
}
const removeAppObserver = application.addEventObserver(async (eventName) => {
const removeAppObserver = application.addEventObserver(async (eventName, data?: unknown) => {
if (eventName === ApplicationEvent.Started) {
onAppStart()
} else if (eventName === ApplicationEvent.Launched) {
@@ -147,6 +154,14 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
type: ToastType.Error,
message: 'Too many requests. Please try again later.',
})
} else if (eventName === ApplicationEvent.FailedSync) {
addToast({
type: ToastType.Error,
message: getErrorMessageFromErrorResponseBody(
data as HttpErrorResponseBody,
'Sync error. Please try again later.',
),
})
}
})

View File

@@ -2,10 +2,13 @@ import Button from '@/Components/Button/Button'
import { WebApplication } from '@/Application/WebApplication'
import { PurchaseFlowPane } from '@/Controllers/PurchaseFlow/PurchaseFlowPane'
import { observer } from 'mobx-react-lite'
import { ChangeEventHandler, FunctionComponent, useEffect, useRef, useState } from 'react'
import { ChangeEventHandler, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import FloatingLabelInput from '@/Components/Input/FloatingLabelInput'
import { isEmailValid } from '@/Utils'
import { BlueDotIcon, CircleIcon, DiamondIcon, CreateAccountIllustration } from '@standardnotes/icons'
import { useCaptcha } from '@/Hooks/useCaptcha'
import { AccountMenuPane } from '../../AccountMenu/AccountMenuPane'
import { isErrorResponse } from '@standardnotes/snjs'
type Props = {
application: WebApplication
@@ -20,6 +23,61 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
const [isEmailInvalid, setIsEmailInvalid] = useState(false)
const [isPasswordNotMatching, setIsPasswordNotMatching] = useState(false)
const [hvmToken, setHVMToken] = useState('')
const [captchaURL, setCaptchaURL] = useState('')
const register = useCallback(() => {
setIsCreatingAccount(true)
application
.register(email, password, hvmToken)
.then(() => {
application.accountMenuController.closeAccountMenu()
application.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu)
})
.catch((err) => {
console.error(err)
application.alerts.alert(err as string).catch(console.error)
})
.finally(() => {
setIsCreatingAccount(false)
})
}, [application, email, hvmToken, password])
const captchaIframe = useCaptcha(captchaURL, (token) => {
setHVMToken(token)
setCaptchaURL('')
})
useEffect(() => {
if (!hvmToken) {
return
}
register()
}, [hvmToken, register])
const checkIfCaptchaRequiredAndRegister = useCallback(() => {
application
.getCaptchaUrl()
.then((response) => {
if (isErrorResponse(response)) {
throw new Error()
}
const { captchaUIUrl } = response.data
if (captchaUIUrl) {
setCaptchaURL(captchaUIUrl)
} else {
setCaptchaURL('')
register()
}
})
.catch((error) => {
console.error(error)
setCaptchaURL('')
register()
})
}, [application, register])
const emailInputRef = useRef<HTMLInputElement>(null)
const passwordInputRef = useRef<HTMLInputElement>(null)
const confirmPasswordInputRef = useRef<HTMLInputElement>(null)
@@ -81,21 +139,52 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
return
}
setIsCreatingAccount(true)
try {
await application.register(email, password)
application.purchaseFlowController.closePurchaseFlow()
void application.purchaseFlowController.openPurchaseFlow()
} catch (err) {
console.error(err)
application.alerts.alert(err as string).catch(console.error)
} finally {
setIsCreatingAccount(false)
}
checkIfCaptchaRequiredAndRegister()
}
const CreateAccountForm = (
<form onSubmit={handleCreateAccount}>
<div className="flex flex-col">
<FloatingLabelInput
className={`min-w-auto md:min-w-90 ${isEmailInvalid ? 'mb-2' : 'mb-4'}`}
id="purchase-sign-in-email"
type="email"
label="Email"
value={email}
onChange={handleEmailChange}
ref={emailInputRef}
disabled={isCreatingAccount}
isInvalid={isEmailInvalid}
/>
{isEmailInvalid ? <div className="mb-4 text-danger">Please provide a valid email.</div> : null}
<FloatingLabelInput
className="min-w-auto mb-4 md:min-w-90"
id="purchase-create-account-password"
type="password"
label="Password"
value={password}
onChange={handlePasswordChange}
ref={passwordInputRef}
disabled={isCreatingAccount}
/>
<FloatingLabelInput
className={`min-w-auto md:min-w-90 ${isPasswordNotMatching ? 'mb-2' : 'mb-4'}`}
id="create-account-confirm"
type="password"
label="Repeat password"
value={confirmPassword}
onChange={handleConfirmPasswordChange}
ref={confirmPasswordInputRef}
disabled={isCreatingAccount}
isInvalid={isPasswordNotMatching}
/>
{isPasswordNotMatching ? (
<div className="mb-4 text-danger">Passwords don't match. Please try again.</div>
) : null}
</div>
</form>
)
return (
<div className="flex items-center">
<CircleIcon className="absolute -left-28 top-[40%] h-8 w-8" />
@@ -109,46 +198,7 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
<div className="mr-0 lg:mr-12">
<h1 className="mb-2 mt-0 text-2xl font-bold">Create your free account</h1>
<div className="mb-4 text-sm font-medium">to continue to Standard Notes.</div>
<form onSubmit={handleCreateAccount}>
<div className="flex flex-col">
<FloatingLabelInput
className={`min-w-auto md:min-w-90 ${isEmailInvalid ? 'mb-2' : 'mb-4'}`}
id="purchase-sign-in-email"
type="email"
label="Email"
value={email}
onChange={handleEmailChange}
ref={emailInputRef}
disabled={isCreatingAccount}
isInvalid={isEmailInvalid}
/>
{isEmailInvalid ? <div className="mb-4 text-danger">Please provide a valid email.</div> : null}
<FloatingLabelInput
className="min-w-auto mb-4 md:min-w-90"
id="purchase-create-account-password"
type="password"
label="Password"
value={password}
onChange={handlePasswordChange}
ref={passwordInputRef}
disabled={isCreatingAccount}
/>
<FloatingLabelInput
className={`min-w-auto md:min-w-90 ${isPasswordNotMatching ? 'mb-2' : 'mb-4'}`}
id="create-account-confirm"
type="password"
label="Repeat password"
value={confirmPassword}
onChange={handleConfirmPasswordChange}
ref={confirmPasswordInputRef}
disabled={isCreatingAccount}
isInvalid={isPasswordNotMatching}
/>
{isPasswordNotMatching ? (
<div className="mb-4 text-danger">Passwords don't match. Please try again.</div>
) : null}
</div>
</form>
{captchaURL ? captchaIframe : CreateAccountForm}
<div className="flex flex-col-reverse items-start justify-between md:flex-row md:items-center">
<div className="flex flex-col">
<button

View File

@@ -6,7 +6,8 @@ import { ChangeEventHandler, FunctionComponent, useEffect, useRef, useState } fr
import FloatingLabelInput from '@/Components/Input/FloatingLabelInput'
import { isEmailValid } from '@/Utils'
import { BlueDotIcon, CircleIcon, DiamondIcon } from '@standardnotes/icons'
import { isErrorResponse } from '@standardnotes/snjs'
import { isErrorResponse, getCaptchaHeader } from '@standardnotes/snjs'
import { useCaptcha } from '@/Hooks/useCaptcha'
type Props = {
application: WebApplication
@@ -21,6 +22,15 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
const [isPasswordInvalid, setIsPasswordInvalid] = useState(false)
const [otherErrorMessage, setOtherErrorMessage] = useState('')
const [captchaURL, setCaptchaURL] = useState('')
const [showCaptcha, setShowCaptcha] = useState(false)
const [hvmToken, setHVMToken] = useState('')
const captchaIframe = useCaptcha(captchaURL, (token) => {
setHVMToken(token)
setShowCaptcha(false)
setCaptchaURL('')
})
const emailInputRef = useRef<HTMLInputElement>(null)
const passwordInputRef = useRef<HTMLInputElement>(null)
@@ -65,10 +75,22 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
return
}
if (captchaURL) {
setShowCaptcha(true)
return
}
setIsSigningIn(true)
try {
const response = await application.signIn(email, password)
const response = await application.signIn(email, password, undefined, undefined, undefined, undefined, hvmToken)
const captchaURL = getCaptchaHeader(response)
if (captchaURL) {
setCaptchaURL(captchaURL)
return
} else {
setCaptchaURL('')
}
if (isErrorResponse(response)) {
throw new Error(response.data.error?.message)
} else {
@@ -78,7 +100,6 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
} catch (err) {
console.error(err)
if ((err as Error).toString().includes('Invalid email or password')) {
setIsSigningIn(false)
setIsEmailInvalid(true)
setIsPasswordInvalid(true)
setOtherErrorMessage('Invalid email or password.')
@@ -86,9 +107,51 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
} else {
application.alerts.alert(err as string).catch(console.error)
}
} finally {
setIsSigningIn(false)
}
}
const signInForm = (
<form onSubmit={handleSignIn}>
<div className="flex flex-col">
<FloatingLabelInput
className={`min-w-auto sm:min-w-90 ${isEmailInvalid && !otherErrorMessage ? 'mb-2' : 'mb-4'}`}
id="purchase-sign-in-email"
type="email"
label="Email"
value={email}
onChange={handleEmailChange}
ref={emailInputRef}
disabled={isSigningIn}
isInvalid={isEmailInvalid}
/>
{isEmailInvalid && !otherErrorMessage ? (
<div className="mb-4 text-danger">Please provide a valid email.</div>
) : null}
<FloatingLabelInput
className={`min-w-auto sm:min-w-90 ${otherErrorMessage ? 'mb-2' : 'mb-4'}`}
id="purchase-sign-in-password"
type="password"
label="Password"
value={password}
onChange={handlePasswordChange}
ref={passwordInputRef}
disabled={isSigningIn}
isInvalid={isPasswordInvalid}
/>
{otherErrorMessage ? <div className="mb-4 text-danger">{otherErrorMessage}</div> : null}
</div>
<Button
className={`${isSigningIn ? 'min-w-30' : 'min-w-24'} mb-5 py-2.5`}
primary
label={isSigningIn ? 'Signing in...' : 'Sign in'}
onClick={handleSignIn}
disabled={isSigningIn}
/>
</form>
)
return (
<div className="flex items-center">
<CircleIcon className="absolute -left-56 top-[35%] h-8 w-8" />
@@ -102,43 +165,7 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
<div>
<h1 className="mb-2 mt-0 text-2xl font-bold">Sign in</h1>
<div className="mb-4 text-sm font-medium">to continue to Standard Notes.</div>
<form onSubmit={handleSignIn}>
<div className="flex flex-col">
<FloatingLabelInput
className={`min-w-auto sm:min-w-90 ${isEmailInvalid && !otherErrorMessage ? 'mb-2' : 'mb-4'}`}
id="purchase-sign-in-email"
type="email"
label="Email"
value={email}
onChange={handleEmailChange}
ref={emailInputRef}
disabled={isSigningIn}
isInvalid={isEmailInvalid}
/>
{isEmailInvalid && !otherErrorMessage ? (
<div className="mb-4 text-danger">Please provide a valid email.</div>
) : null}
<FloatingLabelInput
className={`min-w-auto sm:min-w-90 ${otherErrorMessage ? 'mb-2' : 'mb-4'}`}
id="purchase-sign-in-password"
type="password"
label="Password"
value={password}
onChange={handlePasswordChange}
ref={passwordInputRef}
disabled={isSigningIn}
isInvalid={isPasswordInvalid}
/>
{otherErrorMessage ? <div className="mb-4 text-danger">{otherErrorMessage}</div> : null}
</div>
<Button
className={`${isSigningIn ? 'min-w-30' : 'min-w-24'} mb-5 py-2.5`}
primary
label={isSigningIn ? 'Signing in...' : 'Sign in'}
onClick={handleSignIn}
disabled={isSigningIn}
/>
</form>
{showCaptcha ? captchaIframe : signInForm}
<div className="text-sm font-medium text-passive-1">
Dont have an account yet?{' '}
<a

View File

@@ -0,0 +1,31 @@
import { useEffect } from 'react'
export const useCaptcha = (captchaURL: string, callback: (token: string) => void) => {
useEffect(() => {
function handleCaptchaEvent(event: any) {
if (!captchaURL) {
return
}
if (event.origin !== new URL(captchaURL).origin) {
return
}
if (event?.data?.type?.includes('captcha')) {
callback(event.data.token)
}
}
window.addEventListener('message', handleCaptchaEvent)
return () => {
window.removeEventListener('message', handleCaptchaEvent)
}
}, [callback])
if (!captchaURL) {
return null
}
return <iframe src={captchaURL} height={480}></iframe>
}