Skip to content

Commit bce9510

Browse files
prescottpruerscottenAlexanderArvidssondjejaquinozozoens31
authored
v3.8.0 (#1025)
* feat(auth): enable claims without userProfile (#1008) - @rscotten * fix(types): add arguments to types for onAuthStateChanged (#1018) - @AlexanderArvidsson * fix(auth): dispatch proper error on reset password (#1016) - @djejaquino * fix(types) move static firestore interface to where it's implemented (#1013) - @zozoens31 * feat(auth): add applyActionCode method (#994) - @komachi Co-authored-by: Richard Scotten <[email protected]> Co-authored-by: Alexander Arvidsson <[email protected]> Co-authored-by: Davi Aquino <[email protected]> Co-authored-by: Cyrille Corpet <[email protected]> Co-authored-by: Anton Nesterov <[email protected]>
1 parent dbea9e4 commit bce9510

File tree

7 files changed

+204
-58
lines changed

7 files changed

+204
-58
lines changed

docs/auth.md

+21-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ If you need access to methods that are not available at the top level, you can a
2222

2323
Firebase has a secure way of identifying and making claims about users with [custom claims](https://firebase.google.com/docs/auth/admin/custom-claims). This is a good way to provide roles for users.
2424

25-
If `enableClaims` config option is used along with `userProfile` you will find custom claims in `state.firebase.profile.token.claims`.
25+
If `enableClaims` config option is used you will find custom claims in `state.firebase.profile.token.claims`.
2626

2727
**Note**: If a claim is added to a user who is already logged in those changes will not necessarily be propagated to the client. In order to assure the change is observed, use a `refreshToken` property in your `userProfile` collection and update it's value after the custom claim has been added. Because `react-redux-firebase` watches for profile changes, the custom claim will be fetched along with the `refreshToken` update.
2828

@@ -309,6 +309,26 @@ props.firebase.verifyPasswordResetCode('some reset code')
309309
310310
[**Promise**][promise-url] - Email associated with reset code
311311
312+
## applyActionCode(code)
313+
314+
Applies action code
315+
316+
Calls Firebase's `firebase.auth().applyActionCode()`. If there is an error, it is added into redux state under `state.firebase.authError`.
317+
318+
##### Examples
319+
320+
```js
321+
props.firebase.applyActionCode('some verification code')
322+
```
323+
324+
##### Parameters
325+
326+
- `code` [**String**][string-url] - Verification code
327+
328+
##### Returns
329+
330+
[**Promise**][promise-url] - Resolves on end
331+
312332
## signInWithPhoneNumber(code)
313333

314334
Signs in using a phone number in an async pattern (i.e. requires calling a second method). Calls Firebase's [`firebase.auth().signInWithPhoneNumber()`](https://firebase.google.com/docs/reference/js/firebase.auth.Auth#signInWithPhoneNumber). If there is an error, it is added into redux state under `state.firebase.authError`.

index.d.ts

+72-48
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type FileOrBlob<T> = T extends File ? File : Blob
2121
export interface InferableComponentEnhancerWithProps<
2222
TInjectedProps,
2323
TNeedsProps
24-
> {
24+
> {
2525
<P extends TInjectedProps>(
2626
component: React.ComponentType<P>
2727
): React.ComponentType<Omit<P, keyof TInjectedProps> & TNeedsProps>
@@ -163,12 +163,12 @@ interface FirebaseDatabaseService {
163163
*/
164164
interface BaseExtendedFirebaseInstance
165165
extends DatabaseTypes.FirebaseDatabase,
166-
FirebaseDatabaseService,
167-
ExtendedAuthInstance,
168-
ExtendedStorageInstance {
166+
FirebaseDatabaseService,
167+
ExtendedAuthInstance,
168+
ExtendedStorageInstance {
169169
initializeAuth: VoidFunction
170170

171-
firestore: () => ExtendedFirestoreInstance
171+
firestore: (() => ExtendedFirestoreInstance) & FirestoreStatics
172172

173173
dispatch: Dispatch
174174

@@ -347,7 +347,7 @@ type OptionalOverride<T, b extends string, P> = b extends keyof T ? P : {};
347347
type OptionalPick<T, b extends string> = Pick<T, b & keyof T>
348348

349349
type ExtendedFirebaseInstance = BaseExtendedFirebaseInstance & OptionalPick<FirebaseNamespace, 'messaging' | 'performance' | 'functions' | 'analytics' | 'remoteConfig'>
350-
350+
351351
/**
352352
* Create an extended firebase instance that has methods attached
353353
* which dispatch redux actions.
@@ -381,12 +381,12 @@ export type QueryParamOptions = QueryParamOption | string[]
381381
export interface ReactReduxFirebaseQuerySetting {
382382
path: string
383383
type?:
384-
| 'value'
385-
| 'once'
386-
| 'child_added'
387-
| 'child_removed'
388-
| 'child_changed'
389-
| 'child_moved'
384+
| 'value'
385+
| 'once'
386+
| 'child_added'
387+
| 'child_removed'
388+
| 'child_changed'
389+
| 'child_moved'
390390
queryParams?: QueryParamOptions
391391
storeAs?: string
392392
}
@@ -479,8 +479,7 @@ export type ReduxFirestoreQueriesFunction = (
479479
* @see https://github.com/prescottprue/redux-firestore#api
480480
*/
481481
interface ExtendedFirestoreInstance
482-
extends FirestoreTypes.FirebaseFirestore,
483-
FirestoreStatics {
482+
extends FirestoreTypes.FirebaseFirestore {
484483
/**
485484
* Get data from firestore.
486485
* @see https://github.com/prescottprue/redux-firestore#get
@@ -596,19 +595,19 @@ interface CreateUserCredentials {
596595
type Credentials =
597596
| CreateUserCredentials
598597
| {
599-
provider: 'facebook' | 'google' | 'twitter' | 'github' | 'microsoft.com' | 'apple.com' | 'yahoo.com'
600-
type: 'popup' | 'redirect'
601-
scopes?: string[]
602-
}
598+
provider: 'facebook' | 'google' | 'twitter' | 'github' | 'microsoft.com' | 'apple.com' | 'yahoo.com'
599+
type: 'popup' | 'redirect'
600+
scopes?: string[]
601+
}
603602
| AuthTypes.AuthCredential
604603
| {
605-
token: string
606-
profile: Object
607-
}
604+
token: string
605+
profile: Object
606+
}
608607
| {
609-
phoneNumber: string
610-
applicationVerifier: AuthTypes.ApplicationVerifier
611-
}
608+
phoneNumber: string
609+
applicationVerifier: AuthTypes.ApplicationVerifier
610+
}
612611

613612
type UserProfile<P extends object = {}> = P
614613

@@ -670,6 +669,9 @@ interface ExtendedAuthInstance {
670669
// https://react-redux-firebase.com/docs/auth.html#verifypasswordresetcodecode
671670
verifyPasswordResetCode: AuthTypes.FirebaseAuth['verifyPasswordResetCode']
672671

672+
// https://react-redux-firebase.com/docs/auth.html#applyactioncode
673+
applyActionCode: AuthTypes.FirebaseAuth['applyActionCode']
674+
673675
/**
674676
* Signs in using a phone number in an async pattern (i.e. requires calling a second method).
675677
* @param phoneNumber - Update to be auth object
@@ -802,25 +804,25 @@ interface ExtendedStorageInstance {
802804
*/
803805
export interface UploadFileOptions<T extends File | Blob> {
804806
name?:
805-
| string
806-
| ((
807-
file: FileOrBlob<T>,
808-
internalFirebase: WithFirebaseProps<ProfileType>['firebase'],
809-
uploadConfig: {
810-
path: string
811-
file: FileOrBlob<T>
812-
dbPath?: string
813-
options?: UploadFileOptions<T>
814-
}
815-
) => string)
807+
| string
808+
| ((
809+
file: FileOrBlob<T>,
810+
internalFirebase: WithFirebaseProps<ProfileType>['firebase'],
811+
uploadConfig: {
812+
path: string
813+
file: FileOrBlob<T>
814+
dbPath?: string
815+
options?: UploadFileOptions<T>
816+
}
817+
) => string)
816818
documentId?:
817-
| string
818-
| ((
819-
uploadRes: StorageTypes.UploadTaskSnapshot,
820-
firebase: WithFirebaseProps<ProfileType>['firebase'],
821-
metadata: StorageTypes.UploadTaskSnapshot['metadata'],
822-
downloadURL: string
823-
) => string)
819+
| string
820+
| ((
821+
uploadRes: StorageTypes.UploadTaskSnapshot,
822+
firebase: WithFirebaseProps<ProfileType>['firebase'],
823+
metadata: StorageTypes.UploadTaskSnapshot['metadata'],
824+
downloadURL: string
825+
) => string)
824826
useSetForMetadata?: boolean
825827
metadata?: StorageTypes.UploadMetadata
826828
metadataFactory?: (
@@ -1046,7 +1048,7 @@ interface ReactReduxFirebaseConfig {
10461048
enableRedirectHandling: boolean
10471049
firebaseStateName: string
10481050
logErrors: boolean
1049-
onAuthStateChanged: (user: AuthTypes.User | null) => void
1051+
onAuthStateChanged: (user: AuthTypes.User | null, _firebase: any, dispatch: Dispatch) => void
10501052
presence: any
10511053
preserveOnEmptyAuthChange: any
10521054
preserveOnLogout: any
@@ -1095,8 +1097,8 @@ export interface ReduxFirestoreConfig {
10951097

10961098
// https://github.com/prescottprue/redux-firestore#allowmultiplelisteners
10971099
allowMultipleListeners:
1098-
| ((listenerToAttach: any, currentListeners: any) => boolean)
1099-
| boolean
1100+
| ((listenerToAttach: any, currentListeners: any) => boolean)
1101+
| boolean
11001102

11011103
// https://github.com/prescottprue/redux-firestore#preserveondelete
11021104
preserveOnDelete: null | object
@@ -1106,8 +1108,8 @@ export interface ReduxFirestoreConfig {
11061108

11071109
// https://github.com/prescottprue/redux-firestore#onattemptcollectiondelete
11081110
onAttemptCollectionDelete:
1109-
| null
1110-
| ((queryOption: any, dispatch: any, firebase: any) => void)
1111+
| null
1112+
| ((queryOption: any, dispatch: any, firebase: any) => void)
11111113

11121114
// https://github.com/prescottprue/redux-firestore#mergeordered
11131115
mergeOrdered: boolean
@@ -1200,7 +1202,7 @@ export namespace FirebaseReducer {
12001202
export interface Reducer<
12011203
ProfileType extends Record<string, any> = {},
12021204
Schema extends Record<string, any> = {}
1203-
> {
1205+
> {
12041206
auth: AuthState
12051207
profile: Profile<ProfileType>
12061208
authError: any
@@ -1240,6 +1242,28 @@ export namespace FirebaseReducer {
12401242
export type Profile<ProfileType> = {
12411243
isLoaded: boolean
12421244
isEmpty: boolean
1245+
token?: {
1246+
token: string
1247+
expirationTime: string
1248+
authTime: string
1249+
issuedAtTime: string
1250+
signInProvider: string
1251+
signInSecondFactor: any
1252+
claims: {
1253+
name: string
1254+
picture: string
1255+
iss: string
1256+
aud: string
1257+
auth_time: number
1258+
user_id: string
1259+
sub: string
1260+
iat: number
1261+
exp: number
1262+
email: string
1263+
email_verified: boolean
1264+
[key: string]: any
1265+
};
1266+
}
12431267
} & ProfileType
12441268

12451269
export namespace firebaseStateReducer {

src/actions/auth.js

+35-4
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,16 @@ export const watchUserProfile = (dispatch, firebase) => {
264264
'Real Time Database or Firestore must be included to enable user profile'
265265
)
266266
}
267+
} else if (enableClaims) {
268+
firebase._.profileWatch = firebase
269+
.auth()
270+
.currentUser.getIdTokenResult(true)
271+
.then((token) => {
272+
dispatch({
273+
type: actionTypes.SET_PROFILE,
274+
profile: { token }
275+
})
276+
})
267277
}
268278
}
269279

@@ -738,10 +748,10 @@ export const resetPassword = (dispatch, firebase, email) => {
738748
if (err) {
739749
switch (err.code) {
740750
case 'auth/user-not-found':
741-
dispatchLoginError(
742-
dispatch,
743-
new Error('The specified user account does not exist.')
744-
)
751+
dispatchLoginError(dispatch, {
752+
...err,
753+
message: 'The specified user account does not exist.'
754+
})
745755
break
746756
default:
747757
dispatchLoginError(dispatch, err)
@@ -821,6 +831,27 @@ export const verifyPasswordResetCode = (dispatch, firebase, code) => {
821831
})
822832
}
823833

834+
/**
835+
* Apply a verification code sent via email or other mechanism
836+
* @param {Function} dispatch - Action dispatch function
837+
* @param {object} firebase - Internal firebase object
838+
* @param {string} code - Verification code
839+
* @returns {Promise} Resolves after applying verification code
840+
* @private
841+
*/
842+
export const applyActionCode = (dispatch, firebase, code) => {
843+
dispatchLoginError(dispatch, null)
844+
return firebase
845+
.auth()
846+
.applyActionCode(code)
847+
.catch((err) => {
848+
if (err) {
849+
dispatchLoginError(dispatch, err)
850+
}
851+
return Promise.reject(err)
852+
})
853+
}
854+
824855
/**
825856
* Update user profile
826857
* @param {Function} dispatch - Action dispatch function

src/createFirebaseInstance.js

+10
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,15 @@ export default function createFirebaseInstance(firebase, configs, dispatch) {
456456
const verifyPasswordResetCode = (code) =>
457457
authActions.verifyPasswordResetCode(dispatch, firebase, code)
458458

459+
/**
460+
* Apply verification code
461+
* @param {string} code - Verification code
462+
* @returns {Promise} Resolves on success
463+
* @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#applyactioncode
464+
*/
465+
const applyActionCode = (code) =>
466+
authActions.applyActionCode(dispatch, firebase, code)
467+
459468
/**
460469
* Update user profile on Firebase Real Time Database or
461470
* Firestore (if `useFirestoreForProfile: true` config included).
@@ -584,6 +593,7 @@ export default function createFirebaseInstance(firebase, configs, dispatch) {
584593
resetPassword,
585594
confirmPasswordReset,
586595
verifyPasswordResetCode,
596+
applyActionCode,
587597
watchEvent,
588598
unWatchEvent,
589599
reloadAuth,

test/unit/actions/auth.spec.js

+35-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import {
1818
updateEmail,
1919
resetPassword,
2020
confirmPasswordReset,
21-
verifyPasswordResetCode
21+
verifyPasswordResetCode,
22+
applyActionCode
2223
} from 'actions/auth'
2324
import { cloneDeep } from 'lodash'
2425
import { actionTypes } from 'constants' // eslint-disable-line node/no-deprecated-api
@@ -203,6 +204,23 @@ describe('Actions: Auth -', () => {
203204
expect(firebase._.profileWatch).to.be.a.function
204205
})
205206

207+
it('for only the custom claims token', () => {
208+
const fb = firebaseWithConfig({ userProfile: null, enableClaims: true })
209+
fb.auth = () => ({
210+
currentUser: {
211+
getIdTokenResult: (bool) => ({
212+
then: (func) => func('testToken')
213+
})
214+
}
215+
})
216+
watchUserProfile(functionSpy, fb)
217+
expect(firebase._.profileWatch).to.be.a.function
218+
expect(functionSpy).to.be.calledWith({
219+
type: actionTypes.SET_PROFILE,
220+
profile: { token: 'testToken' }
221+
})
222+
})
223+
206224
describe('populates -', () => {
207225
it('skips populating data into profile by default', () => {
208226
firebase._.config.profileParamsToPopulate = 'role:roles'
@@ -592,6 +610,22 @@ describe('Actions: Auth -', () => {
592610
})
593611
})
594612

613+
describe('applyActionCode', () => {
614+
it('resolves for valid code', async () => {
615+
res = await applyActionCode(dispatch, fakeFirebase, 'test')
616+
// "success" indicates successful pas through of stub function
617+
expect(res).to.equal('success')
618+
})
619+
620+
it('throws for invalid reset code', async () => {
621+
try {
622+
res = await applyActionCode(dispatch, fakeFirebase, 'error')
623+
} catch (err) {
624+
expect(err.code).to.be.a.string
625+
}
626+
})
627+
})
628+
595629
describe('updateProfile', () => {
596630
it('dispatches PROFILE_UPDATE_START with profile', async () => {
597631
const payload = null

0 commit comments

Comments
 (0)