chore: auth verification (#2867) [skip e2e]
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.',
|
||||
),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
Don’t have an account yet?{' '}
|
||||
<a
|
||||
|
||||
31
packages/web/src/javascripts/Hooks/useCaptcha.tsx
Normal file
31
packages/web/src/javascripts/Hooks/useCaptcha.tsx
Normal 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>
|
||||
}
|
||||
Reference in New Issue
Block a user