chore: auth verification (#2867) [skip e2e]
This commit is contained in:
@@ -72,6 +72,7 @@ export class AuthApiService implements AuthApiServiceInterface {
|
||||
password: string
|
||||
codeVerifier: string
|
||||
recoveryCodes: string
|
||||
hvmToken?: string
|
||||
}): Promise<HttpResponse<SignInWithRecoveryCodesResponseBody>> {
|
||||
if (this.operationsInProgress.get(AuthApiOperations.SignInWithRecoveryCodes)) {
|
||||
throw new ApiCallError(ErrorMessage.GenericInProgress)
|
||||
@@ -86,6 +87,7 @@ export class AuthApiService implements AuthApiServiceInterface {
|
||||
password: dto.password,
|
||||
recovery_codes: dto.recoveryCodes,
|
||||
username: dto.username,
|
||||
hvm_token: dto.hvmToken,
|
||||
})
|
||||
|
||||
return response
|
||||
|
||||
@@ -17,5 +17,6 @@ export interface AuthApiServiceInterface {
|
||||
password: string
|
||||
codeVerifier: string
|
||||
recoveryCodes: string
|
||||
hvmToken?: string
|
||||
}): Promise<HttpResponse<SignInWithRecoveryCodesResponseBody>>
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ export class UserApiService implements UserApiServiceInterface {
|
||||
async register(registerDTO: {
|
||||
email: string
|
||||
serverPassword: string
|
||||
hvmToken?: string
|
||||
keyParams: RootKeyParamsInterface
|
||||
ephemeral: boolean
|
||||
}): Promise<HttpResponse<UserRegistrationResponseBody>> {
|
||||
@@ -76,6 +77,7 @@ export class UserApiService implements UserApiServiceInterface {
|
||||
[ApiEndpointParam.ApiVersion]: this.apiVersion,
|
||||
password: registerDTO.serverPassword,
|
||||
email: registerDTO.email,
|
||||
hvm_token: registerDTO.hvmToken,
|
||||
ephemeral: registerDTO.ephemeral,
|
||||
...registerDTO.keyParams.getPortableValue(),
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface UserApiServiceInterface {
|
||||
register(registerDTO: {
|
||||
email: string
|
||||
serverPassword: string
|
||||
hvmToken?: string
|
||||
keyParams: RootKeyParamsInterface
|
||||
ephemeral: boolean
|
||||
}): Promise<HttpResponse<UserRegistrationResponseBody>>
|
||||
|
||||
@@ -4,4 +4,5 @@ export interface SignInWithRecoveryCodesRequestParams {
|
||||
password: string
|
||||
code_verifier: string
|
||||
recovery_codes: string
|
||||
hvm_token?: string
|
||||
}
|
||||
|
||||
@@ -6,5 +6,6 @@ export type UserRegistrationRequestParams = AnyKeyParamsContent & {
|
||||
[additionalParam: string]: unknown
|
||||
password: string
|
||||
email: string
|
||||
hvm_token?: string
|
||||
ephemeral: boolean
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ platform :android do
|
||||
|
||||
desc 'Deploy production app'
|
||||
lane :prod do
|
||||
version = 3_002_000 + ENV['BUILD_NUMBER'].to_i
|
||||
version = 3_004_000 + ENV['BUILD_NUMBER'].to_i
|
||||
deploy_android 'prod', version
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export type MetaEndpointResponse = {
|
||||
captchaUIUrl: string | null
|
||||
}
|
||||
@@ -26,6 +26,30 @@ export function isErrorResponse<T>(response: HttpResponse<T>): response is HttpE
|
||||
return (response.data as HttpErrorResponseBody)?.error != undefined || response.status >= 400
|
||||
}
|
||||
|
||||
export function getCaptchaHeader<T>(response: HttpResponse<T>) {
|
||||
const captchaHeader = response.headers?.get('x-captcha-required')
|
||||
if (captchaHeader) {
|
||||
return captchaHeader
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function getErrorMessageFromErrorResponseBody(data: HttpErrorResponseBody, defaultMessage?: string): string {
|
||||
let errorMessage = defaultMessage || 'Unknown error'
|
||||
if (
|
||||
data &&
|
||||
typeof data === 'object' &&
|
||||
'error' in data &&
|
||||
data.error &&
|
||||
typeof data.error === 'object' &&
|
||||
'message' in data.error
|
||||
) {
|
||||
errorMessage = data.error.message as string
|
||||
}
|
||||
|
||||
return errorMessage
|
||||
}
|
||||
|
||||
export function getErrorFromErrorResponse(response: HttpErrorResponse): HttpError {
|
||||
const embeddedError = response.data.error
|
||||
if (embeddedError) {
|
||||
|
||||
@@ -13,6 +13,7 @@ export * from './Auth/SignInData'
|
||||
export * from './Auth/SignInResponse'
|
||||
export * from './Auth/SignOutResponse'
|
||||
export * from './Auth/User'
|
||||
export * from './Auth/MetaEndpointResponse'
|
||||
|
||||
/** Temps are awaiting final publish state on server repo */
|
||||
export * from './Temp/SharedVaultMoveType'
|
||||
|
||||
@@ -13,8 +13,10 @@ export interface AuthClientInterface {
|
||||
password: string
|
||||
codeVerifier: string
|
||||
recoveryCodes: string
|
||||
hvmToken?: string
|
||||
}): Promise<
|
||||
| {
|
||||
success: true
|
||||
keyParams: AnyKeyParamsContent
|
||||
session: SessionBody
|
||||
user: {
|
||||
@@ -23,6 +25,9 @@ export interface AuthClientInterface {
|
||||
protocolVersion: string
|
||||
}
|
||||
}
|
||||
| false
|
||||
| {
|
||||
success: false
|
||||
captchaURL: string
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AuthApiServiceInterface } from '@standardnotes/api'
|
||||
import { AnyKeyParamsContent } from '@standardnotes/common'
|
||||
import { isErrorResponse, SessionBody } from '@standardnotes/responses'
|
||||
import { isErrorResponse, getCaptchaHeader } from '@standardnotes/responses'
|
||||
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
@@ -45,37 +45,39 @@ export class AuthManager extends AbstractService implements AuthClientInterface
|
||||
}
|
||||
}
|
||||
|
||||
async signInWithRecoveryCodes(dto: {
|
||||
username: string
|
||||
password: string
|
||||
codeVerifier: string
|
||||
recoveryCodes: string
|
||||
}): Promise<
|
||||
| {
|
||||
keyParams: AnyKeyParamsContent
|
||||
session: SessionBody
|
||||
user: {
|
||||
uuid: string
|
||||
email: string
|
||||
protocolVersion: string
|
||||
}
|
||||
}
|
||||
| false
|
||||
> {
|
||||
async signInWithRecoveryCodes(
|
||||
dto: Parameters<AuthClientInterface['signInWithRecoveryCodes']>[0],
|
||||
): ReturnType<AuthClientInterface['signInWithRecoveryCodes']> {
|
||||
try {
|
||||
const result = await this.authApiService.signInWithRecoveryCodes(dto)
|
||||
|
||||
const captchaURL = getCaptchaHeader(result)
|
||||
|
||||
if (captchaURL) {
|
||||
return {
|
||||
success: false,
|
||||
captchaURL,
|
||||
}
|
||||
}
|
||||
|
||||
if (isErrorResponse(result)) {
|
||||
return false
|
||||
return {
|
||||
success: false,
|
||||
captchaURL: '',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
keyParams: result.data.key_params as AnyKeyParamsContent,
|
||||
session: result.data.session,
|
||||
user: result.data.user,
|
||||
}
|
||||
} catch (error) {
|
||||
return false
|
||||
return {
|
||||
success: false,
|
||||
captchaURL: '',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,13 +30,14 @@ export interface SessionsClientInterface {
|
||||
revokeAllOtherSessions(): Promise<void>
|
||||
|
||||
isCurrentSessionReadOnly(): boolean | undefined
|
||||
register(email: string, password: string, ephemeral: boolean): Promise<UserRegistrationResponseBody>
|
||||
register(email: string, password: string, hvmToken: string, ephemeral: boolean): Promise<UserRegistrationResponseBody>
|
||||
signIn(
|
||||
email: string,
|
||||
password: string,
|
||||
strict: boolean,
|
||||
ephemeral: boolean,
|
||||
minAllowedVersion?: ProtocolVersion,
|
||||
hvmToken?: string,
|
||||
): Promise<SessionManagerResponse>
|
||||
bypassChecksAndSignInWithRootKey(
|
||||
email: string,
|
||||
|
||||
@@ -142,6 +142,7 @@ export class UserService
|
||||
public async register(
|
||||
email: string,
|
||||
password: string,
|
||||
hvmToken: string,
|
||||
ephemeral = false,
|
||||
mergeLocal = true,
|
||||
): Promise<UserRegistrationResponseBody> {
|
||||
@@ -157,7 +158,7 @@ export class UserService
|
||||
|
||||
try {
|
||||
this.lockSyncing()
|
||||
const response = await this.sessions.register(email, password, ephemeral)
|
||||
const response = await this.sessions.register(email, password, hvmToken, ephemeral)
|
||||
|
||||
await this.notifyEventSync(AccountEvent.SignedInOrRegistered, {
|
||||
payload: {
|
||||
@@ -190,6 +191,7 @@ export class UserService
|
||||
ephemeral = false,
|
||||
mergeLocal = true,
|
||||
awaitSync = false,
|
||||
hvmToken?: string,
|
||||
): Promise<HttpResponse<SignInResponse>> {
|
||||
if (this.encryption.hasAccount()) {
|
||||
throw Error('Tried to sign in when an account already exists.')
|
||||
@@ -205,7 +207,7 @@ export class UserService
|
||||
/** Prevent a timed sync from occuring while signing in. */
|
||||
this.lockSyncing()
|
||||
|
||||
const { response } = await this.sessions.signIn(email, password, strict, ephemeral)
|
||||
const { response } = await this.sessions.signIn(email, password, strict, ephemeral, undefined, hvmToken)
|
||||
|
||||
if (!isErrorResponse(response)) {
|
||||
const notifyingFunction = awaitSync ? this.notifyEventSync.bind(this) : this.notifyEvent.bind(this)
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface UserServiceInterface extends AbstractService<AccountEvent, Acco
|
||||
register(
|
||||
email: string,
|
||||
password: string,
|
||||
hvmToken: string,
|
||||
ephemeral: boolean,
|
||||
mergeLocal: boolean,
|
||||
): Promise<UserRegistrationResponseBody>
|
||||
@@ -31,6 +32,7 @@ export interface UserServiceInterface extends AbstractService<AccountEvent, Acco
|
||||
ephemeral: boolean,
|
||||
mergeLocal: boolean,
|
||||
awaitSync: boolean,
|
||||
hvmToken?: string,
|
||||
): Promise<HttpResponse<SignInResponse>>
|
||||
deleteAccount(): Promise<{
|
||||
error: boolean
|
||||
|
||||
@@ -203,7 +203,6 @@ export * from './User/SignedInOrRegisteredEventPayload'
|
||||
export * from './User/SignedOutEventPayload'
|
||||
export * from './User/UserService'
|
||||
export * from './User/UserServiceInterface'
|
||||
export * from './User/UserServiceInterface'
|
||||
export * from './UserEvent/NotificationService'
|
||||
export * from './UserEvent/NotificationServiceEvent'
|
||||
export * from './Vault/UseCase/AuthorizeVaultDeletion'
|
||||
|
||||
@@ -98,6 +98,7 @@ import {
|
||||
SignInResponse,
|
||||
ClientDisplayableError,
|
||||
SessionListEntry,
|
||||
MetaEndpointResponse,
|
||||
} from '@standardnotes/responses'
|
||||
import {
|
||||
SyncService,
|
||||
@@ -117,7 +118,7 @@ import {
|
||||
LoggerInterface,
|
||||
canBlockDeinit,
|
||||
} from '@standardnotes/utils'
|
||||
import { UuidString, ApplicationEventPayload } from '../Types'
|
||||
import { UuidString } from '../Types'
|
||||
import { applicationEventForSyncEvent } from '@Lib/Application/Event'
|
||||
import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files'
|
||||
import { ComputePrivateUsername } from '@standardnotes/encryption'
|
||||
@@ -275,12 +276,12 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
}),
|
||||
)
|
||||
|
||||
const syncEventCallback = async (eventName: SyncEvent) => {
|
||||
const syncEventCallback = async (eventName: SyncEvent, data?: unknown) => {
|
||||
const appEvent = applicationEventForSyncEvent(eventName)
|
||||
if (appEvent) {
|
||||
await encryptionService.onSyncEvent(eventName)
|
||||
|
||||
await this.notifyEvent(appEvent)
|
||||
await this.notifyEvent(appEvent, data)
|
||||
|
||||
if (appEvent === ApplicationEvent.CompletedFullSync) {
|
||||
if (!this.handledFullSyncStage) {
|
||||
@@ -535,7 +536,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
return this.addEventObserver(filteredCallback, event)
|
||||
}
|
||||
|
||||
private async notifyEvent(event: ApplicationEvent, data?: ApplicationEventPayload) {
|
||||
private async notifyEvent(event: ApplicationEvent, data?: unknown) {
|
||||
if (event === ApplicationEvent.Started) {
|
||||
this.onStart()
|
||||
} else if (event === ApplicationEvent.Launched) {
|
||||
@@ -768,10 +769,11 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
public async register(
|
||||
email: string,
|
||||
password: string,
|
||||
hvmToken: string,
|
||||
ephemeral = false,
|
||||
mergeLocal = true,
|
||||
): Promise<UserRegistrationResponseBody> {
|
||||
return this.user.register(email, password, ephemeral, mergeLocal)
|
||||
return this.user.register(email, password, hvmToken, ephemeral, mergeLocal)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -785,8 +787,13 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
ephemeral = false,
|
||||
mergeLocal = true,
|
||||
awaitSync = false,
|
||||
hvmToken?: string,
|
||||
): Promise<HttpResponse<SignInResponse>> {
|
||||
return this.user.signIn(email, password, strict, ephemeral, mergeLocal, awaitSync)
|
||||
return this.user.signIn(email, password, strict, ephemeral, mergeLocal, awaitSync, hvmToken)
|
||||
}
|
||||
|
||||
public async getCaptchaUrl(): Promise<HttpResponse<MetaEndpointResponse>> {
|
||||
return this.legacyApi.getCaptchaUrl()
|
||||
}
|
||||
|
||||
public async changeEmail(
|
||||
|
||||
@@ -166,7 +166,9 @@ describe('SignInWithRecoveryCodes', () => {
|
||||
})
|
||||
|
||||
it('should fail if the sign in with recovery code fails', async () => {
|
||||
authManager.signInWithRecoveryCodes = jest.fn().mockReturnValue(false)
|
||||
authManager.signInWithRecoveryCodes = jest.fn().mockReturnValue({
|
||||
success: false,
|
||||
})
|
||||
|
||||
const useCase = createUseCase()
|
||||
const result = await useCase.execute({
|
||||
|
||||
@@ -68,9 +68,18 @@ export class SignInWithRecoveryCodes implements UseCaseInterface<void> {
|
||||
recoveryCodes: dto.recoveryCodes,
|
||||
username: dto.username,
|
||||
password: rootKey.serverPassword as string,
|
||||
hvmToken: dto.hvmToken,
|
||||
})
|
||||
|
||||
if (signInResult === false) {
|
||||
if (signInResult.success === false) {
|
||||
if (signInResult.captchaURL) {
|
||||
return Result.fail(
|
||||
JSON.stringify({
|
||||
captchaURL: signInResult.captchaURL,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return Result.fail('Could not sign in with recovery code')
|
||||
}
|
||||
|
||||
|
||||
@@ -2,4 +2,5 @@ export interface SignInWithRecoveryCodesDTO {
|
||||
recoveryCodes: string
|
||||
username: string
|
||||
password: string
|
||||
hvmToken?: string
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
isErrorResponse,
|
||||
MoveFileResponse,
|
||||
ValetTokenOperation,
|
||||
MetaEndpointResponse,
|
||||
} from '@standardnotes/responses'
|
||||
import { LegacySession, MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
|
||||
import { HttpServiceInterface } from '@standardnotes/api'
|
||||
@@ -290,6 +291,7 @@ export class LegacyApiService
|
||||
email: string
|
||||
serverPassword: string
|
||||
ephemeral: boolean
|
||||
hvmToken?: string
|
||||
}): Promise<HttpResponse<SignInResponse>> {
|
||||
if (this.authenticating) {
|
||||
return this.createErrorResponse(API_MESSAGE_LOGIN_IN_PROGRESS, HttpStatusCode.BadRequest)
|
||||
@@ -301,6 +303,7 @@ export class LegacyApiService
|
||||
password: dto.serverPassword,
|
||||
ephemeral: dto.ephemeral,
|
||||
code_verifier: this.inMemoryStore.getValue(StorageKey.CodeVerifier) as string,
|
||||
hvm_token: dto.hvmToken,
|
||||
})
|
||||
|
||||
const response = await this.request<SignInResponse>({
|
||||
@@ -958,4 +961,9 @@ export class LegacyApiService
|
||||
|
||||
return this.session.accessToken
|
||||
}
|
||||
|
||||
public getCaptchaUrl() {
|
||||
const response = this.httpService.get<MetaEndpointResponse>(Paths.v1.meta)
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ export const Paths = {
|
||||
...SettingsPaths,
|
||||
...SubscriptionPaths,
|
||||
...UserPaths,
|
||||
meta: '/v1/meta',
|
||||
},
|
||||
v2: {
|
||||
...UserPathsV2,
|
||||
|
||||
@@ -404,7 +404,12 @@ export class SessionManager
|
||||
return undefined
|
||||
}
|
||||
|
||||
async register(email: string, password: string, ephemeral: boolean): Promise<UserRegistrationResponseBody> {
|
||||
async register(
|
||||
email: string,
|
||||
password: string,
|
||||
hvmToken: string,
|
||||
ephemeral: boolean,
|
||||
): Promise<UserRegistrationResponseBody> {
|
||||
if (password.length < MINIMUM_PASSWORD_LENGTH) {
|
||||
throw new ApiCallError(
|
||||
ErrorMessage.InsufficientPasswordMessage.replace('%LENGTH%', MINIMUM_PASSWORD_LENGTH.toString()),
|
||||
@@ -429,6 +434,7 @@ export class SessionManager
|
||||
const registerResponse = await this.userApiService.register({
|
||||
email,
|
||||
serverPassword,
|
||||
hvmToken,
|
||||
keyParams,
|
||||
ephemeral,
|
||||
})
|
||||
@@ -503,8 +509,9 @@ export class SessionManager
|
||||
strict = false,
|
||||
ephemeral = false,
|
||||
minAllowedVersion?: Common.ProtocolVersion,
|
||||
hvmToken?: string,
|
||||
): Promise<SessionManagerResponse> {
|
||||
const result = await this.performSignIn(email, password, strict, ephemeral, minAllowedVersion)
|
||||
const result = await this.performSignIn(email, password, strict, ephemeral, minAllowedVersion, hvmToken)
|
||||
if (
|
||||
isErrorResponse(result.response) &&
|
||||
getErrorFromErrorResponse(result.response).tag !== ErrorTag.ClientValidationError &&
|
||||
@@ -515,7 +522,7 @@ export class SessionManager
|
||||
/**
|
||||
* Try signing in with trimmed + lowercase version of email
|
||||
*/
|
||||
return this.performSignIn(cleanedEmail, password, strict, ephemeral, minAllowedVersion)
|
||||
return this.performSignIn(cleanedEmail, password, strict, ephemeral, minAllowedVersion, hvmToken)
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
@@ -530,6 +537,7 @@ export class SessionManager
|
||||
strict = false,
|
||||
ephemeral = false,
|
||||
minAllowedVersion?: Common.ProtocolVersion,
|
||||
hvmToken?: string,
|
||||
): Promise<SessionManagerResponse> {
|
||||
const paramsResult = await this.retrieveKeyParams({
|
||||
email,
|
||||
@@ -593,7 +601,7 @@ export class SessionManager
|
||||
}
|
||||
}
|
||||
const rootKey = await this.encryptionService.computeRootKey(password, keyParams)
|
||||
const signInResponse = await this.bypassChecksAndSignInWithRootKey(email, rootKey, ephemeral)
|
||||
const signInResponse = await this.bypassChecksAndSignInWithRootKey(email, rootKey, ephemeral, hvmToken)
|
||||
|
||||
return {
|
||||
response: signInResponse,
|
||||
@@ -604,6 +612,7 @@ export class SessionManager
|
||||
email: string,
|
||||
rootKey: SNRootKey,
|
||||
ephemeral = false,
|
||||
hvmToken?: string,
|
||||
): Promise<HttpResponse<SignInResponse>> {
|
||||
const { wrappingKey, canceled } = await this.challengeService.getWrappingKeyIfApplicable()
|
||||
|
||||
@@ -619,6 +628,7 @@ export class SessionManager
|
||||
email,
|
||||
serverPassword: rootKey.serverPassword as string,
|
||||
ephemeral,
|
||||
hvmToken,
|
||||
})
|
||||
|
||||
if (!signInResponse.data || isErrorResponse(signInResponse)) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/web",
|
||||
"version": "3.192.10",
|
||||
"version": "3.192.12",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "dist/app.js",
|
||||
"author": "Standard Notes",
|
||||
|
||||
@@ -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