Brave Account: login flow (UI-only) (#32497)
This commit is contained in:
@@ -159,6 +159,10 @@ class BraveAccountUIBase {
|
||||
IDS_BRAVE_ACCOUNT_ERROR_DIALOG_CLIENT_ERROR},
|
||||
{"braveAccountErrorDialogServerError",
|
||||
IDS_BRAVE_ACCOUNT_ERROR_DIALOG_SERVER_ERROR},
|
||||
{"braveAccountErrorDialogIncorrectEmail",
|
||||
IDS_BRAVE_ACCOUNT_ERROR_DIALOG_INCORRECT_EMAIL},
|
||||
{"braveAccountErrorDialogIncorrectPassword",
|
||||
IDS_BRAVE_ACCOUNT_ERROR_DIALOG_INCORRECT_PASSWORD},
|
||||
{"braveAccountErrorDialogAccountExists",
|
||||
IDS_BRAVE_ACCOUNT_ERROR_DIALOG_ACCOUNT_EXISTS},
|
||||
{"braveAccountErrorDialogEmailDomainNotSupported",
|
||||
|
||||
@@ -3,22 +3,11 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { RegisterError } from './brave_account.mojom-webui.js'
|
||||
import { LoginError, RegisterError } from './brave_account.mojom-webui.js'
|
||||
|
||||
type FlowToError = {
|
||||
register: RegisterError
|
||||
}
|
||||
|
||||
export type Flow = keyof FlowToError
|
||||
|
||||
export type FlowToErrorCode = {
|
||||
[F in Flow]: NonNullable<FlowToError[F]['errorCode']>
|
||||
}
|
||||
|
||||
export type Error<F extends Flow = Flow> = {
|
||||
flow: F
|
||||
details: FlowToError[F]
|
||||
}
|
||||
export type Error =
|
||||
| { flow: 'login'; details: LoginError }
|
||||
| { flow: 'register'; details: RegisterError }
|
||||
|
||||
export function onEyeIconClicked(event: Event) {
|
||||
event.preventDefault()
|
||||
|
||||
@@ -209,7 +209,7 @@ export class BraveAccountCreateDialogElement extends CrLitElement {
|
||||
this.fire('error-occurred', {
|
||||
flow: 'register',
|
||||
details,
|
||||
} satisfies Error<'register'>)
|
||||
} satisfies Extract<Error, { flow: 'register' }>)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,7 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import {
|
||||
html,
|
||||
nothing,
|
||||
TemplateResult,
|
||||
} from '//resources/lit/v3_0/lit.rollup.js'
|
||||
import { html, TemplateResult } from '//resources/lit/v3_0/lit.rollup.js'
|
||||
|
||||
import './brave_account_create_dialog.js'
|
||||
import './brave_account_entry_dialog.js'
|
||||
@@ -18,16 +14,12 @@ import { BraveAccountDialogs, Dialog } from './brave_account_dialogs.js'
|
||||
import { Error } from './brave_account_common.js'
|
||||
|
||||
export function getHtml(this: BraveAccountDialogs) {
|
||||
const dialogHtml: Record<
|
||||
Dialog['type'],
|
||||
() => TemplateResult | typeof nothing
|
||||
> = {
|
||||
NONE: () => nothing,
|
||||
const dialogHtml: Record<Dialog['type'], () => TemplateResult> = {
|
||||
ENTRY: () => html`
|
||||
<brave-account-entry-dialog
|
||||
@close-dialog=${this.onCloseDialog}
|
||||
@create-button-clicked=${() => (this.dialog = { type: 'CREATE' })}
|
||||
@self-custody-button-clicked=${() => (this.dialog = { type: 'NONE' })}
|
||||
@self-custody-button-clicked=${() => (this.dialog = { type: 'ENTRY' })}
|
||||
@sign-in-button-clicked=${() => (this.dialog = { type: 'SIGN_IN' })}
|
||||
>
|
||||
</brave-account-entry-dialog>
|
||||
@@ -48,12 +40,13 @@ export function getHtml(this: BraveAccountDialogs) {
|
||||
<brave-account-sign-in-dialog
|
||||
@back-button-clicked=${this.onBackButtonClicked}
|
||||
@close-dialog=${this.onCloseDialog}
|
||||
@error-occurred=${(e: CustomEvent<Error>) =>
|
||||
(this.dialog = {
|
||||
type: 'ERROR',
|
||||
error: e.detail,
|
||||
})}
|
||||
@forgot-password-button-clicked=${() =>
|
||||
(this.dialog = { type: 'FORGOT_PASSWORD' })}
|
||||
@sign-in-button-clicked=${() => {
|
||||
this.dialog = { type: 'NONE' }
|
||||
this.signedIn = true
|
||||
}}
|
||||
>
|
||||
</brave-account-sign-in-dialog>
|
||||
`,
|
||||
@@ -61,6 +54,7 @@ export function getHtml(this: BraveAccountDialogs) {
|
||||
<brave-account-forgot-password-dialog
|
||||
@back-button-clicked=${this.onBackButtonClicked}
|
||||
@close-dialog=${this.onCloseDialog}
|
||||
>
|
||||
</brave-account-forgot-password-dialog>
|
||||
`,
|
||||
ERROR: () => html`
|
||||
|
||||
@@ -13,7 +13,7 @@ import { getHtml } from './brave_account_dialogs.html.js'
|
||||
import { Error } from './brave_account_common.js'
|
||||
|
||||
export type Dialog =
|
||||
| { type: 'NONE' | 'CREATE' | 'ENTRY' | 'FORGOT_PASSWORD' | 'SIGN_IN' }
|
||||
| { type: 'CREATE' | 'ENTRY' | 'FORGOT_PASSWORD' | 'SIGN_IN' }
|
||||
| { type: 'ERROR'; error: Error }
|
||||
|
||||
export class BraveAccountDialogs extends CrLitElement {
|
||||
@@ -28,7 +28,6 @@ export class BraveAccountDialogs extends CrLitElement {
|
||||
static override get properties() {
|
||||
return {
|
||||
dialog: { type: Object },
|
||||
signedIn: { type: Boolean, reflect: true },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +46,6 @@ export class BraveAccountDialogs extends CrLitElement {
|
||||
BraveAccountBrowserProxyImpl.getInstance()
|
||||
|
||||
protected accessor dialog: Dialog = { type: 'ENTRY' }
|
||||
protected accessor signedIn: boolean = false
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -9,51 +9,68 @@ import { html } from '//resources/lit/v3_0/lit.rollup.js'
|
||||
import { loadTimeData } from '//resources/js/load_time_data.js'
|
||||
|
||||
import { BraveAccountErrorDialogElement } from './brave_account_error_dialog.js'
|
||||
import { Flow, FlowToErrorCode } from './brave_account_common.js'
|
||||
import { RegisterErrorCode } from './brave_account.mojom-webui.js'
|
||||
import {
|
||||
LoginErrorCode,
|
||||
RegisterErrorCode,
|
||||
} from './brave_account.mojom-webui.js'
|
||||
|
||||
export function getHtml(this: BraveAccountErrorDialogElement) {
|
||||
return html`<!--_html_template_start_-->
|
||||
<brave-account-dialog
|
||||
alert-message=${(() => {
|
||||
const REGISTER_ERROR_STRINGS = {
|
||||
const LOGIN_ERROR_STRINGS: Partial<Record<LoginErrorCode, string>> = {
|
||||
[LoginErrorCode.kIncorrectEmail]:
|
||||
'$i18n{braveAccountErrorDialogIncorrectEmail}',
|
||||
[LoginErrorCode.kIncorrectPassword]:
|
||||
'$i18n{braveAccountErrorDialogIncorrectPassword}',
|
||||
}
|
||||
|
||||
const REGISTER_ERROR_STRINGS: Partial<
|
||||
Record<RegisterErrorCode, string>
|
||||
> = {
|
||||
[RegisterErrorCode.kAccountExists]:
|
||||
'$i18n{braveAccountErrorDialogAccountExists}',
|
||||
[RegisterErrorCode.kEmailDomainNotSupported]:
|
||||
'$i18n{braveAccountErrorDialogEmailDomainNotSupported}',
|
||||
[RegisterErrorCode.kTooManyVerifications]:
|
||||
'$i18n{braveAccountErrorDialogTooManyVerifications}',
|
||||
} satisfies Partial<Record<RegisterErrorCode, string>>
|
||||
|
||||
const ERROR_STRINGS: {
|
||||
[F in Flow]: Partial<Record<FlowToErrorCode[F], string>>
|
||||
} = {
|
||||
register: REGISTER_ERROR_STRINGS,
|
||||
}
|
||||
|
||||
const { statusCode, errorCode } = this.error.details
|
||||
const getErrorMessage = <T extends LoginErrorCode | RegisterErrorCode>(
|
||||
errorStrings: Partial<Record<T, string>>,
|
||||
details: { statusCode: number | null; errorCode: T | null },
|
||||
): string => {
|
||||
const { statusCode, errorCode } = details
|
||||
|
||||
if (statusCode == null) {
|
||||
// client-side error
|
||||
return loadTimeData.getStringF(
|
||||
'braveAccountErrorDialogClientError',
|
||||
errorCode != null
|
||||
? ` ($i18n{braveAccountErrorDialogError}=${errorCode})`
|
||||
: '',
|
||||
if (statusCode == null) {
|
||||
// client-side error
|
||||
return loadTimeData.getStringF(
|
||||
'braveAccountErrorDialogClientError',
|
||||
errorCode != null
|
||||
? ` ($i18n{braveAccountErrorDialogError}=${errorCode})`
|
||||
: '',
|
||||
)
|
||||
}
|
||||
|
||||
// server-side error
|
||||
return (
|
||||
(errorCode != null ? errorStrings[errorCode] : null)
|
||||
?? loadTimeData.getStringF(
|
||||
'braveAccountErrorDialogServerError',
|
||||
statusCode,
|
||||
errorCode != null
|
||||
? `, $i18n{braveAccountErrorDialogError}=${errorCode}`
|
||||
: '',
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// server-side error
|
||||
return (
|
||||
(errorCode != null ? ERROR_STRINGS[this.error.flow][errorCode] : null)
|
||||
?? loadTimeData.getStringF(
|
||||
'braveAccountErrorDialogServerError',
|
||||
statusCode,
|
||||
errorCode != null
|
||||
? `, $i18n{braveAccountErrorDialogError}=${errorCode}`
|
||||
: '',
|
||||
)
|
||||
)
|
||||
switch (this.error.flow) {
|
||||
case 'login':
|
||||
return getErrorMessage(LOGIN_ERROR_STRINGS, this.error.details)
|
||||
case 'register':
|
||||
return getErrorMessage(REGISTER_ERROR_STRINGS, this.error.details)
|
||||
}
|
||||
})()}
|
||||
dialog-description="$i18n{braveAccountErrorDialogDescription}"
|
||||
dialog-title="$i18n{braveAccountErrorDialogTitle}"
|
||||
|
||||
@@ -21,11 +21,7 @@ export function getHtml(this: BraveAccountSignInDialogElement) {
|
||||
placeholder="$i18n{braveAccountEmailInputPlaceholder}"
|
||||
@input=${this.onEmailInput}
|
||||
>
|
||||
<div
|
||||
class="label ${this.email.length !== 0 && !this.isEmailValid
|
||||
? 'error'
|
||||
: ''}"
|
||||
>
|
||||
<div class="label ${this.shouldShowEmailError ? 'error' : ''}">
|
||||
$i18n{braveAccountEmailInputLabel}
|
||||
</div>
|
||||
</leo-input>
|
||||
@@ -54,7 +50,7 @@ export function getHtml(this: BraveAccountSignInDialogElement) {
|
||||
<leo-button
|
||||
slot="buttons"
|
||||
?isDisabled=${!this.isEmailValid || !this.isPasswordValid}
|
||||
@click=${() => this.fire('sign-in-button-clicked')}
|
||||
@click=${this.onSignInButtonClicked}
|
||||
>
|
||||
$i18n{braveAccountSignInButtonLabel}
|
||||
</leo-button>
|
||||
|
||||
@@ -5,9 +5,14 @@
|
||||
|
||||
import { CrLitElement } from '//resources/lit/v3_0/lit.rollup.js'
|
||||
|
||||
import {
|
||||
BraveAccountBrowserProxy,
|
||||
BraveAccountBrowserProxyImpl,
|
||||
} from './brave_account_browser_proxy.js'
|
||||
import { getCss } from './brave_account_sign_in_dialog.css.js'
|
||||
import { getHtml } from './brave_account_sign_in_dialog.html.js'
|
||||
import { isEmailValid } from './brave_account_common.js'
|
||||
import { Error, isEmailValid } from './brave_account_common.js'
|
||||
import { LoginError, LoginErrorCode } from './brave_account.mojom-webui.js'
|
||||
|
||||
// @ts-expect-error
|
||||
import { Login } from 'chrome://resources/brave/opaque_ke.bundle.js'
|
||||
@@ -28,24 +33,84 @@ export class BraveAccountSignInDialogElement extends CrLitElement {
|
||||
static override get properties() {
|
||||
return {
|
||||
email: { type: String },
|
||||
isEmailValid: { type: Boolean },
|
||||
isPasswordValid: { type: Boolean },
|
||||
password: { type: String },
|
||||
}
|
||||
}
|
||||
|
||||
protected onEmailInput(detail: { value: string }) {
|
||||
this.email = detail.value
|
||||
this.isEmailValid = isEmailValid(this.email)
|
||||
this.email = detail.value.trim()
|
||||
}
|
||||
|
||||
protected onPasswordInput(detail: { value: string }) {
|
||||
this.isPasswordValid = detail.value.length !== 0
|
||||
this.password = detail.value
|
||||
}
|
||||
|
||||
protected accessor email: string = ''
|
||||
protected accessor isEmailValid: boolean = false
|
||||
protected accessor isPasswordValid: boolean = false
|
||||
// The reason this happens here (rather than in BraveAccountService) is that
|
||||
// both `login.start()` and `login.finish()` invoke the OPAQUE
|
||||
// protocol in our WASM (compiled from Rust), and so the flow must run in the
|
||||
// renderer to manage the transient cryptographic state — the service only
|
||||
// transports the two server round trips
|
||||
// (`loginInitialize`/`loginFinalize`). We'll revisit handling this
|
||||
// through Mojo in C++ if that proves practical.
|
||||
protected async onSignInButtonClicked() {
|
||||
try {
|
||||
const serializedKE1 = this.login.start(this.password)
|
||||
const { encryptedLoginToken, serializedKE2 } =
|
||||
await this.browserProxy.authentication.loginInitialize(
|
||||
this.email,
|
||||
serializedKE1,
|
||||
)
|
||||
const clientMac = this.login.finish(
|
||||
serializedKE2,
|
||||
this.password,
|
||||
this.email,
|
||||
)
|
||||
await this.browserProxy.authentication.loginFinalize(
|
||||
encryptedLoginToken,
|
||||
clientMac,
|
||||
)
|
||||
this.fire('close-dialog')
|
||||
} catch (error) {
|
||||
let details: LoginError
|
||||
|
||||
if (error && typeof error === 'object') {
|
||||
details = error as LoginError
|
||||
} else if (typeof error === 'string') {
|
||||
details = {
|
||||
statusCode: null,
|
||||
errorCode: LoginErrorCode.kOpaqueError,
|
||||
}
|
||||
} else {
|
||||
console.error('Unexpected error:', error)
|
||||
details = { statusCode: null, errorCode: null }
|
||||
}
|
||||
|
||||
this.fire('error-occurred', {
|
||||
flow: 'login',
|
||||
details,
|
||||
} satisfies Extract<Error, { flow: 'login' }>)
|
||||
}
|
||||
}
|
||||
|
||||
private browserProxy: BraveAccountBrowserProxy =
|
||||
BraveAccountBrowserProxyImpl.getInstance()
|
||||
|
||||
protected login = new Login()
|
||||
|
||||
protected accessor email: string = ''
|
||||
protected accessor password: string = ''
|
||||
|
||||
protected get isEmailValid(): boolean {
|
||||
return isEmailValid(this.email)
|
||||
}
|
||||
|
||||
protected get shouldShowEmailError(): boolean {
|
||||
return this.email.length !== 0 && !this.isEmailValid
|
||||
}
|
||||
|
||||
protected get isPasswordValid(): boolean {
|
||||
return this.password.length !== 0
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -111,6 +111,12 @@
|
||||
<message name="IDS_BRAVE_ACCOUNT_ERROR_DIALOG_SERVER_ERROR" translateable="false" desc="">
|
||||
Internal server error (HTTP=<ph name="STATUS_CODE">$1</ph><ph name="ERROR_CODE">$2</ph>). Please try again later.
|
||||
</message>
|
||||
<message name="IDS_BRAVE_ACCOUNT_ERROR_DIALOG_INCORRECT_EMAIL" translateable="false" desc="">
|
||||
No account found for that email address. Please try again, or create a new account.
|
||||
</message>
|
||||
<message name="IDS_BRAVE_ACCOUNT_ERROR_DIALOG_INCORRECT_PASSWORD" translateable="false" desc="">
|
||||
Incorrect password. Please try again.
|
||||
</message>
|
||||
<message name="IDS_BRAVE_ACCOUNT_ERROR_DIALOG_ACCOUNT_EXISTS" translateable="false" desc="">
|
||||
An account already exists for this email address. Try logging in instead of creating an account.
|
||||
</message>
|
||||
|
||||
Reference in New Issue
Block a user