@taraai/read-write
Version:
Synchronous NoSQL/Firestore for React
420 lines (388 loc) • 14.1 kB
JavaScript
import { capitalize } from 'lodash'
import { supportedAuthProviders, actionTypes } from '../constants'
/**
* @description Get correct login method and params order based on provided credentials
* @param {object} firebase - Internal firebase object
* @param {string} providerName - Name of Auth Provider (i.e. google, github, facebook, twitter)
* @param {Array|string} scopes - List of scopes to add to auth provider
* @returns {firebase.auth.AuthCredential} provider - Auth Provider
* @private
*/
function createAuthProvider(firebase, providerName, scopes) {
// TODO: Verify scopes are valid before adding
// TODO: Validate parameter inputs
const lowerCaseProviderName = providerName.toLowerCase()
if (
lowerCaseProviderName === 'microsoft.com' ||
lowerCaseProviderName === 'apple.com' ||
lowerCaseProviderName === 'yahoo.com'
) {
const provider = new firebase.auth.OAuthProvider(providerName)
return provider
}
const capitalProviderName = `${capitalize(providerName)}AuthProvider`
// Throw if auth provider does not exist on Firebase instance
if (!firebase.auth[capitalProviderName]) {
throw new Error(
`${providerName} is not a valid auth provider for your firebase instance. If using react-native, use a RN specific auth library.`
)
}
const provider = new firebase.auth[capitalProviderName]()
// Custom Auth Parameters
// TODO: Validate parameter inputs
const { customAuthParameters } = firebase._.config
if (customAuthParameters && customAuthParameters[providerName]) {
provider.setCustomParameters(customAuthParameters[providerName])
}
// Handle providers without scopes
if (
lowerCaseProviderName === 'twitter' ||
typeof provider.addScope !== 'function'
) {
return provider
}
// TODO: Verify scopes are valid before adding
provider.addScope('email')
if (scopes) {
if (Array.isArray(scopes)) {
scopes.forEach((scope) => {
provider.addScope(scope)
})
}
// Add single scope if it is a string
if (typeof scopes === 'string' || scopes instanceof String) {
provider.addScope(scopes)
}
}
return provider
}
/**
* Get correct login method and params order based on provided
* credentials
* @param {object} firebase - Internal firebase object
* @param {object} credentials - Login credentials
* @param {string} credentials.email - Email to login with (only needed for
* email login)
* @param {string} credentials.password - Password to login with (only needed
* for email login)
* @param {string} credentials.provider - Provider name such as google, twitter
* (only needed for 3rd party provider login)
* @param {string} credentials.type - Popup or redirect (only needed for 3rd
* party provider login)
* @param {string} credentials.token - Custom or provider token
* @param {firebase.auth.AuthCredential} credentials.credential - Custom or
* provider token
* @param {Array|string} credentials.scopes - Scopes to add to provider
* (i.e. email)
* @returns {object} Method and params for calling login
* @private
*/
export function getLoginMethodAndParams(firebase, credentials) {
const {
email,
password,
provider,
type,
token,
scopes,
phoneNumber,
applicationVerifier,
credential,
emailLink
} = credentials
// Credential Auth
if (credential) {
// Attempt to use signInAndRetrieveDataWithCredential if it exists (see #467 for more info)
const credentialAuth = firebase.auth().signInAndRetrieveDataWithCredential
if (credentialAuth) {
return {
method: 'signInAndRetrieveDataWithCredential',
params: [credential]
}
}
return { method: 'signInWithCredential', params: [credential] }
}
// Provider Auth
if (provider) {
// Verify providerName is valid
if (supportedAuthProviders.indexOf(provider.toLowerCase()) === -1) {
throw new Error(`${provider} is not a valid Auth Provider`)
}
if (token) {
throw new Error(
'provider with token no longer supported, use credential parameter instead'
)
}
const authProvider = createAuthProvider(firebase, provider, scopes)
if (type === 'popup') {
return { method: 'signInWithPopup', params: [authProvider] }
}
return { method: 'signInWithRedirect', params: [authProvider] }
}
// Token Auth
if (token) {
// Check for new sign in method (see #484 for more info)
const tokenAuth = firebase.auth().signInAndRetrieveDataWithCustomToken
if (tokenAuth) {
return { method: 'signInAndRetrieveDataWithCustomToken', params: [token] }
}
return { method: 'signInWithCustomToken', params: [token] }
}
// Phone Number Auth
if (phoneNumber) {
if (!applicationVerifier) {
throw new Error(
'Application verifier is required for phone authentication'
)
}
return {
method: 'signInWithPhoneNumber',
params: [phoneNumber, applicationVerifier]
}
}
// Passwordless sign-in
if (emailLink && email) {
return { method: 'signInWithEmailLink', params: [email, emailLink] }
}
// Check for new sign in method (see #484 for more info)
// Note: usage of signInAndRetrieveDataWithEmailAndPassword is now a fallback since it is deprecated (see #484 for more info)
if (!firebase.auth().signInWithEmailAndPassword) {
return {
method: 'signInAndRetrieveDataWithEmailAndPassword',
params: [email, password]
}
}
// Email/Password Auth
return { method: 'signInWithEmailAndPassword', params: [email, password] }
}
/**
* Get correct reauthenticate method and params order based on provided
* credentials
* @param {object} firebase - Internal firebase object
* @param {object} credentials - Login credentials
* @param {string} credentials.provider - Provider name such as google, twitter
* (only needed for 3rd party provider login)
* @param {string} credentials.type - Popup or redirect (only needed for 3rd
* party provider login)
* @param {firebase.auth.AuthCredential} credentials.credential - Custom or
* provider token
* @param {Array|string} credentials.scopes - Scopes to add to provider
* (i.e. email)
* @returns {object} Method and params for calling login
* @private
*/
export function getReauthenticateMethodAndParams(firebase, credentials) {
const {
provider,
type,
scopes,
phoneNumber,
applicationVerifier,
credential
} = credentials
// Credential Auth
if (credential) {
// Attempt to use signInAndRetrieveDataWithCredential if it exists (see #467 for more info)
const credentialAuth = firebase.auth()
.reauthenticateAndRetrieveDataWithCredential
if (credentialAuth) {
return {
method: 'reauthenticateAndRetrieveDataWithCredential',
params: [credential]
}
}
return { method: 'reauthenticateWithCredential', params: [credential] }
}
// Provider Auth
if (provider) {
// Verify providerName is valid
if (supportedAuthProviders.indexOf(provider.toLowerCase()) === -1) {
throw new Error(`${provider} is not a valid Auth Provider`)
}
const authProvider = createAuthProvider(firebase, provider, scopes)
if (type === 'popup') {
return { method: 'reauthenticateWithPopup', params: [authProvider] }
}
return { method: 'reauthenticateWithRedirect', params: [authProvider] }
}
// Phone Number Auth
if (!applicationVerifier) {
throw new Error('Application verifier is required for phone authentication')
}
return {
method: 'reauthenticateWithPhoneNumber',
params: [phoneNumber, applicationVerifier]
}
}
/**
* Returns a promise that completes when Firebase Auth is ready in the given
* store using react-redux-firebase.
* @param {object} store - The Redux store on which we want to detect if
* Firebase auth is ready.
* @param {string} [stateName='firebase'] - The attribute name of the
* react-redux-firebase reducer when using multiple combined reducers.
* 'firebase' by default. Set this to `null` to indicate that the
* react-redux-firebase reducer is not in a combined reducer.
* @returns {Promise} Resolves when Firebase auth is ready in the store.
*/
function isAuthReady(store, stateName) {
const state = store.getState()
const firebaseState = stateName ? state[stateName] : state
const firebaseAuthState = firebaseState && firebaseState.auth
if (!firebaseAuthState) {
throw new Error(
`The Firebase auth state could not be found in the store under the attribute '${
stateName ? `${stateName}.` : ''
}auth'. Make sure your react-redux-firebase reducer is correctly set in the store`
)
}
return firebaseState.auth.isLoaded
}
/**
* Returns a promise that completes when Firebase Auth is ready in the given
* store using react-redux-firebase.
* @param {object} store - The Redux store on which we want to detect if
* Firebase auth is ready.
* @param {string} [stateName='firebase'] - The attribute name of the react-redux-firebase
* reducer when using multiple combined reducers. 'firebase' by default. Set
* this to `null` to indicate that the react-redux-firebase reducer is not in a
* combined reducer.
* @returns {Promise} Resolve when Firebase auth is ready in the store.
*/
export function authIsReady(store, stateName = 'firebase') {
return new Promise((resolve) => {
if (isAuthReady(store, stateName)) {
resolve()
} else {
const unsubscribe = store.subscribe(() => {
if (isAuthReady(store, stateName)) {
unsubscribe()
resolve()
}
})
}
})
}
/**
* Function that creates and authIsReady promise
* @param {object} store - The Redux store on which we want to detect if
* Firebase auth is ready.
* @param {object} config - Config options for authIsReady
* @param {string} config.authIsReady - Config options for authIsReady
* @param {string} config.firebaseStateName - Config options for authIsReady
* @returns {Promise} Resolves when Firebase auth is ready in the store.
*/
export function createAuthIsReady(store, config) {
return typeof config.authIsReady === 'function'
? config.authIsReady(store, config)
: authIsReady(store, config.firebaseStateName)
}
/**
* Update profile data on Firebase Real Time Database
* @param {object} firebase - internal firebase object
* @param {object} profileUpdate - Updates to profile object
* @returns {Promise} Resolves with results of profile get
*/
export function updateProfileOnRTDB(firebase, profileUpdate) {
const {
_: { config, authUid }
} = firebase
const profileRef = firebase.database().ref(`${config.userProfile}/${authUid}`)
return profileRef.update(profileUpdate).then(() => profileRef.once('value'))
}
/**
* Update profile data on Firestore by calling set (with merge: true) on
* the profile.
* @param {object} firebase - internal firebase object
* @param {object} profileUpdate - Updates to profile object
* @param {object} options - Options object for configuring how profile
* update occurs
* @param {boolean} [options.useSet=true] - Use set with merge instead of
* update. Setting to `false` uses update (can cause issue if profile document
* does not exist).
* @param {boolean} [options.merge=true] - Whether or not to use merge when
* setting profile
* @returns {Promise} Resolves with results of profile get
*/
export function updateProfileOnFirestore(
firebase,
profileUpdate,
options = {}
) {
const { useSet = true, merge = true } = options
const {
firestore,
_: { config, authUid }
} = firebase
const profileRef = firestore().doc(`${config.userProfile}/${authUid}`)
// Use set with merge (to prevent "No document to update") unless otherwise
// specificed through options
const profileUpdatePromise = useSet
? profileRef.set(profileUpdate, { merge })
: profileRef.update(profileUpdate)
return profileUpdatePromise.then(() => profileRef.get())
}
/**
* Start presence management for a specificed user uid.
* Presence collection contains a list of users that are online currently.
* Sessions collection contains a record of all user sessions.
* This function is called within login functions if enablePresence: true.
* @param {Function} dispatch - Action dispatch function
* @param {object} firebase - Internal firebase object
* @private
*/
export function setupPresence(dispatch, firebase) {
// exit if database does not exist on firebase instance
if (!firebase.database || !firebase.database.ServerValue) {
return
}
const ref = firebase.database().ref()
const {
config: { presence, sessions },
authUid
} = firebase._
const amOnline = ref.child('.info/connected')
const onlineRef = ref
.child(
typeof presence === 'function'
? presence(firebase.auth().currentUser, firebase)
: presence
)
.child(authUid)
let sessionsRef =
typeof sessions === 'function'
? sessions(firebase.auth().currentUser, firebase)
: sessions
if (sessionsRef) {
sessionsRef = ref.child(sessions)
}
amOnline.on('value', (snapShot) => {
if (!snapShot.val()) return
// user is online
if (sessionsRef) {
// add session and set disconnect
dispatch({ type: actionTypes.SESSION_START, payload: authUid })
// add new session to sessions collection
const session = sessionsRef.push({
startedAt: firebase.database.ServerValue.TIMESTAMP,
user: authUid
})
// Support versions of react-native-firebase that do not have setPriority
// on firebase.database.ThenableReference
if (typeof session.setPriority === 'function') {
// set authUid as priority for easy sorting
session.setPriority(authUid)
}
session
.child('endedAt')
.onDisconnect()
.set(firebase.database.ServerValue.TIMESTAMP, () => {
dispatch({ type: actionTypes.SESSION_END })
})
}
// add correct session id to user
// remove from presence list
onlineRef.set(true)
onlineRef.onDisconnect().remove()
})
}