mirror of
https://github.com/openai/codex.git
synced 2026-02-02 15:03:38 +00:00
Compare commits
11 Commits
dev/shell-
...
nux
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec288958e9 | ||
|
|
bc6368cff9 | ||
|
|
ee761180ea | ||
|
|
c21f3346ef | ||
|
|
8382dbc12e | ||
|
|
ce804c62a6 | ||
|
|
b412117929 | ||
|
|
7759f1bc13 | ||
|
|
340270d1fa | ||
|
|
29131b0d88 | ||
|
|
a3a2b1c88c |
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -970,6 +970,7 @@ dependencies = [
|
||||
"unicode-width 0.1.14",
|
||||
"uuid",
|
||||
"vt100",
|
||||
"webbrowser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -33,18 +33,19 @@ codex-common = { path = "../common", features = [
|
||||
"sandbox_summary",
|
||||
] }
|
||||
codex-core = { path = "../core" }
|
||||
codex-protocol = { path = "../protocol" }
|
||||
codex-file-search = { path = "../file-search" }
|
||||
codex-login = { path = "../login" }
|
||||
codex-ollama = { path = "../ollama" }
|
||||
codex-protocol = { path = "../protocol" }
|
||||
color-eyre = "0.6.3"
|
||||
crossterm = { version = "0.28.1", features = ["bracketed-paste"] }
|
||||
diffy = "0.4.2"
|
||||
image = { version = "^0.25.6", default-features = false, features = ["jpeg"] }
|
||||
lazy_static = "1"
|
||||
once_cell = "1"
|
||||
mcp-types = { path = "../mcp-types" }
|
||||
once_cell = "1"
|
||||
path-clean = "1.0.1"
|
||||
rand = "0.9"
|
||||
ratatui = { version = "0.29.0", features = [
|
||||
"scrolling-regions",
|
||||
"unstable-rendered-line-info",
|
||||
@@ -75,7 +76,7 @@ tui-markdown = "0.3.3"
|
||||
unicode-segmentation = "1.12.0"
|
||||
unicode-width = "0.1"
|
||||
uuid = "1"
|
||||
rand = "0.9"
|
||||
webbrowser = "1"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
@@ -139,10 +139,11 @@ impl App<'_> {
|
||||
}
|
||||
|
||||
let login_status = get_login_status(&config);
|
||||
tracing::info!("login_status1: {:?}", login_status);
|
||||
let should_show_onboarding =
|
||||
should_show_onboarding(login_status, &config, show_trust_screen);
|
||||
should_show_onboarding(&login_status, &config, show_trust_screen);
|
||||
let app_state = if should_show_onboarding {
|
||||
let show_login_screen = should_show_login_screen(login_status, &config);
|
||||
let show_login_screen = should_show_login_screen(&login_status, &config);
|
||||
let chat_widget_args = ChatWidgetArgs {
|
||||
config: config.clone(),
|
||||
initial_prompt,
|
||||
@@ -640,7 +641,7 @@ impl App<'_> {
|
||||
}
|
||||
|
||||
fn should_show_onboarding(
|
||||
login_status: LoginStatus,
|
||||
login_status: &LoginStatus,
|
||||
config: &Config,
|
||||
show_trust_screen: bool,
|
||||
) -> bool {
|
||||
@@ -648,10 +649,21 @@ fn should_show_onboarding(
|
||||
return true;
|
||||
}
|
||||
|
||||
if is_free_plan(login_status) {
|
||||
return true;
|
||||
}
|
||||
|
||||
should_show_login_screen(login_status, config)
|
||||
}
|
||||
|
||||
fn should_show_login_screen(login_status: LoginStatus, config: &Config) -> bool {
|
||||
fn is_free_plan(login_status: &LoginStatus) -> bool {
|
||||
match login_status {
|
||||
LoginStatus::Auth(auth) => auth.get_plan_type().as_deref() == Some("free"),
|
||||
LoginStatus::NotAuthenticated => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn should_show_login_screen(login_status: &LoginStatus, config: &Config) -> bool {
|
||||
// Only show the login screen for providers that actually require OpenAI auth
|
||||
// (OpenAI or equivalents). For OSS/other providers, skip login entirely.
|
||||
if !config.model_provider.requires_openai_auth {
|
||||
@@ -660,7 +672,9 @@ fn should_show_login_screen(login_status: LoginStatus, config: &Config) -> bool
|
||||
|
||||
match login_status {
|
||||
LoginStatus::NotAuthenticated => true,
|
||||
LoginStatus::AuthMode(method) => method != config.preferred_auth_method,
|
||||
LoginStatus::Auth(auth) => {
|
||||
auth.mode != config.preferred_auth_method || is_free_plan(login_status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -670,6 +684,7 @@ mod tests {
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config::ConfigToml;
|
||||
use codex_login::AuthMode;
|
||||
use codex_login::CodexAuth;
|
||||
|
||||
fn make_config(preferred: AuthMode) -> Config {
|
||||
let mut cfg = Config::load_from_base_config_with_overrides(
|
||||
@@ -686,7 +701,7 @@ mod tests {
|
||||
fn shows_login_when_not_authenticated() {
|
||||
let cfg = make_config(AuthMode::ChatGPT);
|
||||
assert!(should_show_login_screen(
|
||||
LoginStatus::NotAuthenticated,
|
||||
&LoginStatus::NotAuthenticated,
|
||||
&cfg
|
||||
));
|
||||
}
|
||||
@@ -695,7 +710,7 @@ mod tests {
|
||||
fn shows_login_when_api_key_but_prefers_chatgpt() {
|
||||
let cfg = make_config(AuthMode::ChatGPT);
|
||||
assert!(should_show_login_screen(
|
||||
LoginStatus::AuthMode(AuthMode::ApiKey),
|
||||
&LoginStatus::Auth(CodexAuth::from_api_key("sk-test")),
|
||||
&cfg
|
||||
))
|
||||
}
|
||||
@@ -704,7 +719,7 @@ mod tests {
|
||||
fn hides_login_when_api_key_and_prefers_api_key() {
|
||||
let cfg = make_config(AuthMode::ApiKey);
|
||||
assert!(!should_show_login_screen(
|
||||
LoginStatus::AuthMode(AuthMode::ApiKey),
|
||||
&LoginStatus::Auth(CodexAuth::from_api_key("sk-test")),
|
||||
&cfg
|
||||
))
|
||||
}
|
||||
@@ -713,7 +728,7 @@ mod tests {
|
||||
fn hides_login_when_chatgpt_and_prefers_chatgpt() {
|
||||
let cfg = make_config(AuthMode::ChatGPT);
|
||||
assert!(!should_show_login_screen(
|
||||
LoginStatus::AuthMode(AuthMode::ChatGPT),
|
||||
&LoginStatus::Auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()),
|
||||
&cfg
|
||||
))
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ use codex_core::config::find_codex_home;
|
||||
use codex_core::config::load_config_as_toml_with_cli_overrides;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_login::AuthMode;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_ollama::DEFAULT_OSS_MODEL;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
@@ -297,9 +296,9 @@ fn restore() {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum LoginStatus {
|
||||
AuthMode(AuthMode),
|
||||
Auth(CodexAuth),
|
||||
NotAuthenticated,
|
||||
}
|
||||
|
||||
@@ -309,7 +308,7 @@ fn get_login_status(config: &Config) -> LoginStatus {
|
||||
// to refresh the token. Block on it.
|
||||
let codex_home = config.codex_home.clone();
|
||||
match CodexAuth::from_codex_home(&codex_home, config.preferred_auth_method) {
|
||||
Ok(Some(auth)) => LoginStatus::AuthMode(auth.mode),
|
||||
Ok(Some(auth)) => LoginStatus::Auth(auth),
|
||||
Ok(None) => LoginStatus::NotAuthenticated,
|
||||
Err(err) => {
|
||||
error!("Failed to read auth.json: {err}");
|
||||
|
||||
@@ -29,6 +29,8 @@ use std::path::PathBuf;
|
||||
|
||||
use super::onboarding_screen::StepState;
|
||||
// no additional imports
|
||||
use codex_login::logout;
|
||||
const PRICING_URL: &str = "https://openai.com/chatgpt/pricing";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum SignInState {
|
||||
@@ -36,10 +38,18 @@ pub(crate) enum SignInState {
|
||||
ChatGptContinueInBrowser(ContinueInBrowserState),
|
||||
ChatGptSuccessMessage,
|
||||
ChatGptSuccess,
|
||||
FreePlan,
|
||||
EnvVarMissing,
|
||||
EnvVarFound,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum FreePlanSelection {
|
||||
Upgrade,
|
||||
Logout,
|
||||
Exit,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Used to manage the lifecycle of SpawnedLogin and ensure it gets cleaned up.
|
||||
pub(crate) struct ContinueInBrowserState {
|
||||
@@ -59,12 +69,30 @@ impl Drop for ContinueInBrowserState {
|
||||
impl KeyboardHandler for AuthModeWidget {
|
||||
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||
match key_event.code {
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
self.highlighted_mode = AuthMode::ChatGPT;
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
self.highlighted_mode = AuthMode::ApiKey;
|
||||
}
|
||||
KeyCode::Up | KeyCode::Char('k') => match self.sign_in_state {
|
||||
SignInState::FreePlan => {
|
||||
self.free_plan_selected = match self.free_plan_selected {
|
||||
FreePlanSelection::Upgrade => FreePlanSelection::Exit,
|
||||
FreePlanSelection::Logout => FreePlanSelection::Upgrade,
|
||||
FreePlanSelection::Exit => FreePlanSelection::Logout,
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
self.highlighted_mode = AuthMode::ChatGPT;
|
||||
}
|
||||
},
|
||||
KeyCode::Down | KeyCode::Char('j') => match self.sign_in_state {
|
||||
SignInState::FreePlan => {
|
||||
self.free_plan_selected = match self.free_plan_selected {
|
||||
FreePlanSelection::Upgrade => FreePlanSelection::Logout,
|
||||
FreePlanSelection::Logout => FreePlanSelection::Exit,
|
||||
FreePlanSelection::Exit => FreePlanSelection::Upgrade,
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
self.highlighted_mode = AuthMode::ApiKey;
|
||||
}
|
||||
},
|
||||
KeyCode::Char('1') => {
|
||||
self.start_chatgpt_login();
|
||||
}
|
||||
@@ -78,6 +106,22 @@ impl KeyboardHandler for AuthModeWidget {
|
||||
SignInState::ChatGptSuccessMessage => {
|
||||
self.sign_in_state = SignInState::ChatGptSuccess
|
||||
}
|
||||
SignInState::FreePlan => match self.free_plan_selected {
|
||||
FreePlanSelection::Upgrade => {
|
||||
let _ = webbrowser::open(PRICING_URL);
|
||||
let _ = logout(&self.codex_home);
|
||||
self.login_status = LoginStatus::NotAuthenticated;
|
||||
self.event_tx.send(AppEvent::ExitRequest);
|
||||
}
|
||||
FreePlanSelection::Logout => {
|
||||
let _ = logout(&self.codex_home);
|
||||
self.login_status = LoginStatus::NotAuthenticated;
|
||||
self.sign_in_state = SignInState::PickMode;
|
||||
}
|
||||
FreePlanSelection::Exit => {
|
||||
self.event_tx.send(AppEvent::ExitRequest);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Esc => {
|
||||
@@ -99,6 +143,7 @@ pub(crate) struct AuthModeWidget {
|
||||
pub codex_home: PathBuf,
|
||||
pub login_status: LoginStatus,
|
||||
pub preferred_auth_method: AuthMode,
|
||||
pub free_plan_selected: FreePlanSelection,
|
||||
}
|
||||
|
||||
impl AuthModeWidget {
|
||||
@@ -123,8 +168,8 @@ impl AuthModeWidget {
|
||||
|
||||
// If the user is already authenticated but the method differs from their
|
||||
// preferred auth method, show a brief explanation.
|
||||
if let LoginStatus::AuthMode(current) = self.login_status
|
||||
&& current != self.preferred_auth_method
|
||||
if let LoginStatus::Auth(ref current) = self.login_status
|
||||
&& current.mode != self.preferred_auth_method
|
||||
{
|
||||
let to_label = |mode: AuthMode| match mode {
|
||||
AuthMode::ApiKey => "API key",
|
||||
@@ -132,7 +177,7 @@ impl AuthModeWidget {
|
||||
};
|
||||
let msg = format!(
|
||||
" You’re currently using {} while your preferred method is {}.",
|
||||
to_label(current),
|
||||
to_label(current.mode),
|
||||
to_label(self.preferred_auth_method)
|
||||
);
|
||||
lines.push(Line::from(msg).style(Style::default()));
|
||||
@@ -167,11 +212,9 @@ impl AuthModeWidget {
|
||||
|
||||
vec![line1, line2]
|
||||
};
|
||||
let chatgpt_label = if matches!(self.login_status, LoginStatus::AuthMode(AuthMode::ChatGPT))
|
||||
{
|
||||
"Continue using ChatGPT"
|
||||
} else {
|
||||
"Sign in with ChatGPT"
|
||||
let chatgpt_label = match &self.login_status {
|
||||
LoginStatus::Auth(auth) if auth.mode == AuthMode::ChatGPT => "Continue using ChatGPT",
|
||||
_ => "Sign in with ChatGPT",
|
||||
};
|
||||
|
||||
lines.extend(create_mode_item(
|
||||
@@ -180,11 +223,9 @@ impl AuthModeWidget {
|
||||
chatgpt_label,
|
||||
"Usage included with Plus, Pro, and Team plans",
|
||||
));
|
||||
let api_key_label = if matches!(self.login_status, LoginStatus::AuthMode(AuthMode::ApiKey))
|
||||
{
|
||||
"Continue using API key"
|
||||
} else {
|
||||
"Provide your own API key"
|
||||
let api_key_label = match &self.login_status {
|
||||
LoginStatus::Auth(auth) if auth.mode == AuthMode::ApiKey => "Continue using API key",
|
||||
_ => "Provide your own API key",
|
||||
};
|
||||
lines.extend(create_mode_item(
|
||||
1,
|
||||
@@ -287,6 +328,53 @@ impl AuthModeWidget {
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_free_plan(&self, area: Rect, buf: &mut Buffer) {
|
||||
let mut lines: Vec<Line> = vec![
|
||||
Line::from("> You're currently signed in using a free ChatGPT account"),
|
||||
Line::from(""),
|
||||
Line::from(
|
||||
" To use Codex with your ChatGPT plan, upgrade to a Pro, Plus, and Team account.",
|
||||
),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::raw(" "),
|
||||
"\u{1b}]8;;https://openai.com/chatgpt/pricing\u{7}https://openai.com/chatgpt/pricing\u{1b}]8;;\u{7}"
|
||||
.underlined(),
|
||||
]),
|
||||
Line::from(""),
|
||||
];
|
||||
|
||||
let option_line = |idx: usize, label: &str, selected: bool| {
|
||||
if selected {
|
||||
Line::from(format!("> {idx}. {label}")).fg(Color::Cyan)
|
||||
} else {
|
||||
Line::from(format!(" {idx}. {label}"))
|
||||
}
|
||||
};
|
||||
|
||||
lines.push(option_line(
|
||||
1,
|
||||
"Upgrade plan",
|
||||
matches!(self.free_plan_selected, FreePlanSelection::Upgrade),
|
||||
));
|
||||
lines.push(option_line(
|
||||
2,
|
||||
"Log out to use a different account",
|
||||
matches!(self.free_plan_selected, FreePlanSelection::Logout),
|
||||
));
|
||||
lines.push(option_line(
|
||||
3,
|
||||
"Exit",
|
||||
matches!(self.free_plan_selected, FreePlanSelection::Exit),
|
||||
));
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(" Press Enter to confirm").add_modifier(Modifier::DIM));
|
||||
|
||||
Paragraph::new(lines)
|
||||
.wrap(Wrap { trim: false })
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_env_var_found(&self, area: Rect, buf: &mut Buffer) {
|
||||
let lines = vec![Line::from("✓ Using OPENAI_API_KEY").fg(Color::Green)];
|
||||
|
||||
@@ -314,8 +402,14 @@ impl AuthModeWidget {
|
||||
fn start_chatgpt_login(&mut self) {
|
||||
// If we're already authenticated with ChatGPT, don't start a new login –
|
||||
// just proceed to the success message flow.
|
||||
if matches!(self.login_status, LoginStatus::AuthMode(AuthMode::ChatGPT)) {
|
||||
self.sign_in_state = SignInState::ChatGptSuccess;
|
||||
if let LoginStatus::Auth(auth) = &self.login_status
|
||||
&& auth.mode == AuthMode::ChatGPT
|
||||
{
|
||||
if auth.get_plan_type().as_deref() == Some("free") {
|
||||
self.sign_in_state = SignInState::FreePlan;
|
||||
} else {
|
||||
self.sign_in_state = SignInState::ChatGptSuccess;
|
||||
}
|
||||
self.event_tx.send(AppEvent::RequestRedraw);
|
||||
return;
|
||||
}
|
||||
@@ -348,14 +442,16 @@ impl AuthModeWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// TODO: Read/write from the correct hierarchy config overrides + auth json + OPENAI_API_KEY.
|
||||
fn verify_api_key(&mut self) {
|
||||
if matches!(self.login_status, LoginStatus::AuthMode(AuthMode::ApiKey)) {
|
||||
// We already have an API key configured (e.g., from auth.json or env),
|
||||
// so mark this step complete immediately.
|
||||
self.sign_in_state = SignInState::EnvVarFound;
|
||||
} else {
|
||||
self.sign_in_state = SignInState::EnvVarMissing;
|
||||
match &self.login_status {
|
||||
LoginStatus::Auth(auth) if auth.mode == AuthMode::ApiKey => {
|
||||
// We already have an API key configured (e.g., from auth.json or env),
|
||||
// so mark this step complete immediately.
|
||||
self.sign_in_state = SignInState::EnvVarFound;
|
||||
}
|
||||
_ => {
|
||||
self.sign_in_state = SignInState::EnvVarMissing;
|
||||
}
|
||||
}
|
||||
|
||||
self.event_tx.send(AppEvent::RequestRedraw);
|
||||
@@ -383,7 +479,8 @@ impl StepStateProvider for AuthModeWidget {
|
||||
SignInState::PickMode
|
||||
| SignInState::EnvVarMissing
|
||||
| SignInState::ChatGptContinueInBrowser(_)
|
||||
| SignInState::ChatGptSuccessMessage => StepState::InProgress,
|
||||
| SignInState::ChatGptSuccessMessage
|
||||
| SignInState::FreePlan => StepState::InProgress,
|
||||
SignInState::ChatGptSuccess | SignInState::EnvVarFound => StepState::Complete,
|
||||
}
|
||||
}
|
||||
@@ -404,6 +501,9 @@ impl WidgetRef for AuthModeWidget {
|
||||
SignInState::ChatGptSuccess => {
|
||||
self.render_chatgpt_success(area, buf);
|
||||
}
|
||||
SignInState::FreePlan => {
|
||||
self.render_free_plan(area, buf);
|
||||
}
|
||||
SignInState::EnvVarMissing => {
|
||||
self.render_env_var_missing(area, buf);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::onboarding::continue_to_chat::ContinueToChatWidget;
|
||||
use crate::onboarding::trust_directory::TrustDirectorySelection;
|
||||
use crate::onboarding::trust_directory::TrustDirectoryWidget;
|
||||
use crate::onboarding::welcome::WelcomeWidget;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
@@ -73,6 +74,8 @@ impl OnboardingScreen {
|
||||
let mut steps: Vec<Step> = vec![Step::Welcome(WelcomeWidget {
|
||||
is_logged_in: !matches!(login_status, LoginStatus::NotAuthenticated),
|
||||
})];
|
||||
//
|
||||
|
||||
if show_login_screen {
|
||||
steps.push(Step::Auth(AuthModeWidget {
|
||||
event_tx: event_tx.clone(),
|
||||
@@ -82,6 +85,7 @@ impl OnboardingScreen {
|
||||
codex_home: codex_home.clone(),
|
||||
login_status,
|
||||
preferred_auth_method: chat_widget_args.config.preferred_auth_method,
|
||||
free_plan_selected: crate::onboarding::auth::FreePlanSelection::Upgrade,
|
||||
}))
|
||||
}
|
||||
let is_git_repo = is_inside_git_repo(&cwd);
|
||||
@@ -118,16 +122,30 @@ impl OnboardingScreen {
|
||||
if let Some(Step::Auth(state)) = current_step {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
state.sign_in_state = SignInState::ChatGptSuccessMessage;
|
||||
self.event_tx.send(AppEvent::RequestRedraw);
|
||||
let tx1 = self.event_tx.clone();
|
||||
let tx2 = self.event_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(150));
|
||||
tx1.send(AppEvent::RequestRedraw);
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
tx2.send(AppEvent::RequestRedraw);
|
||||
});
|
||||
// After login, if the plan is free, show the Free Plan state inside Auth.
|
||||
let is_free = match codex_login::CodexAuth::from_codex_home(
|
||||
&state.codex_home,
|
||||
state.preferred_auth_method,
|
||||
) {
|
||||
Ok(Some(auth)) => auth.get_plan_type().as_deref() == Some("free"),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if is_free {
|
||||
state.sign_in_state = SignInState::FreePlan;
|
||||
self.event_tx.send(AppEvent::RequestRedraw);
|
||||
} else {
|
||||
state.sign_in_state = SignInState::ChatGptSuccessMessage;
|
||||
self.event_tx.send(AppEvent::RequestRedraw);
|
||||
let tx1 = self.event_tx.clone();
|
||||
let tx2 = self.event_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(150));
|
||||
tx1.send(AppEvent::RequestRedraw);
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
tx2.send(AppEvent::RequestRedraw);
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
state.sign_in_state = SignInState::PickMode;
|
||||
|
||||
Reference in New Issue
Block a user