Skip to content

Commit 038d214

Browse files
authored
v2.0.4
* feat(profileReducer): `PROFILE_UPDATE_SUCCESS` action updates profile state - prescottprue#395 - @serhiipalash * fix(firestore): prevent issue where default profile creation would break (`createdAt` was undefined) - prescottprue#391 - @compojoom * feat(profile): `profileFactory` support for firestore - prescottprue#391 - @compojoom * fix(login): login-logout-login flow always correctly updates `state.firebase.profile` - prescottprue#401 - @evgeni-tsn * fix(updateProfile): "no document to update" error no longer thrown when updating non-existant profile on Firestore (`useSet` boolean added to make this optional) - prescottprue#403
2 parents cf2c2f0 + 65cda3a commit 038d214

File tree

9 files changed

+212
-26
lines changed

9 files changed

+212
-26
lines changed

docs/api/firebaseInstance.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,12 +387,21 @@ Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/
387387

388388
## updateProfile
389389

390-
Update user profile
390+
Update user profile on Firebase Real Time Database or
391+
Firestore (if `useFirestoreForProfile: true` config passed to
392+
reactReduxFirebase). Real Time Database update uses `update` method
393+
internally while updating profile on Firestore uses `set` with
391394

392395
**Parameters**
393396

394-
- `profileUpdate`
395-
- `profile` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** Profile data to place in new profile
397+
- `profileUpdate` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** Profile data to place in new profile
398+
- `options` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** Options object (used to change how profile
399+
update occurs)
400+
- `options.useSet` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Use set with merge instead of
401+
update. Setting to `false` uses update (can cause issue of profile document
402+
does not exist). Note: Only used when updating profile on Firestore (optional, default `true`)
403+
- `options.merge` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Whether or not to use merge when
404+
setting profile. Note: Only used when updating profile on Firestore (optional, default `true`)
396405

397406
Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)**
398407

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-redux-firebase",
3-
"version": "2.0.3",
3+
"version": "2.0.4",
44
"description": "Redux integration for Firebase. Comes with a Higher Order Components for use with React.",
55
"main": "lib/index.js",
66
"module": "es/index.js",

src/actions/auth.js

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { isArray, isString, isFunction, forEach, omit } from 'lodash'
22
import { actionTypes } from '../constants'
33
import { populate } from '../helpers'
4-
import { stringToDate } from '../utils'
54
import {
65
getLoginMethodAndParams,
76
updateProfileOnRTDB,
@@ -212,19 +211,25 @@ export const createUserProfile = (dispatch, firebase, userData, profile) => {
212211
.doc(userData.uid)
213212
.get()
214213
.then(profileSnap => {
215-
// Convert to JSON format (to prevent issue of writing invalid type to Firestore)
216-
const userDataObject = userData.toJSON ? userData.toJSON() : userData
217-
// Remove unnessesary auth params (configurable) and preserve types of timestamps
218-
const newProfile = {
219-
...omit(userDataObject, config.keysToRemoveFromAuth),
220-
avatarUrl: userDataObject.photoURL, // match profile pattern used for RTDB
221-
createdAt: stringToDate(userDataObject.createdAt),
222-
lastLoginAt: stringToDate(userDataObject.lastLoginAt)
223-
}
224214
// Return if config for updating profile is not enabled and profile exists
225215
if (!config.updateProfileOnLogin && profileSnap.exists) {
226216
return profileSnap.data()
227217
}
218+
219+
let newProfile = {}
220+
// If the user did supply a profileFactory, we should use the result of it for the new Profile
221+
if (isFunction(config.profileFactory)) {
222+
newProfile = profile
223+
} else {
224+
// Convert to JSON format (to prevent issue of writing invalid type to Firestore)
225+
const userDataObject = userData.toJSON ? userData.toJSON() : userData
226+
// Remove unnecessary auth params (configurable) and preserve types of timestamps
227+
newProfile = {
228+
...omit(userDataObject, config.keysToRemoveFromAuth),
229+
avatarUrl: userDataObject.photoURL // match profile pattern used for RTDB
230+
}
231+
}
232+
228233
// Create/Update the profile
229234
return profileSnap.ref
230235
.set(newProfile, { merge: true })
@@ -345,14 +350,14 @@ const handleAuthStateChange = (dispatch, firebase, authData) => {
345350
setupPresence(dispatch, firebase)
346351
}
347352

348-
watchUserProfile(dispatch, firebase)
349-
350353
dispatch({
351354
type: actionTypes.LOGIN,
352355
auth: authData,
353356
preserve: config.preserveOnLogin
354357
})
355358

359+
watchUserProfile(dispatch, firebase)
360+
356361
// Run onAuthStateChanged if it exists in config
357362
if (isFunction(config.onAuthStateChanged)) {
358363
config.onAuthStateChanged(authData, firebase, dispatch)

src/createFirebaseInstance.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -408,12 +408,22 @@ export const createFirebaseInstance = (firebase, configs, dispatch) => {
408408
authActions.verifyPasswordResetCode(dispatch, firebase, code)
409409

410410
/**
411-
* @description Update user profile
412-
* @param {Object} profile - Profile data to place in new profile
411+
* @description Update user profile on Firebase Real Time Database or
412+
* Firestore (if `useFirestoreForProfile: true` config passed to
413+
* reactReduxFirebase). Real Time Database update uses `update` method
414+
* internally while updating profile on Firestore uses `set` with
415+
* @param {Object} profileUpdate - Profile data to place in new profile
416+
* @param {Object} options - Options object (used to change how profile
417+
* update occurs)
418+
* @param {Boolean} [options.useSet=true] - Use set with merge instead of
419+
* update. Setting to `false` uses update (can cause issue of profile document
420+
* does not exist). Note: Only used when updating profile on Firestore
421+
* @param {Boolean} [options.merge=true] - Whether or not to use merge when
422+
* setting profile. Note: Only used when updating profile on Firestore
413423
* @return {Promise}
414424
*/
415-
const updateProfile = profileUpdate =>
416-
authActions.updateProfile(dispatch, firebase, profileUpdate)
425+
const updateProfile = (profileUpdate, options) =>
426+
authActions.updateProfile(dispatch, firebase, profileUpdate, options)
417427

418428
/**
419429
* @description Update Auth Object

src/reducers.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ const {
2828
AUTH_LINK_SUCCESS,
2929
UNAUTHORIZED_ERROR,
3030
AUTH_UPDATE_SUCCESS,
31-
AUTH_RELOAD_SUCCESS
31+
AUTH_RELOAD_SUCCESS,
32+
PROFILE_UPDATE_SUCCESS
3233
} = actionTypes
3334

3435
/**
@@ -281,6 +282,8 @@ export const profileReducer = (
281282
isEmpty: false,
282283
isLoaded: true
283284
}
285+
case PROFILE_UPDATE_SUCCESS:
286+
return Object.assign({}, state, action.payload)
284287
case LOGIN:
285288
// Support keeping data when logging out
286289
if (action.preserve && action.preserve.profile) {

src/utils/auth.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,13 +191,31 @@ export const updateProfileOnRTDB = (firebase, profileUpdate) => {
191191
}
192192

193193
/**
194-
* Update profile data on Firestore
194+
* Update profile data on Firestore by calling set (with merge: true) on
195+
* the profile.
195196
* @param {Object} firebase - internal firebase object
196197
* @param {Object} profileUpdate - Updates to profile object
198+
* @param {Object} options - Options object for configuring how profile
199+
* update occurs
200+
* @param {Boolean} [options.useSet=true] - Use set with merge instead of
201+
* update. Setting to `false` uses update (can cause issue of profile document
202+
* does not exist).
203+
* @param {Boolean} [options.merge=true] - Whether or not to use merge when
204+
* setting profile
197205
* @return {Promise} Resolves with results of profile get
198206
*/
199-
export const updateProfileOnFirestore = (firebase, profileUpdate) => {
207+
export const updateProfileOnFirestore = (
208+
firebase,
209+
profileUpdate,
210+
options = {}
211+
) => {
212+
const { useSet = true, merge = true } = options
200213
const { firestore, _: { config, authUid } } = firebase
201214
const profileRef = firestore().doc(`${config.userProfile}/${authUid}`)
202-
return profileRef.update(profileUpdate).then(() => profileRef.get())
215+
// Use set with merge (to prevent "No document to update") unless otherwise
216+
// specificed through options
217+
const profileUpdatePromise = useSet
218+
? profileRef.set(profileUpdate, { merge })
219+
: profileRef.update(profileUpdate)
220+
return profileUpdatePromise.then(() => profileRef.get())
203221
}

test/unit/helpers.spec.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,27 @@ describe('Helpers:', () => {
269269
exampleData.data[rootName].ABC.displayName
270270
)
271271
})
272+
273+
it('supports childParam', () => {
274+
populates = [
275+
{
276+
child: 'collaborators',
277+
root: rootName,
278+
childParam: 'displayName'
279+
}
280+
]
281+
// Non matching collaborator
282+
expect(
283+
helpers.populate(exampleData, path, populates)
284+
).to.have.deep.property(`${valName}.collaborators.abc`, true)
285+
// Matching collaborator
286+
expect(
287+
helpers.populate(exampleData, path, populates)
288+
).to.have.deep.property(
289+
`${valName}.collaborators.ABC`,
290+
exampleData.data[rootName].ABC.displayName
291+
)
292+
})
272293
})
273294

274295
describe('config as function', () => {
@@ -354,7 +375,6 @@ describe('Helpers:', () => {
354375
)
355376
})
356377

357-
// Skipped since this is not currently supported
358378
it('from same root', () => {
359379
populates = [
360380
{ child: 'owner', root: rootName },
@@ -408,4 +428,19 @@ describe('Helpers:', () => {
408428
expect(helpers.isEmpty([{}])).to.be.false
409429
})
410430
})
431+
432+
describe('fixPath', () => {
433+
it('exists', () => {
434+
expect(helpers).to.respondTo('fixPath')
435+
})
436+
437+
it('returns original path if no fix is required', () => {
438+
const originalPath = '/asdf'
439+
expect(helpers.fixPath(originalPath)).to.equal(originalPath)
440+
})
441+
442+
it('adds / to beginning of path', () => {
443+
expect(helpers.fixPath('asdf')).to.equal('/asdf')
444+
})
445+
})
411446
})

test/unit/reducer.spec.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,15 @@ describe('reducer', () => {
527527
})
528528
})
529529

530+
describe('PROFILE_UPDATE_SUCCESS action -', () => {
531+
it('sets auth state to isLoaded: true, isEmpty: false', () => {
532+
const payload = { some: 'value' }
533+
action = { type: actionTypes.PROFILE_UPDATE_SUCCESS, payload }
534+
const currentState = firebaseReducer({}, action)
535+
expect(currentState).to.have.deep.property('profile.some', payload.some)
536+
})
537+
})
538+
530539
describe('LOGIN action -', () => {
531540
it('sets auth state to isLoaded: true, isEmpty: false', () => {
532541
const auth = { some: 'value' }
@@ -552,6 +561,103 @@ describe('reducer', () => {
552561
true
553562
)
554563
})
564+
565+
describe('preserve parameter', () => {
566+
describe('profile state is preserved when provided', () => {
567+
it('an array of keys', () => {
568+
const payload = { some: 'other' }
569+
const initialState = { profile: { some: 'value' } }
570+
action = {
571+
type: actionTypes.LOGIN,
572+
preserve: { profile: ['some'] },
573+
payload
574+
}
575+
expect(firebaseReducer(initialState, action)).to.have.deep.property(
576+
'profile.some',
577+
'value'
578+
)
579+
})
580+
581+
it('a boolean (true)', () => {
582+
const payload = { some: 'other' }
583+
const initialState = { profile: { some: 'value' } }
584+
action = {
585+
type: actionTypes.LOGIN,
586+
preserve: { profile: true },
587+
payload
588+
}
589+
expect(firebaseReducer(initialState, action)).to.have.deep.property(
590+
'profile.some',
591+
'value'
592+
)
593+
})
594+
})
595+
596+
describe('profile state is updated when is already exists and is provided', () => {
597+
it('a boolean (true)', () => {
598+
const payload = { some: 'other' }
599+
const initialState = { profile: { some: 'value' } }
600+
action = {
601+
type: actionTypes.LOGIN,
602+
preserve: { profile: true },
603+
payload
604+
}
605+
expect(firebaseReducer(initialState, action)).to.have.deep.property(
606+
'profile.some',
607+
'value'
608+
)
609+
})
610+
})
611+
612+
describe('auth state is preserved when provided', () => {
613+
it('an array of keys', () => {
614+
const payload = { some: 'other' }
615+
const initialState = { auth: { some: 'value' } }
616+
action = {
617+
type: actionTypes.LOGIN,
618+
preserve: { auth: ['some'] },
619+
auth: payload
620+
}
621+
expect(firebaseReducer(initialState, action)).to.have.deep.property(
622+
'auth.some',
623+
'value'
624+
)
625+
})
626+
627+
it('a boolean (true)', () => {
628+
const initialState = { auth: { some: 'value' } }
629+
action = {
630+
type: actionTypes.LOGIN,
631+
preserve: { auth: true },
632+
auth: {}
633+
}
634+
expect(firebaseReducer(initialState, action)).to.have.deep.property(
635+
'auth.some',
636+
'value'
637+
)
638+
})
639+
})
640+
641+
describe('auth state is updated when is already exists and is provided', () => {
642+
it('a boolean (true)', () => {
643+
const payload = { some: 'other', another: 'thing' }
644+
const initialState = { auth: { some: 'value' } }
645+
action = {
646+
type: actionTypes.LOGIN,
647+
preserve: { auth: true },
648+
auth: payload
649+
}
650+
expect(firebaseReducer(initialState, action)).to.have.deep.property(
651+
'auth.some',
652+
payload.some
653+
)
654+
expect(firebaseReducer(initialState, action)).to.have.deep.property(
655+
'auth.another',
656+
payload.another
657+
)
658+
})
659+
})
660+
})
555661
})
556662

557663
describe('REMOVE action -', () => {

0 commit comments

Comments
 (0)