google_signin_client/
lib.rs

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
17/// Settings for initialize
18///
19/// <https://developers.google.com/identity/gsi/web/reference/js-reference#IdConfiguration>
20pub 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    //intermediate_iframe_close_callback
34    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    /// Create a new IdConfiguration with only client_id filled in
42    /// ```rust
43    /// use google_signin_client::IdConfiguration;
44    /// let configuration = IdConfiguration::new("YOUR_GOOGLE_CLIENT_ID".to_string());
45    /// ```
46    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    /// <https://developers.google.com/identity/gsi/web/reference/js-reference#auto_select>
68    pub fn set_auto_select(&mut self, auto_select: bool) {
69        self.auto_select = Some(auto_select);
70    }
71    /// set a callback for a ID-Token
72    /// <https://developers.google.com/identity/gsi/web/reference/js-reference#callback>
73    /// ```rust
74    /// use log::info;
75    /// use google_signin_client::{IdConfiguration, UxMode};
76    /// let mut configuration = IdConfiguration::new("YOUR_GOOGLE_CLIENT_ID".to_string());
77    /// configuration.set_ux_mode(UxMode::Popup);
78    /// configuration.set_callback(Box::new(move |response| {
79    ///     info!("Callback received, token: {}", response.credential());
80    /// }));
81    /// ```
82    pub fn set_callback(&mut self, callback: Box<dyn Fn(CredentialResponse)>) {
83        self.callback = Some(callback);
84    }
85    /// <https://developers.google.com/identity/gsi/web/reference/js-reference#login_uri>
86    pub fn set_login_uri(&mut self, login_uri: String) {
87        self.login_uri = Some(login_uri);
88    }
89    /// <https://developers.google.com/identity/gsi/web/reference/js-reference#native_callback>
90    pub fn set_native_callback(&mut self, native_callback: Box<dyn Fn(Credential)>) {
91        self.native_callback = Some(native_callback);
92    }
93    /// <https://developers.google.com/identity/gsi/web/reference/js-reference#cancel_on_tap_outside>
94    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    /// <https://developers.google.com/identity/gsi/web/reference/js-reference#prompt_parent_id>
98    pub fn set_prompt_parent_id(&mut self, prompt_parent_id: String) {
99        self.prompt_parent_id = Some(prompt_parent_id);
100    }
101    /// <https://developers.google.com/identity/gsi/web/reference/js-reference#nonce>
102    pub fn set_nonce(&mut self, nonce: String) {
103        self.nonce = Some(nonce);
104    }
105    /// <https://developers.google.com/identity/gsi/web/reference/js-reference#context>
106    pub fn set_context(&mut self, context: String) {
107        self.context = Some(context);
108    }
109    /// <https://developers.google.com/identity/gsi/web/reference/js-reference#state_cookie_domain>
110    pub fn set_state_cookie_domain(&mut self, state_cookie_domain: String) {
111        self.state_cookie_domain = Some(state_cookie_domain);
112    }
113    /// <https://developers.google.com/identity/gsi/web/reference/js-reference#ux_mode>
114    pub fn set_ux_mode(&mut self, ux_mode: UxMode) {
115        self.ux_mode = Some(ux_mode);
116    }
117    /// <https://developers.google.com/identity/gsi/web/reference/js-reference#allowed_parent_origin>
118    pub fn set_allowed_parent_origin(&mut self, allowed_parent_origin: Box<[String]>) {
119        self.allowed_parent_origin = allowed_parent_origin;
120    }
121    /// <https://developers.google.com/identity/gsi/web/reference/js-reference#itp_support>
122    pub fn set_itp_support(&mut self, itp_support: bool) {
123        self.itp_support = Some(itp_support);
124    }
125    /// <https://developers.google.com/identity/gsi/web/reference/js-reference#login_hint>
126    pub fn set_login_hint(&mut self, login_hint: String) {
127        self.login_hint = Some(login_hint);
128    }
129    /// <https://developers.google.com/identity/gsi/web/reference/js-reference#hd>
130    pub fn set_hd(&mut self, hd: String) {
131        self.hd = Some(hd);
132    }
133    /// <https://developers.google.com/identity/gsi/web/reference/js-reference#use_fedcm_for_prompt>
134    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/// Credentials after login
140///
141/// <https://developers.google.com/identity/gsi/web/reference/js-reference#CredentialResponse>
142#[derive(Debug, Clone, PartialEq)]
143pub struct CredentialResponse {
144    credential: String,
145    select_by: SelectBy,
146}
147
148impl CredentialResponse {
149    /// JWT Token from Google
150    /// <https://developers.google.com/identity/gsi/web/reference/js-reference#credential>
151    pub fn credential(&self) -> &str {
152        &self.credential
153    }
154    pub fn select_by(&self) -> SelectBy {
155        self.select_by
156    }
157}
158
159/// Credentials from native_callback
160///
161/// <https://developers.google.com/identity/gsi/web/reference/js-reference#type-Credential>
162pub 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/// Information on the user interaction
177///
178/// <https://developers.google.com/identity/gsi/web/reference/js-reference#select_by>
179#[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
192/// UX Mode for login dialog
193/// <https://developers.google.com/identity/gsi/web/reference/js-reference#ux_mode>
194pub enum UxMode {
195    /// Show a Popup-Window for login
196    Popup,
197    /// Redirect whole browser to login page
198    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
228/// Initialize google sign in client
229///
230/// <https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.initialize>
231pub 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    // intermediate_iframe_close_callback
335    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
364/// call prompt with callback
365///
366/// <https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.prompt>
367pub fn prompt(callback: Option<Box<dyn Fn(PromptMomentNotification)>>) {
368    if let Some(handler) = do_prompt(callback) {
369        handler.into_js_value();
370    }
371}
372
373/// Async wrapper around google.accounts.id.prompt
374///
375/// more details on <https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.prompt>
376///
377/// ```rust
378/// use google_signin_client::{DismissedReason, prompt_async, PromptResult};
379/// async {
380///     let result = prompt_async().await;
381///     if result != PromptResult::Dismissed(DismissedReason::CredentialReturned) {
382///         // handle error condition (in case of success, the callback configured before is called)
383///     }
384/// };
385/// ```
386pub 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/// Rust wrapper for PromptMomentNotification
474///
475/// <https://developers.google.com/identity/gsi/web/reference/js-reference#PromptMomentNotification>
476#[derive(Debug, Copy, Clone, Eq, PartialEq)]
477pub enum PromptMomentNotification {
478    Display(Option<NotDisplayedReason>),
479    Skipped(SkippedReason),
480    Dismissed(DismissedReason),
481}
482
483/// Result from prompt response
484///
485/// <https://developers.google.com/identity/gsi/web/reference/js-reference#PromptMomentNotification>
486#[derive(Debug, Copy, Clone, Eq, PartialEq)]
487pub enum PromptResult {
488    NotDisplayed(NotDisplayedReason),
489    Skipped(SkippedReason),
490    Dismissed(DismissedReason),
491}
492
493/// result codes of getNotDisplayedReason()
494///
495/// <https://developers.google.com/identity/gsi/web/reference/js-reference#PromptMomentNotification>
496#[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/// result codes of getSkippedReason()
509///
510/// <https://developers.google.com/identity/gsi/web/reference/js-reference#PromptMomentNotification>
511#[derive(Debug, Copy, Clone, Eq, PartialEq)]
512pub enum SkippedReason {
513    AutoCancel,
514    UserCancel,
515    TapOutside,
516    IssuingFailed,
517}
518
519/// result codes of getDismissedReason()
520///
521/// <https://developers.google.com/identity/gsi/web/reference/js-reference#PromptMomentNotification>
522#[derive(Debug, Copy, Clone, Eq, PartialEq)]
523pub enum DismissedReason {
524    CredentialReturned,
525    CancelCalled,
526    FlowRestarted,
527}
528
529/// configure appearance of login button
530///
531/// <https://developers.google.com/identity/gsi/web/reference/js-reference#GsiButtonConfiguration>
532pub 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
548/// type of login button
549///
550/// <https://developers.google.com/identity/gsi/web/reference/js-reference#type>
551pub enum ButtonType {
552    Standard,
553    Icon,
554}
555
556/// theme of login button
557///
558/// <https://developers.google.com/identity/gsi/web/reference/js-reference#theme>
559pub enum ButtonTheme {
560    Outline,
561    FilledBlue,
562    FilledBlack,
563}
564
565/// Size of login button
566///
567/// <https://developers.google.com/identity/gsi/web/reference/js-reference#size>
568pub enum ButtonSize {
569    Large,
570    Medium,
571    Small,
572}
573
574/// render a new login button
575///
576/// <https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.renderButton>
577pub 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}