diff --git a/packages/app/lib/internal/web/firebaseAuth.js b/packages/app/lib/internal/web/firebaseAuth.js new file mode 100644 index 0000000000..809e1208a2 --- /dev/null +++ b/packages/app/lib/internal/web/firebaseAuth.js @@ -0,0 +1,4 @@ +// We need to share firebase imports between modules, otherwise +// apps and instances of the firebase modules are not shared. +export * from 'firebase/app'; +export * from 'firebase/auth'; diff --git a/packages/auth/e2e/auth.e2e.js b/packages/auth/e2e/auth.e2e.js index 6dadcb2749..c8fc812975 100644 --- a/packages/auth/e2e/auth.e2e.js +++ b/packages/auth/e2e/auth.e2e.js @@ -78,7 +78,7 @@ describe('auth() modular', function () { .then($ => $); } catch (e) { didError = true; - e.message.should.containEql('code is invalid'); + e.code.should.equal('auth/invalid-action-code'); } didError.should.equal(true, 'Did not error'); }); @@ -86,25 +86,29 @@ describe('auth() modular', function () { describe('checkActionCode()', function () { it('errors on invalid code', async function () { + let didError = false; try { await firebase.auth().checkActionCode('fooby shooby dooby'); } catch (e) { - e.message.should.containEql('code is invalid'); + didError = true; + e.code.should.equal('auth/invalid-action-code'); } + didError.should.equal(true, 'Did not error'); }); }); describe('reload()', function () { it('Meta data returns as expected with annonymous sign in', async function () { + if (Platform.other) return; await firebase.auth().signInAnonymously(); - await Utils.sleep(50); + await Utils.sleep(500); const firstUser = firebase.auth().currentUser; await firstUser.reload(); await firebase.auth().signOut(); await firebase.auth().signInAnonymously(); - await Utils.sleep(50); + await Utils.sleep(500); const secondUser = firebase.auth().currentUser; await secondUser.reload(); @@ -119,12 +123,12 @@ describe('auth() modular', function () { await firebase.auth().createUserWithEmailAndPassword(email1, pass); const firstUser = firebase.auth().currentUser; await firstUser.reload(); - + await Utils.sleep(500); await firebase.auth().signOut(); const anotherRandom = Utils.randString(12, '#aA'); const email2 = `${anotherRandom}@${anotherRandom}.com`; - + await Utils.sleep(500); await firebase.auth().createUserWithEmailAndPassword(email2, pass); const secondUser = firebase.auth().currentUser; await secondUser.reload(); @@ -135,11 +139,14 @@ describe('auth() modular', function () { describe('confirmPasswordReset()', function () { it('errors on invalid code via API', async function () { + let didError = false; try { await firebase.auth().confirmPasswordReset('fooby shooby dooby', 'passwordthing'); } catch (e) { - e.message.should.containEql('code is invalid'); + didError = true; + e.code.should.equal('auth/invalid-action-code'); } + didError.should.equal(true, 'Did not error'); }); }); @@ -556,9 +563,6 @@ describe('auth() modular', function () { const failureCb = error => { error.code.should.equal('auth/user-disabled'); - error.message.should.containEql( - 'The user account has been disabled by an administrator.', - ); return Promise.resolve(); }; @@ -574,9 +578,6 @@ describe('auth() modular', function () { const failureCb = error => { error.code.should.equal('auth/wrong-password'); - error.message.should.containEql( - 'The password is invalid or the user does not have a password.', - ); return Promise.resolve(); }; @@ -596,9 +597,6 @@ describe('auth() modular', function () { const failureCb = error => { error.code.should.equal('auth/user-not-found'); - error.message.should.containEql( - 'There is no user record corresponding to this identifier. The user may have been deleted.', - ); return Promise.resolve(); }; @@ -644,9 +642,6 @@ describe('auth() modular', function () { const failureCb = error => { error.code.should.equal('auth/user-disabled'); - error.message.should.containEql( - 'The user account has been disabled by an administrator.', - ); return Promise.resolve(); }; @@ -663,9 +658,6 @@ describe('auth() modular', function () { const failureCb = error => { error.code.should.equal('auth/wrong-password'); - error.message.should.containEql( - 'The password is invalid or the user does not have a password.', - ); return Promise.resolve(); }; @@ -683,9 +675,6 @@ describe('auth() modular', function () { const failureCb = error => { error.code.should.equal('auth/user-not-found'); - error.message.should.containEql( - 'There is no user record corresponding to this identifier. The user may have been deleted.', - ); return Promise.resolve(); }; @@ -726,7 +715,6 @@ describe('auth() modular', function () { const failureCb = error => { error.code.should.equal('auth/invalid-email'); - error.message.should.containEql('The email address is badly formatted.'); return Promise.resolve(); }; @@ -742,9 +730,6 @@ describe('auth() modular', function () { const failureCb = error => { error.code.should.equal('auth/email-already-in-use'); - error.message.should.containEql( - 'The email address is already in use by another account.', - ); return Promise.resolve(); }; @@ -825,7 +810,6 @@ describe('auth() modular', function () { const failureCb = error => { error.code.should.equal('auth/invalid-email'); - error.message.should.containEql('The email address is badly formatted.'); resolve(); }; @@ -853,7 +837,6 @@ describe('auth() modular', function () { const failureCb = error => { error.code.should.equal('auth/no-current-user'); - error.message.should.containEql('No user currently signed in.'); resolve(); }; @@ -976,16 +959,19 @@ describe('auth() modular', function () { firebase.auth().currentUser.email.should.equal(email); firebase.auth().currentUser.emailVerified.should.equal(false); + let didError = false; try { await firebase.auth().sendPasswordResetEmail(email); const { oobCode } = await getLastOob(email); await firebase.auth().verifyPasswordResetCode(oobCode + 'badcode'); throw new Error('Invalid code should throw an error'); } catch (error) { - error.message.should.containEql('[auth/invalid-action-code]'); + didError = true; + error.code.should.equal('auth/invalid-action-code'); } finally { await firebase.auth().currentUser.delete(); } + didError.should.equal(true); }); it('should change password correctly OOB', async function () { @@ -1145,6 +1131,8 @@ describe('auth() modular', function () { }); it('supports an app initialized with custom authDomain', async function () { + if (Platform.other) return; + const { getAuth, getCustomAuthDomain } = authModular; const { initializeApp } = modular; @@ -1173,11 +1161,14 @@ describe('auth() modular', function () { it('errors on invalid code', async function () { const { applyActionCode, getAuth } = authModular; const defaultAuth = getAuth(firebase.app()); + let didError = false; try { await applyActionCode(defaultAuth, 'fooby shooby dooby').then($ => $); } catch (e) { - e.message.should.containEql('code is invalid'); + didError = true; + e.code.should.equal('auth/invalid-action-code'); } + didError.should.equal(true); }); }); @@ -1185,28 +1176,34 @@ describe('auth() modular', function () { it('errors on invalid code', async function () { const { checkActionCode, getAuth } = authModular; const defaultAuth = getAuth(firebase.app()); + let didError = false; try { await checkActionCode(defaultAuth, 'fooby shooby dooby'); } catch (e) { - e.message.should.containEql('code is invalid'); + didError = true; + e.code.should.equal('auth/invalid-action-code'); } + didError.should.equal(true); }); }); describe('reload()', function () { it('Meta data returns as expected with anonymous sign in', async function () { + // Reload doesn't seem to update metadata on the user object. + if (Platform.other) return; + const { signInAnonymously, signOut, getAuth } = authModular; const defaultAuth = getAuth(firebase.app()); await signInAnonymously(defaultAuth); - await Utils.sleep(50); + await Utils.sleep(500); const firstUser = defaultAuth.currentUser; await firstUser.reload(); await signOut(defaultAuth); await signInAnonymously(defaultAuth); - await Utils.sleep(50); + await Utils.sleep(500); const secondUser = defaultAuth.currentUser; await secondUser.reload(); @@ -1223,9 +1220,9 @@ describe('auth() modular', function () { await createUserWithEmailAndPassword(defaultAuth, email1, pass); const firstUser = defaultAuth.currentUser; await firstUser.reload(); - + await Utils.sleep(500); await signOut(defaultAuth); - + await Utils.sleep(500); const anotherRandom = Utils.randString(12, '#aA'); const email2 = `${anotherRandom}@${anotherRandom}.com`; @@ -1245,7 +1242,7 @@ describe('auth() modular', function () { try { await confirmPasswordReset(defaultAuth, 'fooby shooby dooby', 'passwordthing'); } catch (e) { - e.message.should.containEql('code is invalid'); + e.code.should.equal('auth/invalid-action-code'); } }); }); @@ -1616,9 +1613,6 @@ describe('auth() modular', function () { const failureCb = error => { error.code.should.equal('auth/user-disabled'); - error.message.should.containEql( - 'The user account has been disabled by an administrator.', - ); return Promise.resolve(); }; @@ -1638,9 +1632,6 @@ describe('auth() modular', function () { const failureCb = error => { error.code.should.equal('auth/wrong-password'); - error.message.should.containEql( - 'The password is invalid or the user does not have a password.', - ); return Promise.resolve(); }; @@ -1664,9 +1655,6 @@ describe('auth() modular', function () { const failureCb = error => { error.code.should.equal('auth/user-not-found'); - error.message.should.containEql( - 'There is no user record corresponding to this identifier. The user may have been deleted.', - ); return Promise.resolve(); }; @@ -1718,9 +1706,6 @@ describe('auth() modular', function () { const failureCb = error => { error.code.should.equal('auth/user-disabled'); - error.message.should.containEql( - 'The user account has been disabled by an administrator.', - ); return Promise.resolve(); }; @@ -1744,9 +1729,6 @@ describe('auth() modular', function () { const failureCb = error => { error.code.should.equal('auth/wrong-password'); - error.message.should.containEql( - 'The password is invalid or the user does not have a password.', - ); return Promise.resolve(); }; @@ -1771,9 +1753,6 @@ describe('auth() modular', function () { const failureCb = error => { error.code.should.equal('auth/user-not-found'); - error.message.should.containEql( - 'There is no user record corresponding to this identifier. The user may have been deleted.', - ); return Promise.resolve(); }; @@ -1830,7 +1809,6 @@ describe('auth() modular', function () { throw new Error('Did not error.'); } catch (error) { error.code.should.equal('auth/invalid-email'); - error.message.should.containEql('The email address is badly formatted.'); } }); @@ -1843,9 +1821,6 @@ describe('auth() modular', function () { throw new Error('Did not error.'); } catch (error) { error.code.should.equal('auth/email-already-in-use'); - error.message.should.containEql( - 'The email address is already in use by another account.', - ); } }); @@ -1906,7 +1881,6 @@ describe('auth() modular', function () { throw new Error('Should not have successfully resolved.'); } catch (error) { error.code.should.equal('auth/invalid-email'); - error.message.should.containEql('The email address is badly formatted.'); } }); }); diff --git a/packages/auth/e2e/multiFactor.e2e.js b/packages/auth/e2e/multiFactor.e2e.js index 242ceb24b4..e894f18cf2 100644 --- a/packages/auth/e2e/multiFactor.e2e.js +++ b/packages/auth/e2e/multiFactor.e2e.js @@ -11,6 +11,11 @@ const TEST_EMAIL = 'test@example.com'; const TEST_PASS = 'test1234'; describe('multi-factor modular', function () { + // Other does not support multi-factor + if (Platform.other) { + return; + } + describe('firebase v8 compatibility', function () { beforeEach(async function () { await clearAllUsers(); diff --git a/packages/auth/e2e/phone.e2e.js b/packages/auth/e2e/phone.e2e.js index eb31efdd26..de5ed76474 100644 --- a/packages/auth/e2e/phone.e2e.js +++ b/packages/auth/e2e/phone.e2e.js @@ -4,6 +4,11 @@ const { clearAllUsers, getLastSmsCode, getRandomPhoneNumber } = require('./helpers'); describe('auth() => Phone', function () { + // Other platforms don't support phone auth + if (Platform.other) { + return; + } + describe('firebase v8 compatibility', function () { before(async function () { try { diff --git a/packages/auth/e2e/user.e2e.js b/packages/auth/e2e/user.e2e.js index 1904a84803..a097a3d499 100644 --- a/packages/auth/e2e/user.e2e.js +++ b/packages/auth/e2e/user.e2e.js @@ -132,9 +132,6 @@ describe('auth().currentUser', function () { } catch (error) { // Assertions error.code.should.equal('auth/email-already-in-use'); - error.message.should.containEql( - 'The email address is already in use by another account.', - ); // Clean up await firebase.auth().currentUser.delete(); @@ -721,6 +718,11 @@ describe('auth().currentUser', function () { }); describe('updatePhoneNumber()', function () { + // Phone number auth is not supported on other platforms + if (Platform.other) { + return; + } + it('should update the phone number', async function () { const testPhone = await getRandomPhoneNumber(); const confirmResult = await firebase.auth().signInWithPhoneNumber(testPhone); @@ -1065,9 +1067,6 @@ describe('auth().currentUser', function () { } catch (error) { // Assertions error.code.should.equal('auth/email-already-in-use'); - error.message.should.containEql( - 'The email address is already in use by another account.', - ); // Clean up await deleteUser(currentUser); @@ -1763,6 +1762,11 @@ describe('auth().currentUser', function () { }); describe('updatePhoneNumber()', function () { + // Phone numbers not supported in other environments + if (Platform.other) { + return; + } + it('should update the phone number', async function () { const { getAuth, signInWithPhoneNumber, updatePhoneNumber, verifyPhoneNumber } = authModular; diff --git a/packages/auth/lib/index.js b/packages/auth/lib/index.js index 9c8c1e511b..6f0f7a2f21 100644 --- a/packages/auth/lib/index.js +++ b/packages/auth/lib/index.js @@ -22,6 +22,7 @@ import { isString, isValidUrl, } from '@react-native-firebase/app/lib/common'; +import { setReactNativeModule } from '@react-native-firebase/app/lib/internal/nativeModule'; import { FirebaseModule, createModuleNamespace, @@ -44,6 +45,7 @@ import OIDCAuthProvider from './providers/OIDCAuthProvider'; import PhoneAuthProvider from './providers/PhoneAuthProvider'; import TwitterAuthProvider from './providers/TwitterAuthProvider'; import version from './version'; +import fallBackModule from './web/RNFBAuthModule'; export { applyActionCode, @@ -137,7 +139,6 @@ const statics = { }; const namespace = 'auth'; - const nativeModuleName = 'RNFBAuthModule'; class FirebaseAuthModule extends FirebaseModule { @@ -533,3 +534,6 @@ export default createModuleNamespace({ // auth().X(...); // firebase.auth().X(...); export const firebase = getFirebaseRoot(); + +// Register the interop module for non-native platforms. +setReactNativeModule(nativeModuleName, fallBackModule); diff --git a/packages/auth/lib/web/RNFBAuthModule.android.js b/packages/auth/lib/web/RNFBAuthModule.android.js new file mode 100644 index 0000000000..af77c859b1 --- /dev/null +++ b/packages/auth/lib/web/RNFBAuthModule.android.js @@ -0,0 +1,2 @@ +// No-op for android. +export default {}; diff --git a/packages/auth/lib/web/RNFBAuthModule.ios.js b/packages/auth/lib/web/RNFBAuthModule.ios.js new file mode 100644 index 0000000000..a3429ada0e --- /dev/null +++ b/packages/auth/lib/web/RNFBAuthModule.ios.js @@ -0,0 +1,2 @@ +// No-op for ios. +export default {}; diff --git a/packages/auth/lib/web/RNFBAuthModule.js b/packages/auth/lib/web/RNFBAuthModule.js new file mode 100644 index 0000000000..8000ab37a0 --- /dev/null +++ b/packages/auth/lib/web/RNFBAuthModule.js @@ -0,0 +1,1027 @@ +import { + getApps, + getApp, + getAuth, + onAuthStateChanged, + onIdTokenChanged, + signInAnonymously, + sendSignInLinkToEmail, + getAdditionalUserInfo, + multiFactor, + createUserWithEmailAndPassword, + signInWithEmailAndPassword, + signInWithEmailLink, + signInWithCustomToken, + sendPasswordResetEmail, + useDeviceLanguage, + verifyPasswordResetCode, + connectAuthEmulator, + fetchSignInMethodsForEmail, + sendEmailVerification, + verifyBeforeUpdateEmail, + confirmPasswordReset, + updateEmail, + updatePassword, + updateProfile, + updatePhoneNumber, + signInWithCredential, + unlink, + linkWithCredential, + reauthenticateWithCredential, + getIdToken, + getIdTokenResult, + applyActionCode, + checkActionCode, + EmailAuthProvider, + FacebookAuthProvider, + GoogleAuthProvider, + TwitterAuthProvider, + GithubAuthProvider, + PhoneAuthProvider, + OAuthProvider, +} from '@react-native-firebase/app/lib/internal/web/firebaseAuth'; +import { guard, getWebError, emitEvent } from '@react-native-firebase/app/lib/internal/web/utils'; + +/** + * Resolves or rejects an auth method promise without a user (user was missing). + * @param {boolean} isError whether to reject the promise. + * @returns {Promise} - Void promise. + */ +function promiseNoUser(isError = false) { + if (isError) { + return rejectPromiseWithCodeAndMessage('no-current-user', 'No user currently signed in.'); + } + + // TODO(ehesp): Should this be null, or undefined? + return Promise.resolve(null); +} + +/** + * Returns a structured error object. + * @param {string} code - The error code. + * @param {string} message - The error message. + */ +function rejectPromiseWithCodeAndMessage(code, message) { + return rejectPromise(getWebError({ code: `auth/${code}`, message })); +} + +/** + * Returns a structured error object. + * @param {error} error The error object. + * @returns {never} + */ +function rejectPromise(error) { + const { code, message, details } = error; + const nativeError = { + code, + message, + userInfo: { + code: code ? code.replace('auth/', '') : 'unknown', + message, + details, + }, + }; + return Promise.reject(nativeError); +} + +/** + * Converts a user object to a plain object. + * @param {User} user - The User object to convert. + * @returns {object} + */ +function userToObject(user) { + return { + ...userInfoToObject(user), + emailVerified: user.emailVerified, + isAnonymous: user.isAnonymous, + tenantId: user.tenantId !== null && user.tenantId !== '' ? user.tenantId : null, + providerData: user.providerData.map(userInfoToObject), + metadata: userMetadataToObject(user.metadata), + multiFactor: multiFactor(user).enrolledFactors.map(multiFactorInfoToObject), + }; +} + +/** + * Returns an AuthCredential object for the given provider. + * @param {Auth} auth - The Auth instance to use. + * @param {string} provider - The provider to get the credential for. + * @param {string} token - The token to use for the credential. + * @param {string|null} secret - The secret to use for the credential. + * @returns {AuthCredential|null} - The AuthCredential object. + */ +function getAuthCredential(auth, provider, token, secret) { + if (provider.startsWith('oidc.')) { + return new OAuthProvider(provider).credential({ + idToken: token, + }); + } + + switch (provider) { + case 'facebook.com': + return FacebookAuthProvider().credential(token); + case 'google.com': + return GoogleAuthProvider().credential(token, secret); + case 'twitter.com': + return TwitterAuthProvider().credential(token, secret); + case 'github.com': + return GithubAuthProvider().credential(token); + case 'apple.com': + return new OAuthProvider(provider).credential({ + idToken: token, + rawNonce: secret, + }); + case 'oauth': + return OAuthProvider(provider).credential({ + idToken: token, + accessToken: secret, + }); + case 'phone': + return PhoneAuthProvider.credential(token, secret); + case 'password': + return EmailAuthProvider.credential(token, secret); + case 'emailLink': + return EmailAuthProvider.credentialWithLink(token, secret); + default: + return null; + } +} + +/** + * Converts a user info object to a plain object. + * @param {UserInfo} userInfo - The UserInfo object to convert. + */ +function userInfoToObject(userInfo) { + return { + providerId: userInfo.providerId, + uid: userInfo.uid, + displayName: + userInfo.displayName !== null && userInfo.displayName !== '' ? userInfo.displayName : null, + email: userInfo.email !== null && userInfo.email !== '' ? userInfo.email : null, + photoURL: userInfo.photoURL !== null && userInfo.photoURL !== '' ? userInfo.photoURL : null, + phoneNumber: + userInfo.phoneNumber !== null && userInfo.phoneNumber !== '' ? userInfo.phoneNumber : null, + }; +} + +/** + * Converts a user metadata object to a plain object. + * @param {UserMetadata} metadata - The UserMetadata object to convert. + */ +function userMetadataToObject(metadata) { + return { + creationTime: metadata.creationTime ? new Date(metadata.creationTime).toISOString() : null, + lastSignInTime: metadata.lastSignInTime + ? new Date(metadata.lastSignInTime).toISOString() + : null, + }; +} + +/** + * Converts a MultiFactorInfo object to a plain object. + * @param {MultiFactorInfo} multiFactorInfo - The MultiFactorInfo object to convert. + */ +function multiFactorInfoToObject(multiFactorInfo) { + const obj = { + displayName: multiFactorInfo.displayName, + enrollmentTime: multiFactorInfo.enrollmentTime, + factorId: multiFactorInfo.factorId, + uid: multiFactorInfo.uid, + }; + + // If https://firebase.google.com/docs/reference/js/auth.phonemultifactorinfo + if ('phoneNumber' in multiFactorInfo) { + obj.phoneNumber = multiFactorInfo.phoneNumber; + } + + return obj; +} + +/** + * Converts a user credential object to a plain object. + * @param {UserCredential} userCredential - The user credential object to convert. + */ +function authResultToObject(userCredential) { + const additional = getAdditionalUserInfo(userCredential); + return { + user: userToObject(userCredential.user), + additionalUserInfo: { + isNewUser: additional.isNewUser, + profile: additional.profile, + providerId: additional.providerId, + username: additional.username, + }, + }; +} + +const instances = {}; +const authStateListeners = {}; +const idTokenListeners = {}; +const sessionMap = new Map(); +let sessionId = 0; + +// Returns a cached Firestore instance. +function getCachedAuthInstance(appName) { + return (instances[appName] ??= getAuth(getApp(appName))); +} + +// getConstants +const CONSTANTS = { + APP_LANGUAGE: {}, + APP_USER: {}, +}; + +for (const appName of getApps()) { + const instance = getAuth(getApp(appName)); + CONSTANTS.APP_LANGUAGE[appName] = instance.languageCode; + if (instance.currentUser) { + CONSTANTS.APP_USER[appName] = userToObject(instance.currentUser); + } +} + +/** + * This is a 'NativeModule' for the web platform. + * Methods here are identical to the ones found in + * the native android/ios modules e.g. `@ReactMethod` annotated + * java methods on Android. + */ +export default { + // Expose all the constants. + ...CONSTANTS, + + async useUserAccessGroup() { + // noop + }, + + configureAuthDomain() { + return rejectPromiseWithCodeAndMessage( + 'unsupported', + 'This operation is not supported in this environment.', + ); + }, + + async getCustomAuthDomain() { + return rejectPromiseWithCodeAndMessage( + 'unsupported', + 'This operation is not supported in this environment.', + ); + }, + + /** + * Create a new auth state listener instance for a given app. + * @param {string} appName - The name of the app to get the auth instance for. + * @returns {Promise} - Void promise. + */ + addAuthStateListener(appName) { + if (authStateListeners[appName]) { + return; + } + + return guard(async () => { + const auth = getCachedAuthInstance(appName); + + authStateListeners[appName] = onAuthStateChanged(auth, user => { + emitEvent('auth_state_changed', { + appName, + user: user ? userToObject(user) : null, + }); + }); + }); + }, + + /** + * Remove an auth state listener instance for a given app. + * @param {string} appName - The name of the app to get the auth instance for. + * @returns {Promise} - Void promise. + */ + removeAuthStateListener(appName) { + if (authStateListeners[appName]) { + authStateListeners[appName](); + delete authStateListeners[appName]; + } + }, + + /** + * Create a new ID token listener instance for a given app. + * @param {string} appName - The name of the app to get the auth instance for. + * @returns {Promise} - Void promise. + */ + addIdTokenListener(appName) { + if (idTokenListeners[appName]) { + return; + } + + return guard(async () => { + const auth = getCachedAuthInstance(appName); + + idTokenListeners[appName] = onIdTokenChanged(auth, user => { + emitEvent('auth_id_token_changed', { + authenticated: !!user, + appName, + user: user ? userToObject(user) : null, + }); + }); + }); + }, + + /** + * Remove an ID token listener instance for a given app. + * @param {string} appName - The name of the app to get the auth instance for. + * @returns {Promise} - Void promise. + */ + removeIdTokenListener(appName) { + if (idTokenListeners[appName]) { + idTokenListeners[appName](); + delete idTokenListeners[appName]; + } + }, + + async forceRecaptchaFlowForTesting() { + return rejectPromiseWithCodeAndMessage( + 'unsupported', + 'This operation is not supported in this environment.', + ); + }, + + async setAutoRetrievedSmsCodeForPhoneNumber() { + return rejectPromiseWithCodeAndMessage( + 'unsupported', + 'This operation is not supported in this environment.', + ); + }, + + async setAppVerificationDisabledForTesting() { + return rejectPromiseWithCodeAndMessage( + 'unsupported', + 'This operation is not supported in this environment.', + ); + }, + + /** + * Sign out the current user. + * @param {string} appName - The name of the app to get the auth instance for. + * @returns {Promise} - Void promise. + */ + signOut(appName) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + + if (auth.currentUser === null) { + return promiseNoUser(true); + } + + await auth.signOut(); + return promiseNoUser(); + }); + }, + + /** + * Sign in anonymously. + * @param {*} appName - The name of the app to get the auth instance for. + * @returns + */ + signInAnonymously(appName) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + const credential = await signInAnonymously(auth); + return authResultToObject(credential); + }); + }, + + /** + * Sign in with email and password. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} email - The email to sign in with. + * @param {string} password - The password to sign in with. + * @returns {Promise} - The result of the sign in. + */ + async createUserWithEmailAndPassword(appName, email, password) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + const credential = await createUserWithEmailAndPassword(auth, email, password); + return authResultToObject(credential); + }); + }, + + /** + * Sign in with email and password. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} email - The email to sign in with. + * @param {string} password - The password to sign in with. + * @returns {Promise} - The result of the sign in. + */ + async signInWithEmailAndPassword(appName, email, password) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + const credential = await signInWithEmailAndPassword(auth, email, password); + return authResultToObject(credential); + }); + }, + + /** + * Sign in with email link. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} email - The email to sign in with. + * @param {string} emailLink - The email link to sign in with. + * @returns {Promise} - The result of the sign in. + */ + async signInWithEmailLink(appName, email, emailLink) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + const credential = await signInWithEmailLink(auth, email, emailLink); + return authResultToObject(credential); + }); + }, + + /** + * Sign in with a custom token. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} token - The token to sign in with. + * @returns {Promise} - The result of the sign in. + */ + async signInWithCustomToken(appName, token) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + const credential = await signInWithCustomToken(auth, token); + return authResultToObject(credential); + }); + }, + + /** + * Not implemented on web. + */ + async revokeToken() { + return promiseNoUser(); + }, + + /** + * Send a password reset email. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} email - The email to send the password reset email to. + * @param {ActionCodeSettings} settings - The settings to use for the password reset email. + * @returns {Promise} + */ + async sendPasswordResetEmail(appName, email, settings) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + await sendPasswordResetEmail(auth, email, settings); + return promiseNoUser(); + }); + }, + + /** + * Send a sign in link to an email. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} email - The email to send the password reset email to. + * @param {ActionCodeSettings} settings - The settings to use for the password reset email. + * @returns {Promise} + */ + async sendSignInLinkToEmail(appName, email, settings) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + await sendSignInLinkToEmail(auth, email, settings); + return promiseNoUser(); + }); + }, + + /* ---------------------- + * .currentUser methods + * ---------------------- */ + + /** + * Delete the current user. + * @param {string} appName - The name of the app to get the auth instance for. + * @returns {Promise} + */ + async delete(appName) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + + if (auth.currentUser === null) { + return promiseNoUser(true); + } + + await auth.currentUser.delete(); + return promiseNoUser(); + }); + }, + + /** + * Reload the current user. + * @param {string} appName - The name of the app to get the auth instance for. + * @returns {Promise} - The current user object. + */ + async reload(appName) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + + if (auth.currentUser === null) { + return promiseNoUser(true); + } + + await auth.currentUser.reload(); + return userToObject(auth.currentUser); + }); + }, + + /** + * Send a verification email to the current user. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {ActionCodeSettings} actionCodeSettings - The settings to use for the email verification. + * @returns {Promise} - The current user object. + */ + async sendEmailVerification(appName, actionCodeSettings) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + + if (auth.currentUser === null) { + return promiseNoUser(true); + } + + await sendEmailVerification(auth.currentUser, actionCodeSettings); + return userToObject(auth.currentUser); + }); + }, + + /** + * Verify the email before updating it. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} email - The email to verify. + * @param {ActionCodeSettings} actionCodeSettings - The settings to use for the email verification. + * @returns {Promise} - The current user object. + */ + async verifyBeforeUpdateEmail(appName, email, actionCodeSettings) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + + if (auth.currentUser === null) { + return promiseNoUser(true); + } + + await verifyBeforeUpdateEmail(auth.currentUser, email, actionCodeSettings); + return userToObject(auth.currentUser); + }); + }, + + /** + * Update the current user's email. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} email - The email to update. + * @returns {Promise} - The current user object. + */ + async updateEmail(appName, email) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + + if (auth.currentUser === null) { + return promiseNoUser(true); + } + + await updateEmail(auth.currentUser, email); + return userToObject(auth.currentUser); + }); + }, + + /** + * Update the current user's password. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} password - The password to update. + * @returns {Promise} - The current user object. + */ + async updatePassword(appName, password) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + + if (auth.currentUser === null) { + return promiseNoUser(true); + } + + await updatePassword(auth.currentUser, password); + return userToObject(auth.currentUser); + }); + }, + + /** + * Update the current user's phone number. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} provider - The provider to update the phone number with. + * @param {string} authToken - The auth token to update the phone number with. + * @param {string} authSecret - The auth secret to update the phone number with. + * @returns {Promise} - The current user object. + */ + async updatePhoneNumber(appName, provider, authToken, authSecret) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + + if (auth.currentUser === null) { + return promiseNoUser(true); + } + + if (provider !== 'phone') { + return rejectPromiseWithCodeAndMessage( + 'invalid-credential', + 'The supplied auth credential does not have a phone provider.', + ); + } + + const credential = getAuthCredential(auth, provider, authToken, authSecret); + + if (!credential) { + return rejectPromiseWithCodeAndMessage( + 'invalid-credential', + 'The supplied auth credential is malformed, has expired or is not currently supported.', + ); + } + + await updatePhoneNumber(auth.currentUser, credential); + + return userToObject(auth.currentUser); + }); + }, + + /** + * Update the current user's profile. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {object} props - The properties to update. + * @returns {Promise} - The current user object. + */ + async updateProfile(appName, props) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + + if (auth.currentUser === null) { + return promiseNoUser(true); + } + + await updateProfile(auth.currentUser, { + displayName: props.displayName, + photoURL: props.photoURL, + }); + + return userToObject(auth.currentUser); + }); + }, + + /** + * Sign in with a credential. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} provider - The provider to sign in with. + * @param {string} authToken - The auth token to sign in with. + * @param {string} authSecret - The auth secret to sign in with. + * @returns {Promise} - The result of the sign in. + */ + async signInWithCredential(appName, provider, authToken, authSecret) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + const credential = getAuthCredential(auth, provider, authToken, authSecret); + + if (credential === null) { + return rejectPromiseWithCodeAndMessage( + 'invalid-credential', + 'The supplied auth credential is malformed, has expired or is not currently supported.', + ); + } + + const credentialResult = await signInWithCredential(auth, credential); + return authResultToObject(credentialResult); + }); + }, + + async signInWithProvider() { + return rejectPromiseWithCodeAndMessage( + 'unsupported', + 'This operation is not supported in this environment.', + ); + }, + + // TODO... + async signInWithPhoneNumber() {}, + + /** + * Get a multi-factor session. + * @param {string} appName - The name of the app to get the auth instance for. + * @returns {Promise} - The session ID. + */ + async getSession(appName) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + + if (auth.currentUser === null) { + return promiseNoUser(true); + } + + const session = await multiFactor(auth.currentUser).getSession(); + + // Increment the session ID. + sessionId++; + + const key = `${sessionId}`; + sessionMap.set(key, session); + return key; + }); + }, + + verifyPhoneNumberForMultiFactor() { + return rejectPromiseWithCodeAndMessage( + 'unsupported', + 'This operation is not supported in this environment.', + ); + }, + + finalizeMultiFactorEnrollment() { + return rejectPromiseWithCodeAndMessage( + 'unsupported', + 'This operation is not supported in this environment.', + ); + }, + + resolveMultiFactorSignIn() { + return rejectPromiseWithCodeAndMessage( + 'unsupported', + 'This operation is not supported in this environment.', + ); + }, + + confirmationResultConfirm() { + return rejectPromiseWithCodeAndMessage( + 'unsupported', + 'This operation is not supported in this environment.', + ); + }, + + verifyPhoneNumber() { + return rejectPromiseWithCodeAndMessage( + 'unsupported', + 'This operation is not supported in this environment.', + ); + }, + + /** + * Confirm the password reset code. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} code - The code to confirm. + * @param {string} newPassword - The new password to set. + * @returns {Promise} + */ + async confirmPasswordReset(appName, code, newPassword) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + await confirmPasswordReset(auth, code, newPassword); + return promiseNoUser(); + }); + }, + + /** + * Apply an action code. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} code - The code to apply. + * @returns {Promise} - Void promise. + */ + async applyActionCode(appName, code) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + await applyActionCode(auth, code); + }); + }, + + /** + * Check an action code. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} code - The code to check. + * @returns {Promise} - The result of the check. + */ + async checkActionCode(appName, code) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + const result = await checkActionCode(auth, code); + + return { + operation: result.operation, + data: { + email: result.data.email, + fromEmail: result.data.previousEmail, + // multiFactorInfo - not implemented + }, + }; + }); + }, + + /** + * Link a credential to the current user. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} provider - The provider to link. + * @param {string} authToken - The auth token to link. + * @param {string} authSecret - The auth secret to link. + * @returns {Promise} - The current user object. + */ + async linkWithCredential(appName, provider, authToken, authSecret) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + const credential = getAuthCredential(auth, provider, authToken, authSecret); + + if (credential === null) { + return rejectPromiseWithCodeAndMessage( + 'invalid-credential', + 'The supplied auth credential is malformed, has expired or is not currently supported.', + ); + } + + if (auth.currentUser === null) { + return promiseNoUser(true); + } + + return authResultToObject(await linkWithCredential(auth.currentUser, credential)); + }); + }, + + async linkWithProvider() { + // TODO: We could check if window is available here, but for now it's not supported. + return rejectPromiseWithCodeAndMessage( + 'unsupported', + 'This operation is not supported in this environment.', + ); + }, + + /** + * Unlink a provider from the current user. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} providerId - The provider ID to unlink. + * @returns {Promise} - The current user object. + */ + async unlink(appName, providerId) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + + if (auth.currentUser === null) { + return promiseNoUser(true); + } + + const user = await unlink(auth.currentUser, providerId); + return userToObject(user); + }); + }, + + /** + * Reauthenticate with a credential. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} provider - The provider to reauthenticate with. + * @param {string} authToken - The auth token to reauthenticate with. + * @param {string} authSecret - The auth secret to reauthenticate with. + * @returns {Promise} - The current user object. + */ + async reauthenticateWithCredential(appName, provider, authToken, authSecret) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + const credential = getAuthCredential(auth, provider, authToken, authSecret); + + if (credential === null) { + return rejectPromiseWithCodeAndMessage( + 'invalid-credential', + 'The supplied auth credential is malformed, has expired or is not currently supported.', + ); + } + + if (auth.currentUser === null) { + return promiseNoUser(true); + } + + return authResultToObject(await reauthenticateWithCredential(auth.currentUser, credential)); + }); + }, + + async reauthenticateWithProvider() { + // TODO: We could check if window is available here, but for now it's not supported. + return rejectPromiseWithCodeAndMessage( + 'unsupported', + 'This operation is not supported in this environment.', + ); + }, + + /** + * Get the ID token for the current user. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {boolean} forceRefresh - Whether to force a token refresh. + * @returns {Promise} - The ID token. + */ + async getIdToken(appName, forceRefresh) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + + if (auth.currentUser === null) { + return promiseNoUser(true); + } + + const token = await getIdToken(auth.currentUser, forceRefresh); + return token; + }); + }, + + /** + * Get the ID token result for the current user. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {boolean} forceRefresh - Whether to force a token refresh. + * @returns {Promise} - The ID token result. + */ + async getIdTokenResult(appName, forceRefresh) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + + if (auth.currentUser === null) { + return promiseNoUser(true); + } + + const result = await getIdTokenResult(auth.currentUser, forceRefresh); + + // TODO(ehesp): Result looks expected, might be safer to keep fixed object? + return { + authTime: result.authTime, + expirationTime: result.expirationTime, + issuedAtTime: result.issuedAtTime, + claims: result.claims, + signInProvider: result.signInProvider, + token: result.token, + }; + }); + }, + + /* ---------------------- + * other methods + * ---------------------- */ + + /** + * Fetch the sign in methods for an email. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} email - The email to fetch the sign in methods for. + * @returns {Promise} - The sign in methods for the email. + */ + async fetchSignInMethodsForEmail(appName, email) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + const methods = await fetchSignInMethodsForEmail(auth, email); + return methods; + }); + }, + + /** + * Set the language code. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} code - The language code to set. + * @returns {void} + */ + setLanguageCode(appName, code) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + auth.languageCode = code; + }); + }, + + /** + * Set the tenant ID. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} tenantId - The tenant ID to set. + * @returns {void} + */ + setTenantId(appName, tenantId) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + auth.tenantId = tenantId; + }); + }, + + /** + * Use the device language. + * @param {string} appName - The name of the app to get the auth instance for. + * @returns void + */ + useDeviceLanguage(appName) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + useDeviceLanguage(auth); + }); + }, + + /** + * Verify the provided password reset code. + * @returns {string} - The users email address if valid. + */ + verifyPasswordResetCode(appName, code) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + const email = await verifyPasswordResetCode(auth, code); + return email; + }); + }, + + /** + * Connect to the auth emulator. + * @param {string} appName - The name of the app to get the auth instance for. + * @param {string} host - The host to use for the auth emulator. + * @param {number} port - The port to use for the auth emulator. + * @returns {void} + */ + useEmulator(appName, host, port) { + return guard(async () => { + const auth = getCachedAuthInstance(appName); + connectAuthEmulator(auth, `http://${host}:${port}`); + }); + }, +}; diff --git a/tests/app.js b/tests/app.js index dadcac1914..5cfbe4ebfc 100644 --- a/tests/app.js +++ b/tests/app.js @@ -25,6 +25,7 @@ const platformSupportedModules = []; if (Platform.other) { platformSupportedModules.push('app'); platformSupportedModules.push('functions'); + platformSupportedModules.push('auth'); platformSupportedModules.push('storage'); // TODO add more modules here once they are supported. }