firebase-wasm 0.8.1

Rust bindings for the Firebase v9 JS SDK.
Documentation
mod user;

use crate::FirebaseError;
use std::{error::Error, fmt};
pub use user::*;
use wasm_bindgen::{prelude::*, JsCast};

#[derive(Clone, Debug, derive_more::Deref)]
pub struct AuthError {
    pub kind: AuthErrorKind,
    #[deref]
    pub source: FirebaseError,
}

impl fmt::Display for AuthError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.source.fmt(f)
    }
}

impl Error for AuthError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        Some(&self.source)
    }
}

impl From<FirebaseError> for AuthError {
    fn from(err: FirebaseError) -> Self {
        Self {
            kind: err.code().parse().unwrap(),
            source: err,
        }
    }
}

#[derive(Clone, Debug, strum_macros::EnumString)]
#[non_exhaustive]
pub enum AuthErrorKind {
    #[strum(serialize = "auth/app-deleted")]
    AppDeleted,
    #[strum(serialize = "auth/app-not-authorized")]
    AppNotAuthorized,
    #[strum(serialize = "auth/argument-error")]
    ArgumentError,
    #[strum(serialize = "auth/invalid-api-key")]
    InvalidApiKey,
    #[strum(serialize = "auth/invalid-user-token")]
    InvalidUserToken,
    #[strum(serialize = "auth/invalid-tenant-id")]
    InvalidTenantId,
    #[strum(serialize = "auth/network-request-failed")]
    NetworkRequestFailed,
    #[strum(serialize = "auth/operation-not-allowed")]
    OperationNotAllowed,
    #[strum(serialize = "auth/requires-recent-login")]
    RequiresRecentLogin,
    #[strum(serialize = "auth/too-many-requests")]
    TooManyRequests,
    #[strum(serialize = "auth/unauthorized-domain")]
    UnauthorizedDomain,
    #[strum(serialize = "auth/user-disabled")]
    UserDisabled,
    #[strum(serialize = "auth/user-token-expired")]
    UserTokenExpired,
    #[strum(serialize = "auth/web-storage-unsupported")]
    WebStorageUnsupported,
    #[strum(serialize = "auth/invalid-email")]
    InvalidEmail,
    #[strum(serialize = "auth/user-not-found")]
    UserNotFound,
    #[strum(serialize = "auth/wrong-password")]
    WrongPassword,
    #[strum(serialize = "auth/email-already-in-use")]
    EmailAlreadyInUse,
    #[strum(serialize = "auth/weak-password")]
    WeakPassword,
    #[strum(serialize = "auth/missing-android-pkg-name")]
    MissingAndroidPackageName,
    #[strum(serialize = "auth/missing-continue-uri")]
    MissingContinueUri,
    #[strum(serialize = "auth/missing-ios-bundle-id")]
    MissingIOSBundleId,
    #[strum(serialize = "auth/invalid-continue-uri")]
    InvalidContinueUri,
    #[strum(serialize = "auth/unauthorized-continue-uri")]
    UnauthorizedContinueUri,
    #[strum(serialize = "auth/expired-action-code")]
    ExpiredActionCode,
    #[strum(default)]
    Other(String),
}

#[derive(Debug, Clone, TypedBuilder, serde::Serialize)]
#[builder(field_defaults(default))]
pub struct ActionCodeSettings {
    pub android: Option<AndroidActionCodeSettings>,
    #[builder(setter(strip_option))]
    pub handle_code_in_app: Option<bool>,
    pub ios: Option<IOSActionCodeSettings>,
    #[builder(!default)]
    pub url: String,
    pub dynamic_link_domain: Option<String>,
}

#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, PartialEq, Eq, TypedBuilder, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AndroidActionCodeSettings {
    #[builder(default, setter(strip_option))]
    pub install_app: Option<bool>,
    #[builder(default)]
    pub minimum_version: Option<String>,
    pub package_name: String,
}

#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, PartialEq, Eq, TypedBuilder, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IOSActionCodeSettings {
    pub bundle_id: String,
}

pub async fn create_user_with_email_and_password(
    auth: Auth,
    email: String,
    password: String,
) -> Result<UserCredential, AuthError> {
    create_user_with_email_and_password_js(auth, email, password)
        .await
        .map(|cred| cred.unchecked_into::<UserCredential>())
        .map_err(|err| err.unchecked_into::<FirebaseError>().into())
}

pub async fn sign_in_with_email_and_password(
    auth: Auth,
    email: String,
    password: String,
) -> Result<UserCredential, AuthError> {
    sign_in_with_email_and_password_js(auth, email, password)
        .await
        .map(|cred| cred.unchecked_into::<UserCredential>())
        .map_err(|err| err.unchecked_into::<FirebaseError>().into())
}

pub async fn send_sign_in_link_to_email(
    auth: Auth,
    email: String,
    action_code_settings: ActionCodeSettings,
) -> Result<(), AuthError> {
    let action_code_settings = serde_wasm_bindgen::to_value(&action_code_settings).unwrap();

    send_sign_in_link_to_email_js(auth, email, action_code_settings)
        .await
        .map_err(|err| err.unchecked_into::<FirebaseError>().into())
}

pub async fn sign_in_with_email_link(
    auth: Auth,
    email: String,
    email_link: String,
) -> Result<UserCredential, AuthError> {
    sign_in_with_email_link_js(auth, email, email_link)
        .await
        .map(|u| u.unchecked_into::<UserCredential>())
        .map_err(|err| err.unchecked_into::<FirebaseError>().into())
}

pub async fn send_password_reset_email(
    auth: Auth,
    email: String,
    action_code_settings: Option<ActionCodeSettings>,
) -> Result<(), AuthError> {
    let action_code_settings = serde_wasm_bindgen::to_value(&action_code_settings).unwrap();

    send_password_reset_email_js(auth, email, action_code_settings)
        .await
        .map_err(|err| err.unchecked_into::<FirebaseError>().into())
}

pub async fn verify_password_reset_code(auth: Auth, code: String) -> Result<String, AuthError> {
    verify_password_reset_code_js(auth, code)
        .await
        .map(|res| res.unchecked_into::<js_sys::JsString>())
        .map(|s| ToString::to_string(&s))
        .map_err(|err| err.unchecked_into::<FirebaseError>().into())
}

pub async fn confirm_password_reset(
    auth: Auth,
    code: String,
    new_password: String,
) -> Result<(), JsValue> {
    confirm_password_reset_js(auth, code, new_password)
        .await
        .map_err(|err| err.unchecked_into::<FirebaseError>().into())
}

#[wasm_bindgen(module = "firebase/auth")]
extern "C" {
    #[derive(Clone, Debug)]
    pub type Auth;
    #[derive(Clone, Debug)]
    pub type UserCredential;

    #[wasm_bindgen(js_name = getAuth)]
    pub fn get_auth() -> Auth;

    #[wasm_bindgen(js_name = onAuthStateChanged)]
    pub fn on_auth_state_changed(auth: Auth, callback: &Closure<dyn FnMut(Option<User>)>);

    #[wasm_bindgen(js_name = createUserWithEmailAndPassword, catch)]
    async fn create_user_with_email_and_password_js(
        auth: Auth,
        email: String,
        password: String,
    ) -> Result<JsValue, JsValue>;

    #[wasm_bindgen(js_name = signInWithEmailAndPassword, catch)]
    async fn sign_in_with_email_and_password_js(
        auth: Auth,
        email: String,
        password: String,
    ) -> Result<JsValue, JsValue>;

    #[wasm_bindgen(js_name = signInWithEmailLink, catch)]
    async fn sign_in_with_email_link_js(
        auth: Auth,
        email: String,
        email_link: String,
    ) -> Result<JsValue, JsValue>;

    #[wasm_bindgen(js_name = isSignInWithEmailLink, )]
    pub fn is_sign_in_with_email_link(auth: Auth, email_link: &str) -> bool;

    #[wasm_bindgen(js_name = sendSignInLinkToEmail, catch)]
    async fn send_sign_in_link_to_email_js(
        auth: Auth,
        email: String,
        action_code_settings: JsValue,
    ) -> Result<(), JsValue>;

    #[wasm_bindgen(js_name = signOut)]
    pub async fn sign_out(auth: Auth);

    #[wasm_bindgen(js_name = sendPasswordResetEmail, catch)]
    async fn send_password_reset_email_js(
        auth: Auth,
        email: String,
        action_code_settings: JsValue,
    ) -> Result<(), JsValue>;

    #[wasm_bindgen(js_name = verifyPasswordResetCode, catch)]
    async fn verify_password_reset_code_js(auth: Auth, code: String) -> Result<JsValue, JsValue>;

    #[wasm_bindgen(js_name = confirmPasswordReset, catch)]
    async fn confirm_password_reset_js(
        auth: Auth,
        code: String,
        new_password: String,
    ) -> Result<(), JsValue>;

    // =======================================================
    //                  UserCredential
    // =======================================================

    #[wasm_bindgen(method, getter)]
    pub fn user(this: &UserCredential) -> user::User;

    #[wasm_bindgen(method, getter, js_name = providerId)]
    pub fn provider_id(this: &UserCredential) -> String;

    #[wasm_bindgen(method, getter, js_name = operationType)]
    pub fn operation_type(this: &UserCredential) -> String;
}