atproto blogging
1use dioxus::logger::tracing::{error, info};
2use dioxus::prelude::*;
3use jacquard::oauth::client::OAuthClient;
4use jacquard::oauth::session::ClientData;
5use jacquard::{oauth::types::AuthorizeOptions, smol_str::SmolStr};
6
7use crate::components::{
8 button::{Button, ButtonVariant},
9 dialog::{DialogContent, DialogRoot, DialogTitle},
10 input::Input,
11};
12use crate::fetch::Fetcher;
13use crate::CONFIG;
14
15fn handle_submit(
16 cached_route: String,
17 fetcher: Fetcher,
18 mut error: Signal<Option<String>>,
19 mut is_loading: Signal<bool>,
20 handle_input: Signal<String>,
21 mut open: Signal<bool>,
22) {
23 let handle = handle_input.read().clone();
24 if handle.is_empty() {
25 error.set(Some("Please enter a handle".to_string()));
26 return;
27 }
28
29 is_loading.set(true);
30 error.set(None);
31
32 #[cfg(target_arch = "wasm32")]
33 {
34 use gloo_storage::Storage;
35 gloo_storage::LocalStorage::set("cached_route", cached_route).ok();
36 spawn(async move {
37 match start_oauth_flow(handle, fetcher).await {
38 Ok(_) => {
39 open.set(false);
40 }
41 Err(e) => {
42 error!("Authentication failed: {}", e);
43 error.set(Some(format!("Authentication failed: {}", e)));
44 is_loading.set(false);
45 }
46 }
47 });
48 }
49}
50
51#[component]
52pub fn LoginModal(open: Signal<bool>, cached_route: String) -> Element {
53 let mut handle_input = use_signal(|| String::new());
54 let error = use_signal(|| Option::<String>::None);
55 let is_loading = use_signal(|| false);
56 let fetcher = use_context::<Fetcher>();
57 let cached_route_clone = cached_route.clone();
58 let submit_fetcher = fetcher.clone();
59 let submit_closure1 = move || {
60 let cached_route = cached_route_clone.clone();
61 let submit_fetcher = submit_fetcher.clone();
62 handle_submit(
63 cached_route,
64 submit_fetcher,
65 error,
66 is_loading,
67 handle_input,
68 open,
69 );
70 };
71
72 let submit_closure2 = move || {
73 let cached_route = cached_route.clone();
74 let submit_fetcher = fetcher.clone();
75 handle_submit(
76 cached_route,
77 submit_fetcher,
78 error,
79 is_loading,
80 handle_input,
81 open,
82 );
83 };
84
85 rsx! {
86 DialogRoot { open: open(), on_open_change: move |v| open.set(v),
87 DialogContent {
88 button {
89 class: "dialog-close",
90 r#type: "button",
91 aria_label: "Close",
92 tabindex: if open() { "0" } else { "-1" },
93 onclick: move |_| {
94 open.set(false)
95 },
96 "×"
97 }
98 DialogTitle { "Sign In with AT Protocol" }
99 Input {
100 aria_label: "Handle",
101 oninput: move |e: FormEvent| handle_input.set(e.value()),
102 onkeypress: move |k: KeyboardEvent| {
103 if k.key() == Key::Enter {
104 submit_closure1();
105 }
106 },
107 placeholder: "Enter your handle",
108 value: "{handle_input}",
109 }
110 if let Some(err) = error() {
111 div { class: "error", "{err}" }
112 }
113 Button {
114 r#type: "submit",
115 disabled: is_loading(),
116 onclick: move |_| {
117 submit_closure2();
118 },
119 if is_loading() { "Authenticating..." } else { "Sign In" }
120 }
121 Button {
122 r#type: "button",
123 onclick: move |_| {
124 open.set(false)
125 },
126 disabled: is_loading(),
127 variant: ButtonVariant::Secondary,
128 "Cancel"
129 }
130
131
132 }
133 }
134 }
135}
136
137async fn start_oauth_flow(handle: String, fetcher: Fetcher) -> Result<(), SmolStr> {
138 info!("Starting OAuth flow for handle: {}", handle);
139
140 let client_data = ClientData {
141 keyset: fetcher
142 .client
143 .oauth_client
144 .registry
145 .client_data
146 .keyset
147 .clone(),
148 config: CONFIG.oauth.clone(),
149 };
150
151 // Build client using store and resolver
152 let flow_client = OAuthClient::new_with_shared(
153 fetcher.client.oauth_client.registry.store.clone(),
154 fetcher.client.oauth_client.client.clone(),
155 client_data.clone(),
156 );
157
158 let auth_url = flow_client
159 .start_auth(handle, AuthorizeOptions::default())
160 .await
161 .map_err(|e| format!("{:?}", e))?;
162 #[cfg(target_arch = "wasm32")]
163 {
164 let window = web_sys::window().ok_or("no window")?;
165 let location = window.location();
166 location
167 .set_href(&auth_url)
168 .map_err(|e| format!("{:?}", e))?;
169 }
170 #[cfg(not(target_arch = "wasm32"))]
171 {
172 webbrowser::open(&auth_url).map_err(|e| format!("{:?}", e))?;
173 }
174 Ok(())
175}