Account · Sign in: log in, sign up, providers, a link by email, a second factor
LoginForm takes an email and a password, keeps one signed in on a box, and signs in on Enter once the address reads and a password is typed; a failed try says why above Sign in. SignupForm ticks a new password's rules as they hold and waits for all of them and the terms. OAuthButtons offer each provider, one spinning while the rest rest. MagicLinkForm and ForgotPassword send a link to an address and, once sent, say where it went and offer it again. TwoFactorInput takes a six-digit code at once and, after one fails, clears the boxes through forms::PinInput's new attempt, keeping its focus. The Account chapter splits into sign-in, profile and access, and billing and team.
This commit is contained in:
1 parent
69befddeb3
commit
c9e3400a71
15 files changed
+1455
-10
No files matched your search
@@ -0,0 +1,46 @@
|
||||
use gpui::{AnyElement, App, Window, div, prelude::*};
|
||||
|
||||
mod sign_in;
|
||||
|
||||
use super::Page;
|
||||
use crate::script::Step;
|
||||
|
||||
pub const PAGE: Page = Page {
|
||||
number: 36,
|
||||
slug: "account",
|
||||
title: "Account",
|
||||
summary: "Signing in and up, a link by email, a second factor, and the account once inside.",
|
||||
render,
|
||||
script: SCRIPT,
|
||||
};
|
||||
|
||||
const SCRIPT: &[Step] = &[
|
||||
Step::DownAt("account-login", 150.0, 280.0),
|
||||
Step::UpAt("account-login", 150.0, 280.0),
|
||||
Step::Type("ada@example.com"),
|
||||
Step::Key("tab"),
|
||||
Step::Type("hunter22"),
|
||||
Step::Key("enter"),
|
||||
Step::Wait(200),
|
||||
Step::Shot("sign-in-failed"),
|
||||
Step::DownAt("account-magic", 150.0, 150.0),
|
||||
Step::UpAt("account-magic", 150.0, 150.0),
|
||||
Step::Type("ada@example.com"),
|
||||
Step::Key("enter"),
|
||||
Step::Wait(200),
|
||||
Step::Shot("link-sent"),
|
||||
Step::DownAt("account-factor", 120.0, 110.0),
|
||||
Step::UpAt("account-factor", 120.0, 110.0),
|
||||
Step::Type("123456"),
|
||||
Step::Wait(200),
|
||||
Step::Shot("code-failed"),
|
||||
];
|
||||
|
||||
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
div()
|
||||
.child(sign_in::sign_in(window, cx))
|
||||
.child(sign_in::sign_up(window, cx))
|
||||
.child(sign_in::by_email(window, cx))
|
||||
.child(sign_in::second_factor(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
use ely_gpui_component::{
|
||||
account::{ForgotPassword, LoginForm, MagicLinkForm, OAuthButtons, SignupForm, TwoFactorInput},
|
||||
feedback::InlineMessage,
|
||||
layout::{Card, CardHeader},
|
||||
primitives::{Divider, Severity},
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{change, keep, section},
|
||||
};
|
||||
|
||||
/// The sign-in demo: the provider on its way, and why the last try failed.
|
||||
#[derive(Default)]
|
||||
struct SignIn {
|
||||
provider: Option<SharedString>,
|
||||
error: Option<SharedString>,
|
||||
}
|
||||
|
||||
pub fn sign_in(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = keep("account-sign-in", SignIn::default, window, cx);
|
||||
let now = state.read(cx);
|
||||
let (provider, error) = (now.provider.clone(), now.error.clone());
|
||||
let [picked, tried] = [(); 2].map(|_| state.clone());
|
||||
let mut providers = OAuthButtons::new(
|
||||
"account-oauth",
|
||||
[
|
||||
("github", "GitHub"),
|
||||
("google", "Google"),
|
||||
("apple", "Apple"),
|
||||
],
|
||||
)
|
||||
.on_pick(move |key, _, cx| change(&picked, cx, |state| state.provider = Some(key.clone())));
|
||||
if let Some(provider) = provider {
|
||||
providers = providers.busy(provider);
|
||||
}
|
||||
let mut form = LoginForm::new("account-login")
|
||||
.on_submit(move |_, _, cx| {
|
||||
change(&tried, cx, |state| {
|
||||
state.error = Some("No account uses that email and password.".into())
|
||||
})
|
||||
})
|
||||
.on_forgot(|_, _| log::info!("gallery: forgot password"))
|
||||
.on_sign_up(|_, _| log::info!("gallery: sign up"));
|
||||
if let Some(error) = error {
|
||||
form = form.error(error);
|
||||
}
|
||||
section(
|
||||
"LoginForm · OAuthButtons",
|
||||
"Sign in with a provider or with an email and a password. Sign in rests until the address reads and a password is typed; Enter in a field signs in, and a failed try says why above the button.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"account-login",
|
||||
div().w(px(380.)).child(
|
||||
Card::new()
|
||||
.header(CardHeader::new("Sign in to Ely").description("Welcome back."))
|
||||
.child(div().flex().flex_col().gap_5().child(providers).child(Divider::horizontal().label("or")).child(form)),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
/// The sign-up demo: why the last try failed.
|
||||
#[derive(Default)]
|
||||
struct SignUp {
|
||||
error: Option<SharedString>,
|
||||
}
|
||||
|
||||
pub fn sign_up(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = keep("account-sign-up", SignUp::default, window, cx);
|
||||
let error = state.read(cx).error.clone();
|
||||
let tried = state.clone();
|
||||
let mut form = SignupForm::new("account-signup")
|
||||
.terms("I agree to the terms of service")
|
||||
.on_submit(move |signup, _, cx| {
|
||||
let taken = format!("An account already uses {}.", signup.email);
|
||||
change(&tried, cx, |state| state.error = Some(taken.into()))
|
||||
})
|
||||
.on_sign_in(|_, _| log::info!("gallery: sign in"));
|
||||
if let Some(error) = error {
|
||||
form = form.error(error);
|
||||
}
|
||||
section(
|
||||
"SignupForm",
|
||||
"A name, an email and a new password, whose rules tick as they hold. Create account waits for every rule and the terms.",
|
||||
cx,
|
||||
)
|
||||
.child(div().w(px(380.)).child(Card::new().header(CardHeader::new("Create an account")).child(form)))
|
||||
}
|
||||
|
||||
/// The email demos: whether each link went.
|
||||
#[derive(Default)]
|
||||
struct ByEmail {
|
||||
magic: bool,
|
||||
reset: bool,
|
||||
}
|
||||
|
||||
pub fn by_email(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = keep("account-by-email", ByEmail::default, window, cx);
|
||||
let now = state.read(cx);
|
||||
let (magic, reset) = (now.magic, now.reset);
|
||||
let [sent, other, asked, back] = [(); 4].map(|_| state.clone());
|
||||
section(
|
||||
"MagicLinkForm · ForgotPassword",
|
||||
"A link by email: one that signs in, one that sets a new password. Once sent, each says where it went and offers it again.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.gap_6()
|
||||
.child(probe(
|
||||
"account-magic",
|
||||
div().w(px(360.)).child(
|
||||
Card::new().header(CardHeader::new("Sign in by email")).child(
|
||||
MagicLinkForm::new("account-magic")
|
||||
.sent(magic)
|
||||
.on_send(move |_, _, cx| change(&sent, cx, |state| state.magic = true))
|
||||
.on_other(move |_, cx| change(&other, cx, |state| state.magic = false)),
|
||||
),
|
||||
),
|
||||
))
|
||||
.child(
|
||||
div().w(px(360.)).child(
|
||||
Card::new().header(CardHeader::new("Reset your password")).child(
|
||||
ForgotPassword::new("account-forgot")
|
||||
.sent(reset)
|
||||
.on_send(move |_, _, cx| change(&asked, cx, |state| state.reset = true))
|
||||
.on_back(move |_, cx| change(&back, cx, |state| state.reset = false)),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// The code the demo takes.
|
||||
const CODE: &str = "246810";
|
||||
|
||||
/// The second-factor demo: codes that failed, and whether one passed.
|
||||
#[derive(Default)]
|
||||
struct Factor {
|
||||
failed: usize,
|
||||
passed: bool,
|
||||
}
|
||||
|
||||
pub fn second_factor(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = keep("account-factor", Factor::default, window, cx);
|
||||
let now = state.read(cx);
|
||||
let (failed, passed) = (now.failed, now.passed);
|
||||
let checked = state.clone();
|
||||
section(
|
||||
"TwoFactorInput",
|
||||
format!("Six boxes for an authenticator's code; a full code goes at once. A code that fails clears the boxes and says so. The demo takes {CODE}."),
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"account-factor",
|
||||
div().w(px(380.)).child(
|
||||
Card::new()
|
||||
.header(CardHeader::new("Two-step sign-in"))
|
||||
.child(
|
||||
TwoFactorInput::new("account-factor")
|
||||
.failed(failed)
|
||||
.on_code(move |code, _, cx| {
|
||||
change(&checked, cx, |state| match code.as_ref() == CODE {
|
||||
true => state.passed = true,
|
||||
false => state.failed += 1,
|
||||
})
|
||||
})
|
||||
.on_recovery(|_, _| log::info!("gallery: recovery code")),
|
||||
)
|
||||
.children(passed.then(|| InlineMessage::new(Severity::Success, "Signed in."))),
|
||||
),
|
||||
))
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod account;
|
||||
mod agent;
|
||||
mod buttons;
|
||||
mod calendar;
|
||||
@@ -85,6 +86,7 @@ pub const ALL: &[Page] = &[
|
||||
devtools::PAGE,
|
||||
dashboard::PAGE,
|
||||
settings::PAGE,
|
||||
account::PAGE,
|
||||
theme::PAGE,
|
||||
];
|
||||
|
||||
|
||||
Reference in new issue
Block a user