Add auth callback internal page
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
mod about;
|
mod about;
|
||||||
mod advanced;
|
mod advanced;
|
||||||
mod appearance;
|
mod appearance;
|
||||||
|
mod auth_callback;
|
||||||
mod bookmarks;
|
mod bookmarks;
|
||||||
mod crash;
|
mod crash;
|
||||||
mod download_actions;
|
mod download_actions;
|
||||||
@@ -70,6 +71,7 @@ impl ElyShell {
|
|||||||
"ely://task-manager" => self.render_task_manager_page(snapshot),
|
"ely://task-manager" => self.render_task_manager_page(snapshot),
|
||||||
"ely://site-compatibility" => self.render_site_compatibility_page(snapshot),
|
"ely://site-compatibility" => self.render_site_compatibility_page(snapshot),
|
||||||
"ely://plugins" => self.render_plugin_catalog_page(snapshot, cx),
|
"ely://plugins" => self.render_plugin_catalog_page(snapshot, cx),
|
||||||
|
url if url.starts_with("ely://auth/callback") => self.render_auth_callback_page(url),
|
||||||
url if url.starts_with("ely://crash/") => self.render_crash_route(snapshot, url, cx),
|
url if url.starts_with("ely://crash/") => self.render_crash_route(snapshot, url, cx),
|
||||||
url if url.starts_with("ely://plugin/") => {
|
url if url.starts_with("ely://plugin/") => {
|
||||||
self.render_plugin_detail_page(snapshot, url, cx)
|
self.render_plugin_detail_page(snapshot, url, cx)
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
use ely_design_system::colors;
|
||||||
|
use gpui::{AnyElement, IntoElement, ParentElement, Styled, div, px, rgb};
|
||||||
|
use gpui_component::{IconName, StyledExt};
|
||||||
|
|
||||||
|
use crate::brand::{AUTH_CALLBACK_URL, SYNC_SERVICE_NAME};
|
||||||
|
|
||||||
|
use super::{ElyShell, render_canvas_surface};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
enum AuthCallbackState {
|
||||||
|
CodeReceived,
|
||||||
|
ProviderError(String),
|
||||||
|
MissingCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ElyShell {
|
||||||
|
pub(super) fn render_auth_callback_page(&mut self, url: &str) -> AnyElement {
|
||||||
|
let state = auth_callback_state(url);
|
||||||
|
render_canvas_surface(
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.p_8()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_5()
|
||||||
|
.child(render_auth_callback_header(&state))
|
||||||
|
.child(render_auth_callback_rows(&state)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_auth_callback_header(state: &AuthCallbackState) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_end()
|
||||||
|
.justify_between()
|
||||||
|
.gap_4()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
div().text_size(px(26.0)).text_color(rgb(colors::INK)).child("Auth Callback"),
|
||||||
|
)
|
||||||
|
.child(div().text_sm().text_color(rgb(colors::MUTED)).child(SYNC_SERVICE_NAME)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.text_xs()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(rgb(auth_callback_color(state)))
|
||||||
|
.child(auth_callback_icon(state))
|
||||||
|
.child(auth_callback_badge(state)),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_auth_callback_rows(state: &AuthCallbackState) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.min_h_0()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(rgb(colors::HAIRLINE))
|
||||||
|
.child(auth_callback_row(
|
||||||
|
IconName::Globe,
|
||||||
|
"Service",
|
||||||
|
SYNC_SERVICE_NAME,
|
||||||
|
"Cloud account authority",
|
||||||
|
))
|
||||||
|
.child(auth_callback_row(
|
||||||
|
IconName::ExternalLink,
|
||||||
|
"Callback",
|
||||||
|
AUTH_CALLBACK_URL,
|
||||||
|
"Desktop protocol",
|
||||||
|
))
|
||||||
|
.child(auth_callback_row(
|
||||||
|
auth_callback_icon(state),
|
||||||
|
"Result",
|
||||||
|
auth_callback_result(state),
|
||||||
|
auth_callback_detail(state),
|
||||||
|
))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auth_callback_row(
|
||||||
|
icon: IconName,
|
||||||
|
label: &'static str,
|
||||||
|
value: impl Into<String>,
|
||||||
|
detail: impl Into<String>,
|
||||||
|
) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.py_4()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(rgb(colors::HAIRLINE))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.gap_4()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.min_w_0()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_3()
|
||||||
|
.child(div().text_color(rgb(colors::MUTED)).child(icon))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.min_w_0()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(rgb(colors::INK))
|
||||||
|
.child(label),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.truncate()
|
||||||
|
.text_color(rgb(colors::MUTED))
|
||||||
|
.child(detail.into()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(div().text_xs().font_semibold().text_color(rgb(colors::MUTED)).child(value.into()))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auth_callback_state(url: &str) -> AuthCallbackState {
|
||||||
|
if let Some(error) = query_value(url, "error")
|
||||||
|
&& !error.is_empty()
|
||||||
|
{
|
||||||
|
return AuthCallbackState::ProviderError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if query_value(url, "code").is_some_and(|code| !code.is_empty()) {
|
||||||
|
return AuthCallbackState::CodeReceived;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthCallbackState::MissingCode
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_value(url: &str, name: &str) -> Option<String> {
|
||||||
|
let query = url.strip_prefix(AUTH_CALLBACK_URL)?.strip_prefix('?')?;
|
||||||
|
query.split('&').find_map(|pair| {
|
||||||
|
let (key, value) = pair.split_once('=')?;
|
||||||
|
(key == name).then(|| value.to_string())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auth_callback_icon(state: &AuthCallbackState) -> IconName {
|
||||||
|
match state {
|
||||||
|
AuthCallbackState::CodeReceived => IconName::CircleCheck,
|
||||||
|
AuthCallbackState::ProviderError(_) => IconName::TriangleAlert,
|
||||||
|
AuthCallbackState::MissingCode => IconName::Info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auth_callback_color(state: &AuthCallbackState) -> u32 {
|
||||||
|
match state {
|
||||||
|
AuthCallbackState::CodeReceived => colors::SUCCESS,
|
||||||
|
AuthCallbackState::ProviderError(_) => colors::ERROR,
|
||||||
|
AuthCallbackState::MissingCode => colors::MUTED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auth_callback_badge(state: &AuthCallbackState) -> &'static str {
|
||||||
|
match state {
|
||||||
|
AuthCallbackState::CodeReceived => "Code received",
|
||||||
|
AuthCallbackState::ProviderError(_) => "Provider error",
|
||||||
|
AuthCallbackState::MissingCode => "Callback pending",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auth_callback_result(state: &AuthCallbackState) -> String {
|
||||||
|
match state {
|
||||||
|
AuthCallbackState::CodeReceived => "Authorization code".to_string(),
|
||||||
|
AuthCallbackState::ProviderError(error) => error.to_string(),
|
||||||
|
AuthCallbackState::MissingCode => "Awaiting code".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auth_callback_detail(state: &AuthCallbackState) -> &'static str {
|
||||||
|
match state {
|
||||||
|
AuthCallbackState::CodeReceived => "Ready for session exchange",
|
||||||
|
AuthCallbackState::ProviderError(_) => "Provider returned an OAuth error",
|
||||||
|
AuthCallbackState::MissingCode => "Callback code missing",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{AuthCallbackState, auth_callback_state, query_value};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auth_callback_state_reads_code() {
|
||||||
|
assert_eq!(
|
||||||
|
auth_callback_state("ely://auth/callback?code=abc&state=nonce"),
|
||||||
|
AuthCallbackState::CodeReceived
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auth_callback_state_reads_provider_error() {
|
||||||
|
assert_eq!(
|
||||||
|
auth_callback_state("ely://auth/callback?error=access_denied"),
|
||||||
|
AuthCallbackState::ProviderError("access_denied".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auth_callback_state_handles_missing_code() {
|
||||||
|
assert_eq!(auth_callback_state("ely://auth/callback"), AuthCallbackState::MissingCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn query_value_filters_other_routes() {
|
||||||
|
assert_eq!(query_value("ely://history?code=abc", "code"), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ fn internal_page_title(url: &str) -> Option<&'static str> {
|
|||||||
"ely://task-manager" => Some("Task Manager"),
|
"ely://task-manager" => Some("Task Manager"),
|
||||||
"ely://site-compatibility" => Some("Site Compatibility"),
|
"ely://site-compatibility" => Some("Site Compatibility"),
|
||||||
"ely://plugins" => Some("Plugin Marketplace"),
|
"ely://plugins" => Some("Plugin Marketplace"),
|
||||||
|
url if auth_callback_route(url) => Some("Auth Callback"),
|
||||||
url if crash_route_tab_id(url).is_some() => Some("Tab Recovery"),
|
url if crash_route_tab_id(url).is_some() => Some("Tab Recovery"),
|
||||||
url if plugin_detail_route_id(url).is_some() => Some("Plugin Details"),
|
url if plugin_detail_route_id(url).is_some() => Some("Plugin Details"),
|
||||||
url if SiteOrigin::from_site_route(url).ok().flatten().is_some() => Some("Site Settings"),
|
url if SiteOrigin::from_site_route(url).ok().flatten().is_some() => Some("Site Settings"),
|
||||||
@@ -486,3 +487,7 @@ pub(crate) fn crash_route_tab_id(url: &str) -> Option<&str> {
|
|||||||
let tab_id = url.strip_prefix("ely://crash/")?;
|
let tab_id = url.strip_prefix("ely://crash/")?;
|
||||||
(!tab_id.is_empty() && !tab_id.contains('/')).then_some(tab_id)
|
(!tab_id.is_empty() && !tab_id.contains('/')).then_some(tab_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn auth_callback_route(url: &str) -> bool {
|
||||||
|
matches!(url, "ely://auth/callback") || url.starts_with("ely://auth/callback?")
|
||||||
|
}
|
||||||
|
|||||||
@@ -72,6 +72,19 @@ fn opened_tabs_record_active_tab_as_parent() -> Result<(), Box<dyn Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auth_callback_tabs_receive_internal_title() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|
||||||
|
let tab_id = core.open_tab(UrlText::parse("ely://auth/callback?code=abc")?);
|
||||||
|
let snapshot = core.snapshot()?;
|
||||||
|
let tab =
|
||||||
|
snapshot.tabs.iter().find(|tab| tab.id() == &tab_id).ok_or(CoreError::MissingActiveTab)?;
|
||||||
|
|
||||||
|
assert_eq!(tab.title(), "Auth Callback");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn replacement_tabs_have_no_parent_tab() -> Result<(), Box<dyn Error>> {
|
fn replacement_tabs_have_no_parent_tab() -> Result<(), Box<dyn Error>> {
|
||||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user