diff --git a/.changeset/ninety-pants-hunt.md b/.changeset/ninety-pants-hunt.md new file mode 100644 index 00000000..56b1043f --- /dev/null +++ b/.changeset/ninety-pants-hunt.md @@ -0,0 +1,5 @@ +--- +"@capacitor-firebase/authentication": patch +--- + +feat: password authentication diff --git a/packages/authentication/README.md b/packages/authentication/README.md index be8376bf..8e14e986 100644 --- a/packages/authentication/README.md +++ b/packages/authentication/README.md @@ -93,42 +93,100 @@ A working example can be found here: [robingenz/capacitor-firebase-authenticatio ```typescript import { FirebaseAuthentication } from '@capacitor-firebase/authentication'; +const applyActionCode = async () => { + await FirebaseAuthentication.applyActionCode({ oobCode: '1234' }); +}; + +const createUserWithEmailAndPassword = async () => { + const result = await FirebaseAuthentication.createUserWithEmailAndPassword({ + email: 'mail@exmaple.com', + password: '1234', + }); + return result.user; +}; + +const confirmPasswordReset = async () => { + await FirebaseAuthentication.confirmPasswordReset({ + oobCode: '1234', + newPassword: '4321', + }); +}; + const getCurrentUser = async () => { const result = await FirebaseAuthentication.getCurrentUser(); return result.user; }; const getIdToken = async () => { + const currentUser = getCurrentUser(); + if (!currentUser) { + return; + } const result = await FirebaseAuthentication.getIdToken(); return result.token; }; +const sendEmailVerification = async () => { + const currentUser = getCurrentUser(); + if (!currentUser) { + return; + } + await FirebaseAuthentication.sendEmailVerification(); +}; + +const sendPasswordResetEmail = async () => { + await FirebaseAuthentication.sendPasswordResetEmail({ + email: 'mail@example.com', + }); +}; + const setLanguageCode = async () => { await FirebaseAuthentication.setLanguageCode({ languageCode: 'en-US' }); }; const signInWithApple = async () => { - await FirebaseAuthentication.signInWithApple(); + const result = await FirebaseAuthentication.signInWithApple(); + return result.user; +}; + +const signInWithCustomToken = async () => { + const result = await FirebaseAuthentication.signInWithCustomToken({ + token: '1234', + }); + return result.user; +}; + +const signInWithEmailAndPassword = async () => { + const result = await FirebaseAuthentication.signInWithEmailAndPassword({ + email: 'mail@example.com', + password: '1234', + }); + return result.user; }; const signInWithFacebook = async () => { - await FirebaseAuthentication.signInWithFacebook(); + const result = await FirebaseAuthentication.signInWithFacebook(); + return result.user; }; const signInWithGithub = async () => { - await FirebaseAuthentication.signInWithGithub(); + const result = await FirebaseAuthentication.signInWithGithub(); + return result.user; }; const signInWithGoogle = async () => { - await FirebaseAuthentication.signInWithGoogle(); + const result = await FirebaseAuthentication.signInWithGoogle(); + return result.user; }; const signInWithMicrosoft = async () => { - await FirebaseAuthentication.signInWithMicrosoft(); + const result = await FirebaseAuthentication.signInWithMicrosoft(); + return result.user; }; const signInWithPlayGames = async () => { - await FirebaseAuthentication.signInWithPlayGames(); + const result = await FirebaseAuthentication.signInWithPlayGames(); + return result.user; }; const signInWithPhoneNumber = async () => { @@ -140,24 +198,47 @@ const signInWithPhoneNumber = async () => { const verificationCode = window.prompt( 'Please enter the verification code that was sent to your mobile device.', ); - await FirebaseAuthentication.signInWithPhoneNumber({ + const result = await FirebaseAuthentication.signInWithPhoneNumber({ verificationId, verificationCode, }); + return result.user; }; const signInWithTwitter = async () => { - await FirebaseAuthentication.signInWithTwitter(); + const result = await FirebaseAuthentication.signInWithTwitter(); + return result.user; }; const signInWithYahoo = async () => { - await FirebaseAuthentication.signInWithYahoo(); + const result = await FirebaseAuthentication.signInWithYahoo(); + return result.user; }; const signOut = async () => { await FirebaseAuthentication.signOut(); }; +const updateEmail = async () => { + const currentUser = getCurrentUser(); + if (!currentUser) { + return; + } + await FirebaseAuthentication.updateEmail({ + newEmail: 'new.mail@example.com', + }); +}; + +const updatePassword = async () => { + const currentUser = getCurrentUser(); + if (!currentUser) { + return; + } + await FirebaseAuthentication.updatePassword({ + newPassword: '4321', + }); +}; + const useAppLanguage = async () => { await FirebaseAuthentication.useAppLanguage(); }; @@ -174,20 +255,28 @@ const useEmulator = async () => { +* [`applyActionCode(...)`](#applyactioncode) +* [`createUserWithEmailAndPassword(...)`](#createuserwithemailandpassword) +* [`confirmPasswordReset(...)`](#confirmpasswordreset) * [`getCurrentUser()`](#getcurrentuser) * [`getIdToken(...)`](#getidtoken) +* [`sendEmailVerification()`](#sendemailverification) +* [`sendPasswordResetEmail(...)`](#sendpasswordresetemail) * [`setLanguageCode(...)`](#setlanguagecode) * [`signInWithApple(...)`](#signinwithapple) +* [`signInWithCustomToken(...)`](#signinwithcustomtoken) +* [`signInWithEmailAndPassword(...)`](#signinwithemailandpassword) * [`signInWithFacebook(...)`](#signinwithfacebook) * [`signInWithGithub(...)`](#signinwithgithub) * [`signInWithGoogle(...)`](#signinwithgoogle) * [`signInWithMicrosoft(...)`](#signinwithmicrosoft) +* [`signInWithPhoneNumber(...)`](#signinwithphonenumber) * [`signInWithPlayGames(...)`](#signinwithplaygames) * [`signInWithTwitter(...)`](#signinwithtwitter) * [`signInWithYahoo(...)`](#signinwithyahoo) -* [`signInWithPhoneNumber(...)`](#signinwithphonenumber) -* [`signInWithCustomToken(...)`](#signinwithcustomtoken) * [`signOut()`](#signout) +* [`updateEmail(...)`](#updateemail) +* [`updatePassword(...)`](#updatepassword) * [`useAppLanguage()`](#useapplanguage) * [`useEmulator(...)`](#useemulator) * [`addListener('authStateChange', ...)`](#addlistenerauthstatechange) @@ -200,6 +289,54 @@ const useEmulator = async () => { +### applyActionCode(...) + +```typescript +applyActionCode(options: ApplyActionCodeOptions) => Promise +``` + +Applies a verification code sent to the user by email. + +| Param | Type | +| ------------- | ------------------------------------------------------------------------- | +| **`options`** | ApplyActionCodeOptions | + +-------------------- + + +### createUserWithEmailAndPassword(...) + +```typescript +createUserWithEmailAndPassword(options: CreateUserWithEmailAndPasswordOptions) => Promise +``` + +Creates a new user account with email and password. +If the new account was created, the user is signed in automatically. + +| Param | Type | +| ------------- | ------------------------------------------------------------------------------------------------------- | +| **`options`** | CreateUserWithEmailAndPasswordOptions | + +**Returns:** Promise<SignInResult> + +-------------------- + + +### confirmPasswordReset(...) + +```typescript +confirmPasswordReset(options: ConfirmPasswordResetOptions) => Promise +``` + +Completes the password reset process + +| Param | Type | +| ------------- | ----------------------------------------------------------------------------------- | +| **`options`** | ConfirmPasswordResetOptions | + +-------------------- + + ### getCurrentUser() ```typescript @@ -230,6 +367,32 @@ Fetches the Firebase Auth ID Token for the currently signed-in user. -------------------- +### sendEmailVerification() + +```typescript +sendEmailVerification() => Promise +``` + +Sends a verification email to the currently signed in user. + +-------------------- + + +### sendPasswordResetEmail(...) + +```typescript +sendPasswordResetEmail(options: SendPasswordResetEmailOptions) => Promise +``` + +Sends a password reset email. + +| Param | Type | +| ------------- | --------------------------------------------------------------------------------------- | +| **`options`** | SendPasswordResetEmailOptions | + +-------------------- + + ### setLanguageCode(...) ```typescript @@ -262,6 +425,43 @@ Starts the Apple sign-in flow. -------------------- +### signInWithCustomToken(...) + +```typescript +signInWithCustomToken(options: SignInWithCustomTokenOptions) => Promise +``` + +Starts the Custom Token sign-in flow. + +This method cannot be used in combination with `skipNativeAuth` on Android and iOS. +In this case you have to use the `signInWithCustomToken` interface of the Firebase JS SDK directly. + +| Param | Type | +| ------------- | ------------------------------------------------------------------------------------- | +| **`options`** | SignInWithCustomTokenOptions | + +**Returns:** Promise<SignInResult> + +-------------------- + + +### signInWithEmailAndPassword(...) + +```typescript +signInWithEmailAndPassword(options: SignInWithEmailAndPasswordOptions) => Promise +``` + +Starts the sign-in flow using an email and password. + +| Param | Type | +| ------------- | ----------------------------------------------------------------------------------------------- | +| **`options`** | SignInWithEmailAndPasswordOptions | + +**Returns:** Promise<SignInResult> + +-------------------- + + ### signInWithFacebook(...) ```typescript @@ -330,6 +530,27 @@ Starts the Microsoft sign-in flow. -------------------- +### signInWithPhoneNumber(...) + +```typescript +signInWithPhoneNumber(options: SignInWithPhoneNumberOptions) => Promise +``` + +Starts the sign-in flow using a phone number. + +Either the phone number or the verification code and verification ID must be provided. + +Only available for Android and iOS. + +| Param | Type | +| ------------- | ------------------------------------------------------------------------------------- | +| **`options`** | SignInWithPhoneNumberOptions | + +**Returns:** Promise<SignInWithPhoneNumberResult> + +-------------------- + + ### signInWithPlayGames(...) ```typescript @@ -381,54 +602,43 @@ Starts the Yahoo sign-in flow. -------------------- -### signInWithPhoneNumber(...) +### signOut() ```typescript -signInWithPhoneNumber(options: SignInWithPhoneNumberOptions) => Promise +signOut() => Promise ``` -Starts the sign-in flow using a phone number. - -Either the phone number or the verification code and verification ID must be provided. - -Only available for Android and iOS. - -| Param | Type | -| ------------- | ------------------------------------------------------------------------------------- | -| **`options`** | SignInWithPhoneNumberOptions | - -**Returns:** Promise<SignInWithPhoneNumberResult> +Starts the sign-out flow. -------------------- -### signInWithCustomToken(...) +### updateEmail(...) ```typescript -signInWithCustomToken(options: SignInWithCustomTokenOptions) => Promise +updateEmail(options: UpdateEmailOptions) => Promise ``` -Starts the Custom Token sign-in flow. - -This method cannot be used in combination with `skipNativeAuth` on Android and iOS. -In this case you have to use the `signInWithCustomToken` interface of the Firebase JS SDK directly. - -| Param | Type | -| ------------- | ------------------------------------------------------------------------------------- | -| **`options`** | SignInWithCustomTokenOptions | +Updates the email address of the currently signed in user. -**Returns:** Promise<SignInResult> +| Param | Type | +| ------------- | ----------------------------------------------------------------- | +| **`options`** | UpdateEmailOptions | -------------------- -### signOut() +### updatePassword(...) ```typescript -signOut() => Promise +updatePassword(options: UpdatePasswordOptions) => Promise ``` -Starts the sign-out flow. +Updates the password of the currently signed in user. + +| Param | Type | +| ------------- | ----------------------------------------------------------------------- | +| **`options`** | UpdatePasswordOptions | -------------------- @@ -491,11 +701,19 @@ Remove all listeners for this plugin. ### Interfaces -#### GetCurrentUserResult +#### ApplyActionCodeOptions -| Prop | Type | Description | -| ---------- | --------------------------------------------- | --------------------------------------------------------- | -| **`user`** | User \| null | The currently signed-in user, or null if there isn't any. | +| Prop | Type | Description | +| ------------- | ------------------- | ------------------------------------- | +| **`oobCode`** | string | A verification code sent to the user. | + + +#### SignInResult + +| Prop | Type | Description | +| ---------------- | ----------------------------------------------------------------- | --------------------------------------------------------- | +| **`user`** | User \| null | The currently signed-in user, or null if there isn't any. | +| **`credential`** | AuthCredential \| null | Credentials returned by an auth provider. | #### User @@ -513,6 +731,40 @@ Remove all listeners for this plugin. | **`uid`** | string | +#### AuthCredential + +| Prop | Type | Description | +| ----------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| **`providerId`** | string | The authentication provider ID for the credential. Example: `google.com`. | +| **`accessToken`** | string | The OAuth access token associated with the credential if it belongs to an OAuth provider. | +| **`idToken`** | string | The OAuth ID token associated with the credential if it belongs to an OIDC provider. | +| **`secret`** | string | The OAuth access token secret associated with the credential if it belongs to an OAuth 1.0 provider. | +| **`nonce`** | string | The random string used to make sure that the ID token you get was granted specifically in response to your app's authentication request. | + + +#### CreateUserWithEmailAndPasswordOptions + +| Prop | Type | +| -------------- | ------------------- | +| **`email`** | string | +| **`password`** | string | + + +#### ConfirmPasswordResetOptions + +| Prop | Type | Description | +| ----------------- | ------------------- | ------------------------------------- | +| **`oobCode`** | string | A verification code sent to the user. | +| **`newPassword`** | string | The new password. | + + +#### GetCurrentUserResult + +| Prop | Type | Description | +| ---------- | --------------------------------------------- | --------------------------------------------------------- | +| **`user`** | User \| null | The currently signed-in user, or null if there isn't any. | + + #### GetIdTokenResult | Prop | Type | Description | @@ -527,6 +779,13 @@ Remove all listeners for this plugin. | **`forceRefresh`** | boolean | Force refresh regardless of token expiration. | +#### SendPasswordResetEmailOptions + +| Prop | Type | +| ----------- | ------------------- | +| **`email`** | string | + + #### SetLanguageCodeOptions | Prop | Type | Description | @@ -534,25 +793,6 @@ Remove all listeners for this plugin. | **`languageCode`** | string | BCP 47 language code. Example: `en-US`. | -#### SignInResult - -| Prop | Type | Description | -| ---------------- | ----------------------------------------------------------------- | --------------------------------------------------------- | -| **`user`** | User \| null | The currently signed-in user, or null if there isn't any. | -| **`credential`** | AuthCredential \| null | Credentials returned by an auth provider. | - - -#### AuthCredential - -| Prop | Type | Description | -| ----------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -| **`providerId`** | string | The authentication provider ID for the credential. Example: `google.com`. | -| **`accessToken`** | string | The OAuth access token associated with the credential if it belongs to an OAuth provider. | -| **`idToken`** | string | The OAuth ID token associated with the credential if it belongs to an OIDC provider. | -| **`secret`** | string | The OAuth access token secret associated with the credential if it belongs to an OAuth 1.0 provider. | -| **`nonce`** | string | The random string used to make sure that the ID token you get was granted specifically in response to your app's authentication request. | - - #### SignInOptions | Prop | Type | Description | @@ -568,6 +808,21 @@ Remove all listeners for this plugin. | **`value`** | string | The custom parameter value (e.g. `user@firstadd.onmicrosoft.com`). | +#### SignInWithCustomTokenOptions + +| Prop | Type | Description | +| ----------- | ------------------- | --------------------------------- | +| **`token`** | string | The custom token to sign in with. | + + +#### SignInWithEmailAndPasswordOptions + +| Prop | Type | Description | +| -------------- | ------------------- | ------------------------ | +| **`email`** | string | The users email address. | +| **`password`** | string | The users password. | + + #### SignInWithPhoneNumberResult | Prop | Type | Description | @@ -584,11 +839,18 @@ Remove all listeners for this plugin. | **`verificationCode`** | string | The verification code from the SMS message. The `verificationId` must also be provided. | -#### SignInWithCustomTokenOptions +#### UpdateEmailOptions -| Prop | Type | Description | -| ----------- | ------------------- | --------------------------------- | -| **`token`** | string | The custom token to sign in with. | +| Prop | Type | Description | +| -------------- | ------------------- | ---------------------- | +| **`newEmail`** | string | The new email address. | + + +#### UpdatePasswordOptions + +| Prop | Type | Description | +| ----------------- | ------------------- | ----------------- | +| **`newPassword`** | string | The new password. | #### UseEmulatorOptions diff --git a/packages/authentication/android/src/main/java/dev/robingenz/capacitorjs/plugins/firebase/authentication/FirebaseAuthentication.java b/packages/authentication/android/src/main/java/dev/robingenz/capacitorjs/plugins/firebase/authentication/FirebaseAuthentication.java index 4959983f..9ff6f66c 100644 --- a/packages/authentication/android/src/main/java/dev/robingenz/capacitorjs/plugins/firebase/authentication/FirebaseAuthentication.java +++ b/packages/authentication/android/src/main/java/dev/robingenz/capacitorjs/plugins/firebase/authentication/FirebaseAuthentication.java @@ -33,9 +33,6 @@ interface AuthStateChangeListener { @Nullable private AuthStateChangeListener authStateChangeListener; - public static final String ERROR_SIGN_IN_FAILED = "signIn failed."; - public static final String ERROR_CUSTOM_TOKEN_SKIP_NATIVE_AUTH = - "signInWithCustomToken cannot be used in combination with skipNativeAuth."; private FirebaseAuthenticationPlugin plugin; private FirebaseAuthenticationConfig config; private FirebaseAuth firebaseAuthInstance; @@ -70,6 +67,63 @@ public AuthStateChangeListener getAuthStateChangeListener() { return authStateChangeListener; } + public void applyActionCode(@NonNull String oobCode, @NonNull Runnable callback) { + firebaseAuthInstance + .applyActionCode(oobCode) + .addOnCompleteListener( + task -> { + callback.run(); + } + ); + } + + public void createUserWithEmailAndPassword(PluginCall call) { + boolean skipNativeAuth = this.config.getSkipNativeAuth(); + if (skipNativeAuth) { + call.reject(FirebaseAuthenticationPlugin.ERROR_EMAIL_SIGN_IN_SKIP_NATIVE_AUTH); + return; + } + + String email = call.getString("email"); + if (email == null) { + call.reject(FirebaseAuthenticationPlugin.ERROR_EMAIL_MISSING); + return; + } + String password = call.getString("password"); + if (password == null) { + call.reject(FirebaseAuthenticationPlugin.ERROR_PASSWORD_MISSING); + return; + } + + firebaseAuthInstance + .createUserWithEmailAndPassword(email, password) + .addOnCompleteListener( + plugin.getActivity(), + task -> { + if (task.isSuccessful()) { + Log.d(FirebaseAuthenticationPlugin.TAG, "createUserWithEmailAndPassword succeeded."); + FirebaseUser user = getCurrentUser(); + JSObject signInResult = FirebaseAuthenticationHelper.createSignInResult(user, null, null, null, null); + call.resolve(signInResult); + } else { + Log.e(FirebaseAuthenticationPlugin.TAG, "createUserWithEmailAndPassword failed.", task.getException()); + call.reject(FirebaseAuthenticationPlugin.ERROR_CREATE_USER_WITH_EMAIL_AND_PASSWORD_FAILED); + } + } + ); + } + + public void confirmPasswordReset(@NonNull String oobCode, @NonNull String newPassword, @NonNull Runnable callback) { + firebaseAuthInstance + .confirmPasswordReset(oobCode, newPassword) + .addOnCompleteListener( + task -> { + callback.run(); + } + ); + } + + @Nullable public FirebaseUser getCurrentUser() { return firebaseAuthInstance.getCurrentUser(); } @@ -78,20 +132,38 @@ public void getIdToken(Boolean forceRefresh, final GetIdTokenResultCallback resu FirebaseUser user = getCurrentUser(); Task tokenResultTask = user.getIdToken(forceRefresh); tokenResultTask.addOnCompleteListener( - new OnCompleteListener() { - public void onComplete(@NonNull Task task) { - if (task.isSuccessful()) { - String token = task.getResult().getToken(); - resultCallback.success(token); - } else { - String message = task.getException().getLocalizedMessage(); - resultCallback.error(message); - } + task -> { + if (task.isSuccessful()) { + String token = task.getResult().getToken(); + resultCallback.success(token); + } else { + String message = task.getException().getLocalizedMessage(); + resultCallback.error(message); } } ); } + public void sendEmailVerification(FirebaseUser user, @NonNull Runnable callback) { + user + .sendEmailVerification() + .addOnCompleteListener( + task -> { + callback.run(); + } + ); + } + + public void sendPasswordResetEmail(@NonNull String email, @NonNull Runnable callback) { + firebaseAuthInstance + .sendPasswordResetEmail(email) + .addOnCompleteListener( + task -> { + callback.run(); + } + ); + } + public void setLanguageCode(String languageCode) { firebaseAuthInstance.setLanguageCode(languageCode); } @@ -135,7 +207,7 @@ public void signInWithYahoo(PluginCall call) { public void signInWithCustomToken(PluginCall call) { boolean skipNativeAuth = this.config.getSkipNativeAuth(); if (skipNativeAuth) { - call.reject(ERROR_CUSTOM_TOKEN_SKIP_NATIVE_AUTH); + call.reject(FirebaseAuthenticationPlugin.ERROR_CUSTOM_TOKEN_SKIP_NATIVE_AUTH); return; } @@ -145,28 +217,43 @@ public void signInWithCustomToken(PluginCall call) { .signInWithCustomToken(token) .addOnCompleteListener( plugin.getActivity(), - new OnCompleteListener() { - @Override - public void onComplete(@NonNull Task task) { - if (task.isSuccessful()) { - Log.d(FirebaseAuthenticationPlugin.TAG, "signInWithCustomToken succeeded."); - FirebaseUser user = getCurrentUser(); - JSObject signInResult = FirebaseAuthenticationHelper.createSignInResult(user, null, null, null, null); - call.resolve(signInResult); - } else { - Log.e(FirebaseAuthenticationPlugin.TAG, "signInWithCustomToken failed.", task.getException()); - call.reject(ERROR_SIGN_IN_FAILED); - } + task -> { + if (task.isSuccessful()) { + Log.d(FirebaseAuthenticationPlugin.TAG, "signInWithCustomToken succeeded."); + FirebaseUser user = getCurrentUser(); + JSObject signInResult = FirebaseAuthenticationHelper.createSignInResult(user, null, null, null, null); + call.resolve(signInResult); + } else { + Log.e(FirebaseAuthenticationPlugin.TAG, "signInWithCustomToken failed.", task.getException()); + call.reject(FirebaseAuthenticationPlugin.ERROR_SIGN_IN_FAILED); } } - ) - .addOnFailureListener( + ); + } + + public void signInWithEmailAndPassword(PluginCall call) { + boolean skipNativeAuth = this.config.getSkipNativeAuth(); + if (skipNativeAuth) { + call.reject(FirebaseAuthenticationPlugin.ERROR_EMAIL_SIGN_IN_SKIP_NATIVE_AUTH); + return; + } + + String email = call.getString("email", ""); + String password = call.getString("password", ""); + + firebaseAuthInstance + .signInWithEmailAndPassword(email, password) + .addOnCompleteListener( plugin.getActivity(), - new OnFailureListener() { - @Override - public void onFailure(@NonNull Exception exception) { - Log.e(FirebaseAuthenticationPlugin.TAG, "signInWithCustomToken failed.", exception); - call.reject(ERROR_SIGN_IN_FAILED); + task -> { + if (task.isSuccessful()) { + Log.d(FirebaseAuthenticationPlugin.TAG, "signInWithEmailAndPassword succeeded."); + FirebaseUser user = getCurrentUser(); + JSObject signInResult = FirebaseAuthenticationHelper.createSignInResult(user, null, null, null, null); + call.resolve(signInResult); + } else { + Log.e(FirebaseAuthenticationPlugin.TAG, "signInWithEmailAndPassword failed.", task.getException()); + call.reject(FirebaseAuthenticationPlugin.ERROR_SIGN_IN_FAILED); } } ); @@ -186,6 +273,26 @@ public void signOut(PluginCall call) { call.resolve(); } + public void updateEmail(FirebaseUser user, @NonNull String newEmail, @NonNull Runnable callback) { + user + .updateEmail(newEmail) + .addOnCompleteListener( + task -> { + callback.run(); + } + ); + } + + public void updatePassword(FirebaseUser user, @NonNull String newPassword, @NonNull Runnable callback) { + user + .updatePassword(newPassword) + .addOnCompleteListener( + task -> { + callback.run(); + } + ); + } + public void useAppLanguage() { firebaseAuthInstance.useAppLanguage(); } @@ -233,34 +340,21 @@ public void handleSuccessfulSignIn( .signInWithCredential(credential) .addOnCompleteListener( plugin.getActivity(), - new OnCompleteListener() { - @Override - public void onComplete(@NonNull Task task) { - if (task.isSuccessful()) { - Log.d(FirebaseAuthenticationPlugin.TAG, "signInWithCredential succeeded."); - FirebaseUser user = getCurrentUser(); - JSObject signInResult = FirebaseAuthenticationHelper.createSignInResult( - user, - credential, - idToken, - nonce, - accessToken - ); - call.resolve(signInResult); - } else { - Log.e(FirebaseAuthenticationPlugin.TAG, "signInWithCredential failed.", task.getException()); - call.reject(ERROR_SIGN_IN_FAILED); - } - } - } - ) - .addOnFailureListener( - plugin.getActivity(), - new OnFailureListener() { - @Override - public void onFailure(@NonNull Exception exception) { - Log.e(FirebaseAuthenticationPlugin.TAG, "signInWithCredential failed.", exception); - call.reject(ERROR_SIGN_IN_FAILED); + task -> { + if (task.isSuccessful()) { + Log.d(FirebaseAuthenticationPlugin.TAG, "signInWithCredential succeeded."); + FirebaseUser user = getCurrentUser(); + JSObject signInResult = FirebaseAuthenticationHelper.createSignInResult( + user, + credential, + idToken, + nonce, + accessToken + ); + call.resolve(signInResult); + } else { + Log.e(FirebaseAuthenticationPlugin.TAG, "signInWithCredential failed.", task.getException()); + call.reject(FirebaseAuthenticationPlugin.ERROR_SIGN_IN_FAILED); } } ); diff --git a/packages/authentication/android/src/main/java/dev/robingenz/capacitorjs/plugins/firebase/authentication/FirebaseAuthenticationPlugin.java b/packages/authentication/android/src/main/java/dev/robingenz/capacitorjs/plugins/firebase/authentication/FirebaseAuthenticationPlugin.java index 487b31cc..8445226a 100644 --- a/packages/authentication/android/src/main/java/dev/robingenz/capacitorjs/plugins/firebase/authentication/FirebaseAuthenticationPlugin.java +++ b/packages/authentication/android/src/main/java/dev/robingenz/capacitorjs/plugins/firebase/authentication/FirebaseAuthenticationPlugin.java @@ -15,8 +15,20 @@ public class FirebaseAuthenticationPlugin extends Plugin { public static final String TAG = "FirebaseAuthentication"; + public static final String ERROR_NO_USER_SIGNED_IN = "No user is signed in."; + public static final String ERROR_OOB_CODE_MISSING = "oobCode must be provided."; + public static final String ERROR_EMAIL_MISSING = "email must be provided."; + public static final String ERROR_NEW_EMAIL_MISSING = "newEmail must be provided."; + public static final String ERROR_PASSWORD_MISSING = "password must be provided."; + public static final String ERROR_NEW_PASSWORD_MISSING = "newPassword must be provided."; public static final String ERROR_PHONE_NUMBER_SMS_CODE_MISSING = "phoneNumber or verificationId and verificationCode must be provided."; public static final String ERROR_HOST_MISSING = "host must be provided."; + public static final String ERROR_SIGN_IN_FAILED = "signIn failed."; + public static final String ERROR_CREATE_USER_WITH_EMAIL_AND_PASSWORD_FAILED = "createUserWithEmailAndPassword failed."; + public static final String ERROR_CUSTOM_TOKEN_SKIP_NATIVE_AUTH = + "signInWithCustomToken cannot be used in combination with skipNativeAuth."; + public static final String ERROR_EMAIL_SIGN_IN_SKIP_NATIVE_AUTH = + "createUserWithEmailAndPassword and signInWithEmailAndPassword cannot be used in combination with skipNativeAuth."; public static final String AUTH_STATE_CHANGE_EVENT = "authStateChange"; private FirebaseAuthenticationConfig config; private FirebaseAuthentication implementation; @@ -27,6 +39,36 @@ public void load() { implementation.setAuthStateChangeListener(this::updateAuthState); } + @PluginMethod + public void applyActionCode(PluginCall call) { + String oobCode = call.getString("oobCode"); + if (oobCode == null) { + call.reject(ERROR_OOB_CODE_MISSING); + return; + } + implementation.applyActionCode(oobCode, () -> call.resolve()); + } + + @PluginMethod + public void createUserWithEmailAndPassword(PluginCall call) { + implementation.createUserWithEmailAndPassword(call); + } + + @PluginMethod + public void confirmPasswordReset(PluginCall call) { + String oobCode = call.getString("oobCode"); + if (oobCode == null) { + call.reject(ERROR_OOB_CODE_MISSING); + return; + } + String newPassword = call.getString("newPassword"); + if (newPassword == null) { + call.reject(ERROR_NEW_PASSWORD_MISSING); + return; + } + implementation.confirmPasswordReset(oobCode, newPassword, () -> call.resolve()); + } + @PluginMethod public void getCurrentUser(PluginCall call) { FirebaseUser user = implementation.getCurrentUser(); @@ -58,6 +100,26 @@ public void error(String message) { ); } + @PluginMethod + public void sendEmailVerification(PluginCall call) { + FirebaseUser user = implementation.getCurrentUser(); + if (user == null) { + call.reject(ERROR_NO_USER_SIGNED_IN); + return; + } + implementation.sendEmailVerification(user, () -> call.resolve()); + } + + @PluginMethod + public void sendPasswordResetEmail(PluginCall call) { + String email = call.getString("email"); + if (email == null) { + call.reject(ERROR_EMAIL_MISSING); + return; + } + implementation.sendPasswordResetEmail(email, () -> call.resolve()); + } + @PluginMethod public void setLanguageCode(PluginCall call) { String languageCode = call.getString("languageCode", ""); @@ -71,6 +133,16 @@ public void signInWithApple(PluginCall call) { implementation.signInWithApple(call); } + @PluginMethod + public void signInWithCustomToken(PluginCall call) { + implementation.signInWithCustomToken(call); + } + + @PluginMethod + public void signInWithEmailAndPassword(PluginCall call) { + implementation.signInWithEmailAndPassword(call); + } + @PluginMethod public void signInWithFacebook(PluginCall call) { implementation.signInWithFacebook(call); @@ -121,13 +193,38 @@ public void signInWithYahoo(PluginCall call) { } @PluginMethod - public void signInWithCustomToken(PluginCall call) { - implementation.signInWithCustomToken(call); + public void signOut(PluginCall call) { + implementation.signOut(call); } @PluginMethod - public void signOut(PluginCall call) { - implementation.signOut(call); + public void updateEmail(PluginCall call) { + String newEmail = call.getString("newEmail"); + if (newEmail == null) { + call.reject(ERROR_NEW_EMAIL_MISSING); + return; + } + FirebaseUser user = implementation.getCurrentUser(); + if (user == null) { + call.reject(ERROR_NO_USER_SIGNED_IN); + return; + } + implementation.updateEmail(user, newEmail, () -> call.resolve()); + } + + @PluginMethod + public void updatePassword(PluginCall call) { + String newPassword = call.getString("newPassword"); + if (newPassword == null) { + call.reject(ERROR_NEW_PASSWORD_MISSING); + return; + } + FirebaseUser user = implementation.getCurrentUser(); + if (user == null) { + call.reject(ERROR_NO_USER_SIGNED_IN); + return; + } + implementation.updatePassword(user, newPassword, () -> call.resolve()); } @PluginMethod diff --git a/packages/authentication/ios/Plugin/FirebaseAuthentication.swift b/packages/authentication/ios/Plugin/FirebaseAuthentication.swift index 294ac31a..c32c7963 100644 --- a/packages/authentication/ios/Plugin/FirebaseAuthentication.swift +++ b/packages/authentication/ios/Plugin/FirebaseAuthentication.swift @@ -6,8 +6,6 @@ import FirebaseAuth public typealias AuthStateChangedObserver = () -> Void @objc public class FirebaseAuthentication: NSObject { - public let errorDeviceUnsupported = "Device is not supported. At least iOS 13 is required." - public let errorCustomTokenSkipNativeAuth = "signInWithCustomToken cannot be used in combination with skipNativeAuth." public var authStateObserver: AuthStateChangedObserver? private let plugin: FirebaseAuthenticationPlugin private let config: FirebaseAuthenticationConfig @@ -31,6 +29,48 @@ public typealias AuthStateChangedObserver = () -> Void } } + @objc func applyActionCode(oobCode: String, completion: @escaping (Error?) -> Void) { + return Auth.auth().applyActionCode(oobCode, completion: { error in + completion(error) + }) + } + + @objc func createUserWithEmailAndPassword(_ call: CAPPluginCall) { + if config.skipNativeAuth == true { + call.reject(plugin.errorEmailSignInSkipNativeAuth) + return + } + + guard let email = call.getString("email") else { + call.reject(plugin.errorEmailMissing) + return + } + guard let password = call.getString("password") else { + call.reject(plugin.errorPasswordMissing) + return + } + + self.savedCall = call + return Auth.auth().createUser(withEmail: email, password: password) { _, error in + if let error = error { + self.handleFailedSignIn(message: nil, error: error) + return + } + guard let savedCall = self.savedCall else { + return + } + let user = self.getCurrentUser() + let result = FirebaseAuthenticationHelper.createSignInResult(credential: nil, user: user, idToken: nil, nonce: nil, accessToken: nil) + savedCall.resolve(result) + } + } + + @objc func confirmPasswordReset(oobCode: String, newPassword: String, completion: @escaping (Error?) -> Void) { + return Auth.auth().confirmPasswordReset(withCode: oobCode, newPassword: newPassword, completion: { error in + completion(error) + }) + } + @objc func getCurrentUser() -> User? { return Auth.auth().currentUser } @@ -46,6 +86,18 @@ public typealias AuthStateChangedObserver = () -> Void }) } + @objc func sendEmailVerification(user: User, completion: @escaping (Error?) -> Void) { + user.sendEmailVerification(completion: { error in + completion(error) + }) + } + + @objc func sendPasswordResetEmail(email: String, completion: @escaping (Error?) -> Void) { + return Auth.auth().sendPasswordReset(withEmail: email) { error in + completion(error) + } + } + @objc func setLanguageCode(_ languageCode: String) { Auth.auth().languageCode = languageCode } @@ -55,6 +107,53 @@ public typealias AuthStateChangedObserver = () -> Void self.appleAuthProviderHandler?.signIn(call: call) } + @objc func signInWithCustomToken(_ call: CAPPluginCall) { + if config.skipNativeAuth == true { + call.reject(plugin.errorCustomTokenSkipNativeAuth) + return + } + + let token = call.getString("token", "") + + self.savedCall = call + Auth.auth().signIn(withCustomToken: token) { _, error in + if let error = error { + self.handleFailedSignIn(message: nil, error: error) + return + } + guard let savedCall = self.savedCall else { + return + } + let user = self.getCurrentUser() + let result = FirebaseAuthenticationHelper.createSignInResult(credential: nil, user: user, idToken: nil, nonce: nil, accessToken: nil) + savedCall.resolve(result) + } + } + + @objc func signInWithEmailAndPassword(_ call: CAPPluginCall) { + if config.skipNativeAuth == true { + call.reject(plugin.errorEmailSignInSkipNativeAuth) + return + } + + let email = call.getString("email", "") + let password = call.getString("password", "") + + self.savedCall = call + Auth.auth().signIn(withEmail: email, password: password) { _, error in + if let error = error { + self.handleFailedSignIn(message: nil, error: error) + return + } + guard let savedCall = self.savedCall else { + return + } + let user = self.getCurrentUser() + let result = FirebaseAuthenticationHelper.createSignInResult(credential: nil, user: user, idToken: nil, nonce: nil, accessToken: nil) + savedCall.resolve(result) + } + } + @objc func signInWithFacebook(_ call: CAPPluginCall) { self.savedCall = call self.facebookAuthProviderHandler?.signIn(call: call) @@ -90,29 +189,6 @@ public typealias AuthStateChangedObserver = () -> Void self.oAuthProviderHandler?.signIn(call: call, providerId: "yahoo.com") } - @objc func signInWithCustomToken(_ call: CAPPluginCall) { - if config.skipNativeAuth == true { - call.reject(self.errorCustomTokenSkipNativeAuth) - return - } - - let token = call.getString("token", "") - - self.savedCall = call - Auth.auth().signIn(withCustomToken: token) { _, error in - if let error = error { - self.handleFailedSignIn(message: nil, error: error) - return - } - guard let savedCall = self.savedCall else { - return - } - let user = self.getCurrentUser() - let result = FirebaseAuthenticationHelper.createSignInResult(credential: nil, user: user, idToken: nil, nonce: nil, accessToken: nil) - savedCall.resolve(result) - } - } - @objc func signOut(_ call: CAPPluginCall) { do { try Auth.auth().signOut() @@ -124,6 +200,18 @@ public typealias AuthStateChangedObserver = () -> Void } } + @objc func updateEmail(user: User, newEmail: String, completion: @escaping (Error?) -> Void) { + user.updateEmail(to: newEmail) { error in + completion(error) + } + } + + @objc func updatePassword(user: User, newPassword: String, completion: @escaping (Error?) -> Void) { + user.updatePassword(to: newPassword) { error in + completion(error) + } + } + @objc func useAppLanguage() { Auth.auth().useAppLanguage() } diff --git a/packages/authentication/ios/Plugin/FirebaseAuthenticationPlugin.m b/packages/authentication/ios/Plugin/FirebaseAuthenticationPlugin.m index d25a3699..f803ea52 100644 --- a/packages/authentication/ios/Plugin/FirebaseAuthenticationPlugin.m +++ b/packages/authentication/ios/Plugin/FirebaseAuthenticationPlugin.m @@ -4,10 +4,17 @@ // Define the plugin using the CAP_PLUGIN Macro, and // each method the plugin supports using the CAP_PLUGIN_METHOD macro. CAP_PLUGIN(FirebaseAuthenticationPlugin, "FirebaseAuthentication", + CAP_PLUGIN_METHOD(applyActionCode, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(createUserWithEmailAndPassword, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(confirmPasswordReset, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getCurrentUser, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getIdToken, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(sendEmailVerification, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(sendPasswordResetEmail, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(setLanguageCode, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(signInWithApple, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(signInWithCustomToken, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(signInWithEmailAndPassword, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(signInWithFacebook, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(signInWithGithub, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(signInWithGoogle, CAPPluginReturnPromise); @@ -16,8 +23,9 @@ CAP_PLUGIN_METHOD(signInWithTwitter, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(signInWithYahoo, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(signInWithPhoneNumber, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(signInWithCustomToken, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(signOut, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(updateEmail, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(updateEmail, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(useAppLanguage, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(useEmulator, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(removeAllListeners, CAPPluginReturnNone); diff --git a/packages/authentication/ios/Plugin/FirebaseAuthenticationPlugin.swift b/packages/authentication/ios/Plugin/FirebaseAuthenticationPlugin.swift index 70ffa2ee..51e5ed92 100644 --- a/packages/authentication/ios/Plugin/FirebaseAuthenticationPlugin.swift +++ b/packages/authentication/ios/Plugin/FirebaseAuthenticationPlugin.swift @@ -9,8 +9,21 @@ import FirebaseAuth */ @objc(FirebaseAuthenticationPlugin) public class FirebaseAuthenticationPlugin: CAPPlugin { + public let errorNoUserSignedIn = "No user is signed in." + public let errorOobCodeMissing = "oobCode must be provided." + public let errorEmailMissing = "email must be provided." + public let errorNewEmailMissing = "newEmail must be provided." + public let errorPasswordMissing = "password must be provided." + public let errorNewPasswordMissing = "newPassword must be provided." public let errorPhoneNumberVerificationIdCodeMissing = "phoneNumber or verificationId and verificationCode must be provided." public let errorHostMissing = "host must be provided." + public let errorSignInFailed = "signIn failed." + public let errorCreateUserWithEmailAndPasswordFailed = "createUserWithEmailAndPassword failed." + public let errorCustomTokenSkipNativeAuth = + "signInWithCustomToken cannot be used in combination with skipNativeAuth." + public let errorEmailSignInSkipNativeAuth = + "createUserWithEmailAndPassword and signInWithEmailAndPassword cannot be used in combination with skipNativeAuth." + public let errorDeviceUnsupported = "Device is not supported. At least iOS 13 is required." public let authStateChangeEvent = "authStateChange" private var implementation: FirebaseAuthentication? @@ -21,6 +34,44 @@ public class FirebaseAuthenticationPlugin: CAPPlugin { } } + @objc func applyActionCode(_ call: CAPPluginCall) { + guard let oobCode = call.getString("oobCode") else { + call.reject(errorOobCodeMissing) + return + } + + implementation?.applyActionCode(oobCode: oobCode, completion: { error in + if let error = error { + call.reject(error.localizedDescription) + return + } + call.resolve() + }) + } + + @objc func createUserWithEmailAndPassword(_ call: CAPPluginCall) { + implementation?.createUserWithEmailAndPassword(call) + } + + @objc func confirmPasswordReset(_ call: CAPPluginCall) { + guard let oobCode = call.getString("oobCode") else { + call.reject(errorOobCodeMissing) + return + } + guard let newPassword = call.getString("newPassword") else { + call.reject(errorNewPasswordMissing) + return + } + + implementation?.confirmPasswordReset(oobCode: oobCode, newPassword: newPassword, completion: { error in + if let error = error { + call.reject(error.localizedDescription) + return + } + call.resolve() + }) + } + @objc func getCurrentUser(_ call: CAPPluginCall) { let user = implementation?.getCurrentUser() let userResult = FirebaseAuthenticationHelper.createUserResult(user) @@ -43,6 +94,36 @@ public class FirebaseAuthenticationPlugin: CAPPlugin { }) } + @objc func sendEmailVerification(_ call: CAPPluginCall) { + guard let user = implementation?.getCurrentUser() else { + call.reject(errorNoUserSignedIn) + return + } + + implementation?.sendEmailVerification(user: user, completion: { error in + if let error = error { + call.reject(error.localizedDescription) + return + } + call.resolve() + }) + } + + @objc func sendPasswordResetEmail(_ call: CAPPluginCall) { + guard let email = call.getString("email") else { + call.reject(errorEmailMissing) + return + } + + implementation?.sendPasswordResetEmail(email: email, completion: { error in + if let error = error { + call.reject(error.localizedDescription) + return + } + call.resolve() + }) + } + @objc func setLanguageCode(_ call: CAPPluginCall) { let languageCode = call.getString("languageCode", "") @@ -54,6 +135,14 @@ public class FirebaseAuthenticationPlugin: CAPPlugin { implementation?.signInWithApple(call) } + @objc func signInWithCustomToken(_ call: CAPPluginCall) { + implementation?.signInWithCustomToken(call) + } + + @objc func signInWithEmailAndPassword(_ call: CAPPluginCall) { + implementation?.signInWithEmailAndPassword(call) + } + @objc func signInWithFacebook(_ call: CAPPluginCall) { implementation?.signInWithFacebook(call) } @@ -95,14 +184,48 @@ public class FirebaseAuthenticationPlugin: CAPPlugin { implementation?.signInWithYahoo(call) } - @objc func signInWithCustomToken(_ call: CAPPluginCall) { - implementation?.signInWithCustomToken(call) - } - @objc func signOut(_ call: CAPPluginCall) { implementation?.signOut(call) } + @objc func updateEmail(_ call: CAPPluginCall) { + guard let newEmail = call.getString("newEmail") else { + call.reject(errorNewEmailMissing) + return + } + guard let user = implementation?.getCurrentUser() else { + call.reject(errorNoUserSignedIn) + return + } + + implementation?.updateEmail(user: user, newEmail: newEmail, completion: { error in + if let error = error { + call.reject(error.localizedDescription) + return + } + call.resolve() + }) + } + + @objc func updatePassword(_ call: CAPPluginCall) { + guard let newPassword = call.getString("newPassword") else { + call.reject(errorNewPasswordMissing) + return + } + guard let user = implementation?.getCurrentUser() else { + call.reject(errorNoUserSignedIn) + return + } + + implementation?.updatePassword(user: user, newPassword: newPassword, completion: { error in + if let error = error { + call.reject(error.localizedDescription) + return + } + call.resolve() + }) + } + @objc func useAppLanguage(_ call: CAPPluginCall) { implementation?.useAppLanguage() call.resolve() diff --git a/packages/authentication/ios/Plugin/Handlers/AppleAuthProviderHandler.swift b/packages/authentication/ios/Plugin/Handlers/AppleAuthProviderHandler.swift index 332513a2..cd4f889d 100644 --- a/packages/authentication/ios/Plugin/Handlers/AppleAuthProviderHandler.swift +++ b/packages/authentication/ios/Plugin/Handlers/AppleAuthProviderHandler.swift @@ -17,7 +17,7 @@ class AppleAuthProviderHandler: NSObject { if #available(iOS 13, *) { self.startSignInWithAppleFlow() } else { - call.reject(self.pluginImplementation.errorDeviceUnsupported) + call.reject(self.pluginImplementation.getPlugin().errorDeviceUnsupported) } } } diff --git a/packages/authentication/src/definitions.ts b/packages/authentication/src/definitions.ts index 0712c5ad..75558099 100644 --- a/packages/authentication/src/definitions.ts +++ b/packages/authentication/src/definitions.ts @@ -34,6 +34,21 @@ declare module '@capacitor/cli' { } export interface FirebaseAuthenticationPlugin { + /** + * Applies a verification code sent to the user by email. + */ + applyActionCode(options: ApplyActionCodeOptions): Promise; + /** + * Creates a new user account with email and password. + * If the new account was created, the user is signed in automatically. + */ + createUserWithEmailAndPassword( + options: CreateUserWithEmailAndPasswordOptions, + ): Promise; + /** + * Completes the password reset process + */ + confirmPasswordReset(options: ConfirmPasswordResetOptions): Promise; /** * Fetches the currently signed-in user. */ @@ -42,6 +57,14 @@ export interface FirebaseAuthenticationPlugin { * Fetches the Firebase Auth ID Token for the currently signed-in user. */ getIdToken(options?: GetIdTokenOptions): Promise; + /** + * Sends a verification email to the currently signed in user. + */ + sendEmailVerification(): Promise; + /** + * Sends a password reset email. + */ + sendPasswordResetEmail(options: SendPasswordResetEmailOptions): Promise; /** * Sets the user-facing language code for auth operations. */ @@ -50,6 +73,21 @@ export interface FirebaseAuthenticationPlugin { * Starts the Apple sign-in flow. */ signInWithApple(options?: SignInOptions): Promise; + /** + * Starts the Custom Token sign-in flow. + * + * This method cannot be used in combination with `skipNativeAuth` on Android and iOS. + * In this case you have to use the `signInWithCustomToken` interface of the Firebase JS SDK directly. + */ + signInWithCustomToken( + options: SignInWithCustomTokenOptions, + ): Promise; + /** + * Starts the sign-in flow using an email and password. + */ + signInWithEmailAndPassword( + options: SignInWithEmailAndPasswordOptions, + ): Promise; /** * Starts the Facebook sign-in flow. */ @@ -66,6 +104,16 @@ export interface FirebaseAuthenticationPlugin { * Starts the Microsoft sign-in flow. */ signInWithMicrosoft(options?: SignInOptions): Promise; + /** + * Starts the sign-in flow using a phone number. + * + * Either the phone number or the verification code and verification ID must be provided. + * + * Only available for Android and iOS. + */ + signInWithPhoneNumber( + options: SignInWithPhoneNumberOptions, + ): Promise; /** * Starts the Play Games sign-in flow. */ @@ -79,28 +127,17 @@ export interface FirebaseAuthenticationPlugin { */ signInWithYahoo(options?: SignInOptions): Promise; /** - * Starts the sign-in flow using a phone number. - * - * Either the phone number or the verification code and verification ID must be provided. - * - * Only available for Android and iOS. + * Starts the sign-out flow. */ - signInWithPhoneNumber( - options: SignInWithPhoneNumberOptions, - ): Promise; + signOut(): Promise; /** - * Starts the Custom Token sign-in flow. - * - * This method cannot be used in combination with `skipNativeAuth` on Android and iOS. - * In this case you have to use the `signInWithCustomToken` interface of the Firebase JS SDK directly. + * Updates the email address of the currently signed in user. */ - signInWithCustomToken( - options: SignInWithCustomTokenOptions, - ): Promise; + updateEmail(options: UpdateEmailOptions): Promise; /** - * Starts the sign-out flow. + * Updates the password of the currently signed in user. */ - signOut(): Promise; + updatePassword(options: UpdatePasswordOptions): Promise; /** * Sets the user-facing language code to be the default app language. */ @@ -122,6 +159,29 @@ export interface FirebaseAuthenticationPlugin { removeAllListeners(): Promise; } +export interface ApplyActionCodeOptions { + /** + * A verification code sent to the user. + */ + oobCode: string; +} + +export interface ConfirmPasswordResetOptions { + /** + * A verification code sent to the user. + */ + oobCode: string; + /** + * The new password. + */ + newPassword: string; +} + +export interface CreateUserWithEmailAndPasswordOptions { + email: string; + password: string; +} + export interface GetCurrentUserResult { /** * The currently signed-in user, or null if there isn't any. @@ -143,6 +203,10 @@ export interface GetIdTokenResult { token: string; } +export interface SendPasswordResetEmailOptions { + email: string; +} + export interface SetLanguageCodeOptions { /** * BCP 47 language code. @@ -152,6 +216,20 @@ export interface SetLanguageCodeOptions { languageCode: string; } +export interface UpdateEmailOptions { + /** + * The new email address. + */ + newEmail: string; +} + +export interface UpdatePasswordOptions { + /** + * The new password. + */ + newPassword: string; +} + export interface SignInOptions { /** * Configures custom parameters to be passed to the identity provider during the OAuth sign-in flow. @@ -187,6 +265,17 @@ export interface SignInWithPhoneNumberOptions { verificationCode?: string; } +export interface SignInWithEmailAndPasswordOptions { + /** + * The users email address. + */ + email: string; + /** + * The users password. + */ + password: string; +} + export interface SignInWithCustomTokenOptions { /** * The custom token to sign in with. diff --git a/packages/authentication/src/web.ts b/packages/authentication/src/web.ts index 9ba7bce2..a02ade13 100644 --- a/packages/authentication/src/web.ts +++ b/packages/authentication/src/web.ts @@ -2,28 +2,44 @@ import { WebPlugin } from '@capacitor/core'; import type { AuthCredential as FirebaseAuthCredential, User as FirebaseUser, + UserCredential, } from 'firebase/auth'; import { + applyActionCode, + confirmPasswordReset, connectAuthEmulator, + createUserWithEmailAndPassword, FacebookAuthProvider, getAuth, GoogleAuthProvider, OAuthCredential, OAuthProvider, + sendEmailVerification, + sendPasswordResetEmail, signInWithCustomToken, + signInWithEmailAndPassword, signInWithPopup, + updateEmail, + updatePassword, } from 'firebase/auth'; import type { + ApplyActionCodeOptions, AuthCredential, AuthStateChange, + ConfirmPasswordResetOptions, + CreateUserWithEmailAndPasswordOptions, FirebaseAuthenticationPlugin, GetCurrentUserResult, GetIdTokenResult, + SendPasswordResetEmailOptions, SetLanguageCodeOptions, SignInResult, SignInWithCustomTokenOptions, + SignInWithEmailAndPasswordOptions, SignInWithPhoneNumberOptions, + UpdateEmailOptions, + UpdatePasswordOptions, UseEmulatorOptions, User, } from './definitions'; @@ -31,12 +47,38 @@ import type { export class FirebaseAuthenticationWeb extends WebPlugin implements FirebaseAuthenticationPlugin { + public static readonly ERROR_NO_USER_SIGNED_IN = 'No user is signed in.'; + constructor() { super(); const auth = getAuth(); auth.onAuthStateChanged(user => this.handleAuthStateChange(user)); } + public async applyActionCode(options: ApplyActionCodeOptions): Promise { + const auth = getAuth(); + return applyActionCode(auth, options.oobCode); + } + + public async createUserWithEmailAndPassword( + options: CreateUserWithEmailAndPasswordOptions, + ): Promise { + const auth = getAuth(); + const credential = await createUserWithEmailAndPassword( + auth, + options.email, + options.password, + ); + return this.createSignInResultFromUserCredential(credential); + } + + public async confirmPasswordReset( + options: ConfirmPasswordResetOptions, + ): Promise { + const auth = getAuth(); + return confirmPasswordReset(auth, options.oobCode, options.newPassword); + } + public async getCurrentUser(): Promise { const auth = getAuth(); const userResult = this.createUserResult(auth.currentUser); @@ -55,6 +97,22 @@ export class FirebaseAuthenticationWeb return result; } + public async sendEmailVerification(): Promise { + const auth = getAuth(); + const currentUser = auth.currentUser; + if (!currentUser) { + throw new Error(FirebaseAuthenticationWeb.ERROR_NO_USER_SIGNED_IN); + } + return sendEmailVerification(currentUser); + } + + public async sendPasswordResetEmail( + options: SendPasswordResetEmailOptions, + ): Promise { + const auth = getAuth(); + return sendPasswordResetEmail(auth, options.email); + } + public async setLanguageCode(options: SetLanguageCodeOptions): Promise { const auth = getAuth(); auth.languageCode = options.languageCode; @@ -65,7 +123,27 @@ export class FirebaseAuthenticationWeb const auth = getAuth(); const result = await signInWithPopup(auth, provider); const credential = OAuthProvider.credentialFromResult(result); - return this.createSignInResult(result.user, credential); + return this.createSignInResultFromAuthCredential(result.user, credential); + } + + public async signInWithCustomToken( + options: SignInWithCustomTokenOptions, + ): Promise { + const auth = getAuth(); + const result = await signInWithCustomToken(auth, options.token); + return this.createSignInResultFromAuthCredential(result.user, null); + } + + public async signInWithEmailAndPassword( + options: SignInWithEmailAndPasswordOptions, + ): Promise { + const auth = getAuth(); + const credential = await signInWithEmailAndPassword( + auth, + options.email, + options.password, + ); + return this.createSignInResultFromUserCredential(credential); } public async signInWithFacebook(): Promise { @@ -73,7 +151,7 @@ export class FirebaseAuthenticationWeb const auth = getAuth(); const result = await signInWithPopup(auth, provider); const credential = FacebookAuthProvider.credentialFromResult(result); - return this.createSignInResult(result.user, credential); + return this.createSignInResultFromAuthCredential(result.user, credential); } public async signInWithGithub(): Promise { @@ -81,7 +159,7 @@ export class FirebaseAuthenticationWeb const auth = getAuth(); const result = await signInWithPopup(auth, provider); const credential = OAuthProvider.credentialFromResult(result); - return this.createSignInResult(result.user, credential); + return this.createSignInResultFromAuthCredential(result.user, credential); } public async signInWithGoogle(): Promise { @@ -89,7 +167,7 @@ export class FirebaseAuthenticationWeb const auth = getAuth(); const result = await signInWithPopup(auth, provider); const credential = GoogleAuthProvider.credentialFromResult(result); - return this.createSignInResult(result.user, credential); + return this.createSignInResultFromAuthCredential(result.user, credential); } public async signInWithMicrosoft(): Promise { @@ -97,7 +175,13 @@ export class FirebaseAuthenticationWeb const auth = getAuth(); const result = await signInWithPopup(auth, provider); const credential = OAuthProvider.credentialFromResult(result); - return this.createSignInResult(result.user, credential); + return this.createSignInResultFromAuthCredential(result.user, credential); + } + + public async signInWithPhoneNumber( + _options: SignInWithPhoneNumberOptions, + ): Promise { + throw new Error('Not implemented on web.'); } public async signInWithPlayGames(): Promise { @@ -109,7 +193,7 @@ export class FirebaseAuthenticationWeb const auth = getAuth(); const result = await signInWithPopup(auth, provider); const credential = OAuthProvider.credentialFromResult(result); - return this.createSignInResult(result.user, credential); + return this.createSignInResultFromAuthCredential(result.user, credential); } public async signInWithYahoo(): Promise { @@ -117,26 +201,30 @@ export class FirebaseAuthenticationWeb const auth = getAuth(); const result = await signInWithPopup(auth, provider); const credential = OAuthProvider.credentialFromResult(result); - return this.createSignInResult(result.user, credential); + return this.createSignInResultFromAuthCredential(result.user, credential); } - public async signInWithPhoneNumber( - _options: SignInWithPhoneNumberOptions, - ): Promise { - throw new Error('Not implemented on web.'); + public async signOut(): Promise { + const auth = getAuth(); + await auth.signOut(); } - public async signInWithCustomToken( - options: SignInWithCustomTokenOptions, - ): Promise { + public async updateEmail(options: UpdateEmailOptions): Promise { const auth = getAuth(); - const result = await signInWithCustomToken(auth, options.token); - return this.createSignInResult(result.user, null); + const currentUser = auth.currentUser; + if (!currentUser) { + throw new Error(FirebaseAuthenticationWeb.ERROR_NO_USER_SIGNED_IN); + } + return updateEmail(currentUser, options.newEmail); } - public async signOut(): Promise { + public async updatePassword(options: UpdatePasswordOptions): Promise { const auth = getAuth(); - await auth.signOut(); + const currentUser = auth.currentUser; + if (!currentUser) { + throw new Error(FirebaseAuthenticationWeb.ERROR_NO_USER_SIGNED_IN); + } + return updatePassword(currentUser, options.newPassword); } public async useAppLanguage(): Promise { @@ -158,7 +246,7 @@ export class FirebaseAuthenticationWeb this.notifyListeners('authStateChange', change); } - private createSignInResult( + private createSignInResultFromAuthCredential( user: FirebaseUser, credential: FirebaseAuthCredential | null, ): SignInResult { @@ -171,20 +259,13 @@ export class FirebaseAuthenticationWeb return result; } - private createUserResult(user: FirebaseUser | null): User | null { - if (!user) { - return null; - } - const result: User = { - displayName: user.displayName, - email: user.email, - emailVerified: user.emailVerified, - isAnonymous: user.isAnonymous, - phoneNumber: user.phoneNumber, - photoUrl: user.photoURL, - providerId: user.providerId, - tenantId: user.tenantId, - uid: user.uid, + private createSignInResultFromUserCredential( + credential: UserCredential, + ): SignInResult { + const userResult = this.createUserResult(credential.user); + const result: SignInResult = { + user: userResult, + credential: null, }; return result; } @@ -205,4 +286,22 @@ export class FirebaseAuthenticationWeb } return result; } + + private createUserResult(user: FirebaseUser | null): User | null { + if (!user) { + return null; + } + const result: User = { + displayName: user.displayName, + email: user.email, + emailVerified: user.emailVerified, + isAnonymous: user.isAnonymous, + phoneNumber: user.phoneNumber, + photoUrl: user.photoURL, + providerId: user.providerId, + tenantId: user.tenantId, + uid: user.uid, + }; + return result; + } }