Skip to content

Commit

Permalink
v3.8.0 (#1025)
Browse files Browse the repository at this point in the history
* 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 <rscotten@users.noreply.github.com>
Co-authored-by: Alexander Arvidsson <alexander@arvidson.nu>
Co-authored-by: Davi Aquino <djejaquino@users.noreply.github.com>
Co-authored-by: Cyrille Corpet <cyrille@corpet.net>
Co-authored-by: Anton Nesterov <anton@nesterov.cc>
  • Loading branch information
6 people authored Nov 18, 2020
1 parent dbea9e4 commit bce9510
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 58 deletions.
22 changes: 21 additions & 1 deletion docs/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ If you need access to methods that are not available at the top level, you can a

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.

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

**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.

Expand Down Expand Up @@ -309,6 +309,26 @@ props.firebase.verifyPasswordResetCode('some reset code')
[**Promise**][promise-url] - Email associated with reset code
## applyActionCode(code)
Applies action code
Calls Firebase's `firebase.auth().applyActionCode()`. If there is an error, it is added into redux state under `state.firebase.authError`.

##### Examples

```js
props.firebase.applyActionCode('some verification code')
```

##### Parameters

- `code` [**String**][string-url] - Verification code

##### Returns

[**Promise**][promise-url] - Resolves on end

## signInWithPhoneNumber(code)

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`.
Expand Down
120 changes: 72 additions & 48 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type FileOrBlob<T> = T extends File ? File : Blob
export interface InferableComponentEnhancerWithProps<
TInjectedProps,
TNeedsProps
> {
> {
<P extends TInjectedProps>(
component: React.ComponentType<P>
): React.ComponentType<Omit<P, keyof TInjectedProps> & TNeedsProps>
Expand Down Expand Up @@ -163,12 +163,12 @@ interface FirebaseDatabaseService {
*/
interface BaseExtendedFirebaseInstance
extends DatabaseTypes.FirebaseDatabase,
FirebaseDatabaseService,
ExtendedAuthInstance,
ExtendedStorageInstance {
FirebaseDatabaseService,
ExtendedAuthInstance,
ExtendedStorageInstance {
initializeAuth: VoidFunction

firestore: () => ExtendedFirestoreInstance
firestore: (() => ExtendedFirestoreInstance) & FirestoreStatics

dispatch: Dispatch

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

type ExtendedFirebaseInstance = BaseExtendedFirebaseInstance & OptionalPick<FirebaseNamespace, 'messaging' | 'performance' | 'functions' | 'analytics' | 'remoteConfig'>

/**
* Create an extended firebase instance that has methods attached
* which dispatch redux actions.
Expand Down Expand Up @@ -381,12 +381,12 @@ export type QueryParamOptions = QueryParamOption | string[]
export interface ReactReduxFirebaseQuerySetting {
path: string
type?:
| 'value'
| 'once'
| 'child_added'
| 'child_removed'
| 'child_changed'
| 'child_moved'
| 'value'
| 'once'
| 'child_added'
| 'child_removed'
| 'child_changed'
| 'child_moved'
queryParams?: QueryParamOptions
storeAs?: string
}
Expand Down Expand Up @@ -479,8 +479,7 @@ export type ReduxFirestoreQueriesFunction = (
* @see https://github.com/prescottprue/redux-firestore#api
*/
interface ExtendedFirestoreInstance
extends FirestoreTypes.FirebaseFirestore,
FirestoreStatics {
extends FirestoreTypes.FirebaseFirestore {
/**
* Get data from firestore.
* @see https://github.com/prescottprue/redux-firestore#get
Expand Down Expand Up @@ -596,19 +595,19 @@ interface CreateUserCredentials {
type Credentials =
| CreateUserCredentials
| {
provider: 'facebook' | 'google' | 'twitter' | 'github' | 'microsoft.com' | 'apple.com' | 'yahoo.com'
type: 'popup' | 'redirect'
scopes?: string[]
}
provider: 'facebook' | 'google' | 'twitter' | 'github' | 'microsoft.com' | 'apple.com' | 'yahoo.com'
type: 'popup' | 'redirect'
scopes?: string[]
}
| AuthTypes.AuthCredential
| {
token: string
profile: Object
}
token: string
profile: Object
}
| {
phoneNumber: string
applicationVerifier: AuthTypes.ApplicationVerifier
}
phoneNumber: string
applicationVerifier: AuthTypes.ApplicationVerifier
}

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

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

// https://react-redux-firebase.com/docs/auth.html#applyactioncode
applyActionCode: AuthTypes.FirebaseAuth['applyActionCode']

/**
* Signs in using a phone number in an async pattern (i.e. requires calling a second method).
* @param phoneNumber - Update to be auth object
Expand Down Expand Up @@ -802,25 +804,25 @@ interface ExtendedStorageInstance {
*/
export interface UploadFileOptions<T extends File | Blob> {
name?:
| string
| ((
file: FileOrBlob<T>,
internalFirebase: WithFirebaseProps<ProfileType>['firebase'],
uploadConfig: {
path: string
file: FileOrBlob<T>
dbPath?: string
options?: UploadFileOptions<T>
}
) => string)
| string
| ((
file: FileOrBlob<T>,
internalFirebase: WithFirebaseProps<ProfileType>['firebase'],
uploadConfig: {
path: string
file: FileOrBlob<T>
dbPath?: string
options?: UploadFileOptions<T>
}
) => string)
documentId?:
| string
| ((
uploadRes: StorageTypes.UploadTaskSnapshot,
firebase: WithFirebaseProps<ProfileType>['firebase'],
metadata: StorageTypes.UploadTaskSnapshot['metadata'],
downloadURL: string
) => string)
| string
| ((
uploadRes: StorageTypes.UploadTaskSnapshot,
firebase: WithFirebaseProps<ProfileType>['firebase'],
metadata: StorageTypes.UploadTaskSnapshot['metadata'],
downloadURL: string
) => string)
useSetForMetadata?: boolean
metadata?: StorageTypes.UploadMetadata
metadataFactory?: (
Expand Down Expand Up @@ -1046,7 +1048,7 @@ interface ReactReduxFirebaseConfig {
enableRedirectHandling: boolean
firebaseStateName: string
logErrors: boolean
onAuthStateChanged: (user: AuthTypes.User | null) => void
onAuthStateChanged: (user: AuthTypes.User | null, _firebase: any, dispatch: Dispatch) => void
presence: any
preserveOnEmptyAuthChange: any
preserveOnLogout: any
Expand Down Expand Up @@ -1095,8 +1097,8 @@ export interface ReduxFirestoreConfig {

// https://github.com/prescottprue/redux-firestore#allowmultiplelisteners
allowMultipleListeners:
| ((listenerToAttach: any, currentListeners: any) => boolean)
| boolean
| ((listenerToAttach: any, currentListeners: any) => boolean)
| boolean

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

// https://github.com/prescottprue/redux-firestore#onattemptcollectiondelete
onAttemptCollectionDelete:
| null
| ((queryOption: any, dispatch: any, firebase: any) => void)
| null
| ((queryOption: any, dispatch: any, firebase: any) => void)

// https://github.com/prescottprue/redux-firestore#mergeordered
mergeOrdered: boolean
Expand Down Expand Up @@ -1200,7 +1202,7 @@ export namespace FirebaseReducer {
export interface Reducer<
ProfileType extends Record<string, any> = {},
Schema extends Record<string, any> = {}
> {
> {
auth: AuthState
profile: Profile<ProfileType>
authError: any
Expand Down Expand Up @@ -1240,6 +1242,28 @@ export namespace FirebaseReducer {
export type Profile<ProfileType> = {
isLoaded: boolean
isEmpty: boolean
token?: {
token: string
expirationTime: string
authTime: string
issuedAtTime: string
signInProvider: string
signInSecondFactor: any
claims: {
name: string
picture: string
iss: string
aud: string
auth_time: number
user_id: string
sub: string
iat: number
exp: number
email: string
email_verified: boolean
[key: string]: any
};
}
} & ProfileType

export namespace firebaseStateReducer {
Expand Down
39 changes: 35 additions & 4 deletions src/actions/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,16 @@ export const watchUserProfile = (dispatch, firebase) => {
'Real Time Database or Firestore must be included to enable user profile'
)
}
} else if (enableClaims) {
firebase._.profileWatch = firebase
.auth()
.currentUser.getIdTokenResult(true)
.then((token) => {
dispatch({
type: actionTypes.SET_PROFILE,
profile: { token }
})
})
}
}

Expand Down Expand Up @@ -738,10 +748,10 @@ export const resetPassword = (dispatch, firebase, email) => {
if (err) {
switch (err.code) {
case 'auth/user-not-found':
dispatchLoginError(
dispatch,
new Error('The specified user account does not exist.')
)
dispatchLoginError(dispatch, {
...err,
message: 'The specified user account does not exist.'
})
break
default:
dispatchLoginError(dispatch, err)
Expand Down Expand Up @@ -821,6 +831,27 @@ export const verifyPasswordResetCode = (dispatch, firebase, code) => {
})
}

/**
* Apply a verification code sent via email or other mechanism
* @param {Function} dispatch - Action dispatch function
* @param {object} firebase - Internal firebase object
* @param {string} code - Verification code
* @returns {Promise} Resolves after applying verification code
* @private
*/
export const applyActionCode = (dispatch, firebase, code) => {
dispatchLoginError(dispatch, null)
return firebase
.auth()
.applyActionCode(code)
.catch((err) => {
if (err) {
dispatchLoginError(dispatch, err)
}
return Promise.reject(err)
})
}

/**
* Update user profile
* @param {Function} dispatch - Action dispatch function
Expand Down
10 changes: 10 additions & 0 deletions src/createFirebaseInstance.js
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,15 @@ export default function createFirebaseInstance(firebase, configs, dispatch) {
const verifyPasswordResetCode = (code) =>
authActions.verifyPasswordResetCode(dispatch, firebase, code)

/**
* Apply verification code
* @param {string} code - Verification code
* @returns {Promise} Resolves on success
* @see https://react-redux-firebase.com/docs/api/firebaseInstance.html#applyactioncode
*/
const applyActionCode = (code) =>
authActions.applyActionCode(dispatch, firebase, code)

/**
* Update user profile on Firebase Real Time Database or
* Firestore (if `useFirestoreForProfile: true` config included).
Expand Down Expand Up @@ -584,6 +593,7 @@ export default function createFirebaseInstance(firebase, configs, dispatch) {
resetPassword,
confirmPasswordReset,
verifyPasswordResetCode,
applyActionCode,
watchEvent,
unWatchEvent,
reloadAuth,
Expand Down
36 changes: 35 additions & 1 deletion test/unit/actions/auth.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import {
updateEmail,
resetPassword,
confirmPasswordReset,
verifyPasswordResetCode
verifyPasswordResetCode,
applyActionCode
} from 'actions/auth'
import { cloneDeep } from 'lodash'
import { actionTypes } from 'constants' // eslint-disable-line node/no-deprecated-api
Expand Down Expand Up @@ -203,6 +204,23 @@ describe('Actions: Auth -', () => {
expect(firebase._.profileWatch).to.be.a.function
})

it('for only the custom claims token', () => {
const fb = firebaseWithConfig({ userProfile: null, enableClaims: true })
fb.auth = () => ({
currentUser: {
getIdTokenResult: (bool) => ({
then: (func) => func('testToken')
})
}
})
watchUserProfile(functionSpy, fb)
expect(firebase._.profileWatch).to.be.a.function
expect(functionSpy).to.be.calledWith({
type: actionTypes.SET_PROFILE,
profile: { token: 'testToken' }
})
})

describe('populates -', () => {
it('skips populating data into profile by default', () => {
firebase._.config.profileParamsToPopulate = 'role:roles'
Expand Down Expand Up @@ -592,6 +610,22 @@ describe('Actions: Auth -', () => {
})
})

describe('applyActionCode', () => {
it('resolves for valid code', async () => {
res = await applyActionCode(dispatch, fakeFirebase, 'test')
// "success" indicates successful pas through of stub function
expect(res).to.equal('success')
})

it('throws for invalid reset code', async () => {
try {
res = await applyActionCode(dispatch, fakeFirebase, 'error')
} catch (err) {
expect(err.code).to.be.a.string
}
})
})

describe('updateProfile', () => {
it('dispatches PROFILE_UPDATE_START with profile', async () => {
const payload = null
Expand Down
Loading

0 comments on commit bce9510

Please sign in to comment.