1use async_cell::sync::AsyncCell;
2use js_sys::{
3 Array,
4 Object,
5 Reflect::{get, set},
6};
7use log::{error, info, warn};
8use wasm_bindgen::{
9 JsValue,
10 prelude::{
11 Closure,
12 wasm_bindgen,
13 },
14};
15use web_sys::HtmlElement;
16
17pub struct IdConfiguration {
21 client_id: String,
22 auto_select: Option<bool>,
23 callback: Option<Box<dyn Fn(CredentialResponse)>>,
24 login_uri: Option<String>,
25 native_callback: Option<Box<dyn Fn(Credential)>>,
26 cancel_on_tap_outside: Option<bool>,
27 prompt_parent_id: Option<String>,
28 nonce: Option<String>,
29 context: Option<String>,
30 state_cookie_domain: Option<String>,
31 ux_mode: Option<UxMode>,
32 allowed_parent_origin: Box<[String]>,
33 itp_support: Option<bool>,
35 login_hint: Option<String>,
36 hd: Option<String>,
37 use_fedcm_for_prompt: Option<bool>,
38}
39
40impl IdConfiguration {
41 pub fn new(client_id: String) -> Self {
47 Self {
48 client_id,
49 auto_select: None,
50 callback: None,
51 login_uri: None,
52 native_callback: None,
53 cancel_on_tap_outside: None,
54 prompt_parent_id: None,
55 nonce: None,
56 context: None,
57 state_cookie_domain: None,
58 ux_mode: None,
59 allowed_parent_origin: Default::default(),
60 itp_support: None,
61 login_hint: None,
62 hd: None,
63 use_fedcm_for_prompt: None,
64 }
65 }
66
67 pub fn set_auto_select(&mut self, auto_select: bool) {
69 self.auto_select = Some(auto_select);
70 }
71 pub fn set_callback(&mut self, callback: Box<dyn Fn(CredentialResponse)>) {
83 self.callback = Some(callback);
84 }
85 pub fn set_login_uri(&mut self, login_uri: String) {
87 self.login_uri = Some(login_uri);
88 }
89 pub fn set_native_callback(&mut self, native_callback: Box<dyn Fn(Credential)>) {
91 self.native_callback = Some(native_callback);
92 }
93 pub fn set_cancel_on_tap_outside(&mut self, cancel_on_tap_outside: bool) {
95 self.cancel_on_tap_outside = Some(cancel_on_tap_outside);
96 }
97 pub fn set_prompt_parent_id(&mut self, prompt_parent_id: String) {
99 self.prompt_parent_id = Some(prompt_parent_id);
100 }
101 pub fn set_nonce(&mut self, nonce: String) {
103 self.nonce = Some(nonce);
104 }
105 pub fn set_context(&mut self, context: String) {
107 self.context = Some(context);
108 }
109 pub fn set_state_cookie_domain(&mut self, state_cookie_domain: String) {
111 self.state_cookie_domain = Some(state_cookie_domain);
112 }
113 pub fn set_ux_mode(&mut self, ux_mode: UxMode) {
115 self.ux_mode = Some(ux_mode);
116 }
117 pub fn set_allowed_parent_origin(&mut self, allowed_parent_origin: Box<[String]>) {
119 self.allowed_parent_origin = allowed_parent_origin;
120 }
121 pub fn set_itp_support(&mut self, itp_support: bool) {
123 self.itp_support = Some(itp_support);
124 }
125 pub fn set_login_hint(&mut self, login_hint: String) {
127 self.login_hint = Some(login_hint);
128 }
129 pub fn set_hd(&mut self, hd: String) {
131 self.hd = Some(hd);
132 }
133 pub fn set_use_fedcm_for_prompt(&mut self, use_fedcm_for_prompt: bool) {
135 self.use_fedcm_for_prompt = Some(use_fedcm_for_prompt);
136 }
137}
138
139#[derive(Debug, Clone, PartialEq)]
143pub struct CredentialResponse {
144 credential: String,
145 select_by: SelectBy,
146}
147
148impl CredentialResponse {
149 pub fn credential(&self) -> &str {
152 &self.credential
153 }
154 pub fn select_by(&self) -> SelectBy {
155 self.select_by
156 }
157}
158
159pub struct Credential {
163 id: String,
164 password: String,
165}
166
167impl Credential {
168 pub fn id(&self) -> &str {
169 &self.id
170 }
171 pub fn password(&self) -> &str {
172 &self.password
173 }
174}
175
176#[derive(Copy, Clone, Debug, PartialEq)]
180pub enum SelectBy {
181 Auto,
182 User,
183 User1Tap,
184 User2Tap,
185 Btn,
186 BtnConfirm,
187 BtnAddSession,
188 BtnConfirmAddSession,
189 FedCM,
190}
191
192pub enum UxMode {
195 Popup,
197 Redirect,
199}
200
201impl SelectBy {
202 fn new_from_response(value: &str) -> Option<SelectBy> {
203 if value == "auto" {
204 Some(SelectBy::Auto)
205 } else if value == "user" {
206 Some(SelectBy::User)
207 } else if value == "user_1tap" {
208 Some(SelectBy::User1Tap)
209 } else if value == "user_2tap" {
210 Some(SelectBy::User2Tap)
211 } else if value == "btn" {
212 Some(SelectBy::Btn)
213 } else if value == "btn_confirm" {
214 Some(SelectBy::BtnConfirm)
215 } else if value == "btn_add_session" {
216 Some(SelectBy::BtnAddSession)
217 } else if value == "btn_confirm_add_session" {
218 Some(SelectBy::BtnConfirmAddSession)
219 } else if value == "fedcm" {
220 Some(SelectBy::FedCM)
221 } else {
222 error!("Unsupported response: {value}");
223 None
224 }
225 }
226}
227
228pub fn initialize(settings: IdConfiguration) {
232 let object = Object::new();
233 set(
234 &object,
235 &JsValue::from_str("client_id"),
236 &JsValue::from_str(&settings.client_id),
237 )
238 .expect("cannot write client_id");
239 write_bool("auto_select", settings.auto_select, &object);
240 if let Some(callback) = settings.callback {
241 let callback = Closure::<dyn Fn(JsValue)>::new(move |v| {
242 if let (Some(credential), Some(select_by)) = (
243 get(&v, &JsValue::from_str("credential"))
244 .expect("Cannot read credentials")
245 .as_string(),
246 get(&v, &JsValue::from_str("select_by"))
247 .expect("Cannot read select_by")
248 .as_string()
249 .and_then(|v| SelectBy::new_from_response(&v)),
250 ) {
251 callback(CredentialResponse {
252 credential,
253 select_by,
254 });
255 } else {
256 warn!("No valid CredentialResponse");
257 }
258 });
259 set(
260 &object,
261 &JsValue::from_str("callback"),
262 &callback.into_js_value(),
263 )
264 .expect("Cannot write callback");
265 }
266 write_string("login_uri", settings.login_uri.as_deref(), &object);
267 if let Some(native_callback) = settings.native_callback {
268 let callback = Closure::<dyn Fn(JsValue)>::new(move |v| {
269 if let (Some(id), Some(password)) = (
270 get(&v, &JsValue::from_str("id"))
271 .expect("Cannot read id")
272 .as_string(),
273 get(&v, &JsValue::from_str("password"))
274 .expect("Cannot read password")
275 .as_string(),
276 ) {
277 native_callback(Credential { id, password });
278 } else {
279 warn!("No valid Credential");
280 }
281 });
282 set(
283 &object,
284 &JsValue::from_str("callback"),
285 &callback.into_js_value(),
286 )
287 .expect("Cannot write callback");
288 }
289 write_bool(
290 "cancel_on_tap_outside",
291 settings.cancel_on_tap_outside,
292 &object,
293 );
294 write_string(
295 "prompt_parent_id",
296 settings.prompt_parent_id.as_deref(),
297 &object,
298 );
299 write_string("nonce", settings.nonce.as_deref(), &object);
300 write_string("context", settings.context.as_deref(), &object);
301 write_string(
302 "state_cookie_domain",
303 settings.state_cookie_domain.as_deref(),
304 &object,
305 );
306 write_string(
307 "ux_mode",
308 settings.ux_mode.map(|m| match m {
309 UxMode::Popup => "popup",
310 UxMode::Redirect => "redirect",
311 }),
312 &object,
313 );
314
315 if settings.allowed_parent_origin.len() > 1 {
316 let array = Array::new_with_length(settings.allowed_parent_origin.len() as u32);
317 for (idx, value) in settings.allowed_parent_origin.iter().enumerate() {
318 array.set(idx as u32, JsValue::from_str(value));
319 }
320
321 set(
322 &object,
323 &JsValue::from_str("allowed_parent_origin"),
324 &JsValue::from(&array),
325 )
326 .expect("cannot write allowed_parent_origin");
327 } else {
328 write_string(
329 "allowed_parent_origin",
330 settings.allowed_parent_origin.first().map(|x| x.as_str()),
331 &object,
332 )
333 }
334 write_bool("itp_support", settings.itp_support, &object);
336 write_string("login_hint", settings.login_hint.as_deref(), &object);
337 write_string("hd", settings.hd.as_deref(), &object);
338 write_bool(
339 "use_fedcm_for_prompt",
340 settings.use_fedcm_for_prompt,
341 &object,
342 );
343 initialize_js(object.into());
344}
345
346fn write_bool(field: &str, value: Option<bool>, object: &Object) {
347 if let Some(value) = value {
348 set(
349 object,
350 &JsValue::from_str(field),
351 &JsValue::from_bool(value),
352 )
353 .expect("cannot write boolean field");
354 }
355}
356
357fn write_string(field: &str, value: Option<&str>, object: &Object) {
358 if let Some(value) = value {
359 set(object, &JsValue::from_str(field), &JsValue::from_str(value))
360 .expect("cannot write string field");
361 }
362}
363
364pub fn prompt(callback: Option<Box<dyn Fn(PromptMomentNotification)>>) {
368 if let Some(handler) = do_prompt(callback) {
369 handler.into_js_value();
370 }
371}
372
373pub async fn prompt_async() -> PromptResult {
387 let cell = AsyncCell::shared();
388 let future = cell.take_shared();
389
390 let handle = do_prompt(Some(Box::new(
391 move |notification: PromptMomentNotification| {
392 if let Some(result) = match notification {
393 PromptMomentNotification::Display(Some(reason)) => {
394 Some(PromptResult::NotDisplayed(reason))
395 }
396 PromptMomentNotification::Skipped(reason) => Some(PromptResult::Skipped(reason)),
397 PromptMomentNotification::Dismissed(reason) => {
398 Some(PromptResult::Dismissed(reason))
399 }
400 PromptMomentNotification::Display(None) => None,
401 } {
402 cell.set(result);
403 } else {
404 info!("Event: ยด{notification:?}");
405 }
406 },
407 )));
408 let result = future.await;
409 drop(handle);
410 result
411}
412
413fn do_prompt(
414 callback: Option<Box<dyn Fn(PromptMomentNotification)>>,
415) -> Option<Closure<dyn FnMut(PromptMomentNotificationJS)>> {
416 let handler = callback.map(|c| {
417 Closure::new(move |moment: PromptMomentNotificationJS| {
418 let moment_type = moment.getMomentType().as_string();
419 let moment_notification = match moment_type.as_deref() {
420 Some("display") => {
421 let reason_string = moment.getNotDisplayedReason().as_string();
422 let not_display_reason = reason_string.as_deref().map(|reason| match reason {
423 "browser_not_supported" => NotDisplayedReason::BrowserNotSupported,
424 "invalid_client" => NotDisplayedReason::InvalidClient,
425 "missing_client_id" => NotDisplayedReason::MissingClientId,
426 "opt_out_or_no_session" => NotDisplayedReason::OpOutOrNoSession,
427 "secure_http_required" => NotDisplayedReason::SecureHttpRequired,
428 "suppressed_by_user" => NotDisplayedReason::SuppressedByUser,
429 "unregistered_origin" => NotDisplayedReason::UnregisteredOrigin,
430 "unknown_reason" => NotDisplayedReason::UnknownReason,
431 _other => panic!("Unsupported display reason: {_other}"),
432 });
433 PromptMomentNotification::Display(not_display_reason)
434 }
435 Some("skipped") => {
436 let reason_string = moment
437 .getSkippedReason()
438 .as_string()
439 .expect("missing skipped reason");
440 let reason = match reason_string.as_str() {
441 "auto_cancel" => SkippedReason::AutoCancel,
442 "user_cancel" => SkippedReason::UserCancel,
443 "tap_outside" => SkippedReason::TapOutside,
444 "issuing_failed" => SkippedReason::IssuingFailed,
445 _other => panic!("Unsupported skipped reason: {_other}"),
446 };
447 PromptMomentNotification::Skipped(reason)
448 }
449 Some("dismissed") => {
450 let reason_string = moment
451 .getDismissedReason()
452 .as_string()
453 .expect("missing dismissed reason");
454 let reason = match reason_string.as_str() {
455 "credential_returned" => DismissedReason::CredentialReturned,
456 "cancel_called" => DismissedReason::CancelCalled,
457 "flow_restarted" => DismissedReason::FlowRestarted,
458 _other => panic!("Unsupported dismissed reason: {_other}"),
459 };
460 PromptMomentNotification::Dismissed(reason)
461 }
462 _ => {
463 panic!("Unknown moment type: {moment_type:?}");
464 }
465 };
466 c(moment_notification);
467 })
468 });
469 prompt_js(handler.as_ref());
470 handler
471}
472
473#[derive(Debug, Copy, Clone, Eq, PartialEq)]
477pub enum PromptMomentNotification {
478 Display(Option<NotDisplayedReason>),
479 Skipped(SkippedReason),
480 Dismissed(DismissedReason),
481}
482
483#[derive(Debug, Copy, Clone, Eq, PartialEq)]
487pub enum PromptResult {
488 NotDisplayed(NotDisplayedReason),
489 Skipped(SkippedReason),
490 Dismissed(DismissedReason),
491}
492
493#[derive(Debug, Copy, Clone, Eq, PartialEq)]
497pub enum NotDisplayedReason {
498 BrowserNotSupported,
499 InvalidClient,
500 MissingClientId,
501 OpOutOrNoSession,
502 SecureHttpRequired,
503 SuppressedByUser,
504 UnregisteredOrigin,
505 UnknownReason,
506}
507
508#[derive(Debug, Copy, Clone, Eq, PartialEq)]
512pub enum SkippedReason {
513 AutoCancel,
514 UserCancel,
515 TapOutside,
516 IssuingFailed,
517}
518
519#[derive(Debug, Copy, Clone, Eq, PartialEq)]
523pub enum DismissedReason {
524 CredentialReturned,
525 CancelCalled,
526 FlowRestarted,
527}
528
529pub struct GsiButtonConfiguration {
533 r#type: ButtonType,
534 theme: Option<ButtonTheme>,
535 size: Option<ButtonSize>,
536}
537
538impl GsiButtonConfiguration {
539 pub fn new(r#type: ButtonType) -> Self {
540 Self {
541 r#type,
542 theme: None,
543 size: None,
544 }
545 }
546}
547
548pub enum ButtonType {
552 Standard,
553 Icon,
554}
555
556pub enum ButtonTheme {
560 Outline,
561 FilledBlue,
562 FilledBlack,
563}
564
565pub enum ButtonSize {
569 Large,
570 Medium,
571 Small,
572}
573
574pub fn render_button(parent: HtmlElement, options: GsiButtonConfiguration) {
578 let object = Object::new();
579 let type_str = match options.r#type {
580 ButtonType::Icon => "icon",
581 ButtonType::Standard => "standard",
582 };
583 set(
584 &object,
585 &JsValue::from_str("client_id"),
586 &JsValue::from_str(type_str),
587 )
588 .expect("cannot write type field");
589 let theme = options.theme.map(|t| match t {
590 ButtonTheme::Outline => "outline",
591 ButtonTheme::FilledBlue => "filled_blue",
592 ButtonTheme::FilledBlack => "filled_black",
593 });
594 write_string("theme", theme, &object);
595 let size = options.size.map(|s| match s {
596 ButtonSize::Large => "large",
597 ButtonSize::Medium => "medium",
598 ButtonSize::Small => "small",
599 });
600 write_string("size", size, &object);
601 render_button_js(parent, object.into());
602}
603
604#[wasm_bindgen]
605extern "C" {
606 #[wasm_bindgen(js_namespace = ["google", "accounts", "id"], js_name = "initialize")]
607 fn initialize_js(idConfiguration: JsValue);
608 #[wasm_bindgen(js_namespace = ["google", "accounts", "id"], js_name = "prompt")]
609 fn prompt_js(callback: Option<&Closure<dyn FnMut(PromptMomentNotificationJS)>>);
610 #[wasm_bindgen(js_namespace = ["google", "accounts", "id"], js_name = "renderButton")]
611 fn render_button_js(parent: HtmlElement, options: JsValue);
612 #[wasm_bindgen(
613 js_namespace = ["google", "accounts", "id"], js_name = "PromptMomentNotification"
614 )]
615 #[derive(Debug)]
616 type PromptMomentNotificationJS;
617 #[wasm_bindgen(method)]
618 #[deprecated = "see https://developers.google.com/identity/gsi/web/guides/fedcm-migration?s=dc#display_moment"]
619 fn isDisplayMoment(this: &PromptMomentNotificationJS) -> bool;
620 #[wasm_bindgen(method)]
621 #[deprecated = "see https://developers.google.com/identity/gsi/web/guides/fedcm-migration?s=dc#display_moment"]
622 fn isDisplayed(this: &PromptMomentNotificationJS) -> bool;
623 #[wasm_bindgen(method)]
624 #[deprecated = "see https://developers.google.com/identity/gsi/web/guides/fedcm-migration?s=dc#display_moment"]
625 fn isNotDisplayed(this: &PromptMomentNotificationJS) -> bool;
626 #[wasm_bindgen(method)]
627 fn isDismissedMoment(this: &PromptMomentNotificationJS) -> bool;
628 #[wasm_bindgen(method)]
629 fn getMomentType(this: &PromptMomentNotificationJS) -> JsValue;
630 #[wasm_bindgen(method)]
631 #[deprecated = "see https://developers.google.com/identity/gsi/web/guides/fedcm-migration?s=dc#display_moment"]
632 fn getNotDisplayedReason(this: &PromptMomentNotificationJS) -> JsValue;
633 #[wasm_bindgen(method)]
634 #[deprecated = "see https://developers.google.com/identity/gsi/web/guides/fedcm-migration?s=dc#display_moment"]
635 fn getSkippedReason(this: &PromptMomentNotificationJS) -> JsValue;
636 #[wasm_bindgen(method)]
637 fn getDismissedReason(this: &PromptMomentNotificationJS) -> JsValue;
638}