-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(react): add useSocialLogin hook
- Loading branch information
Showing
3 changed files
with
326 additions
and
0 deletions.
There are no files selected for viewing
86 changes: 86 additions & 0 deletions
86
packages/react/src/authentication/hooks/__tests__/useSocialLogin.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import { cleanup, renderHook } from '@testing-library/react'; | ||
import { postAccountLink, postSocialLogin } from '@farfetch/blackout-client'; | ||
import { useSocialLogin } from '../index.js'; | ||
|
||
const mockLoginData = { | ||
provider: 'Google', | ||
socialAccessToken: 'xxx-xxx-xxx-xxx', | ||
rememberMe: true, | ||
countryCode: 'PT', | ||
}; | ||
|
||
const mockAccountLinkData = { | ||
username: 'xxxxxxxx', | ||
password: 'xxxxxxxx', | ||
}; | ||
|
||
jest.mock('@farfetch/blackout-client', () => { | ||
const original = jest.requireActual('@farfetch/blackout-client'); | ||
|
||
return { | ||
...original, | ||
postSocialLogin: jest.fn(), | ||
postAccountLink: jest.fn(), | ||
}; | ||
}); | ||
|
||
const genericMock = { | ||
actions: { | ||
accountLink: expect.any(Function), | ||
login: expect.any(Function), | ||
}, | ||
data: undefined, | ||
error: undefined, | ||
isLoading: undefined, | ||
}; | ||
|
||
describe('useSocialLogin', () => { | ||
beforeEach(jest.clearAllMocks); | ||
|
||
afterEach(cleanup); | ||
|
||
it('should return correctly with initial state and call all hook dependencies with the correct options', () => { | ||
const { | ||
result: { current }, | ||
} = renderHook(() => useSocialLogin(), {}); | ||
|
||
expect(current).toStrictEqual(genericMock); | ||
|
||
expect(postSocialLogin).not.toHaveBeenCalled(); | ||
expect(postAccountLink).not.toHaveBeenCalled(); | ||
}); | ||
|
||
describe('actions', () => { | ||
describe('login', () => { | ||
it('should call `useSocialLogin` login action', async () => { | ||
const { | ||
result: { | ||
current: { | ||
actions: { login }, | ||
}, | ||
}, | ||
} = renderHook(() => useSocialLogin(), {}); | ||
|
||
await login(mockLoginData); | ||
|
||
expect(postSocialLogin).toHaveBeenCalledWith(); | ||
}); | ||
}); | ||
|
||
describe('accountLink', () => { | ||
it('should call `useSocialLogin` accountLink action', async () => { | ||
const { | ||
result: { | ||
current: { | ||
actions: { accountLink }, | ||
}, | ||
}, | ||
} = renderHook(() => useSocialLogin(), {}); | ||
|
||
await accountLink(mockAccountLinkData); | ||
|
||
expect(postSocialLogin).toHaveBeenCalledWith(); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export { default as useAuthentication } from './useAuthentication.js'; | ||
export { default as useUserProfile } from './useUserProfile.js'; | ||
export { default as useSocialLogin } from './useSocialLogin.js'; |
239 changes: 239 additions & 0 deletions
239
packages/react/src/authentication/hooks/useSocialLogin.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,239 @@ | ||
import { | ||
type BlackoutError, | ||
type Config, | ||
postAccountLink, | ||
type PostAccountLinkData, | ||
postSocialLogin, | ||
type PostSocialLoginData, | ||
} from '@farfetch/blackout-client'; | ||
import { useCallback, useReducer, useRef } from 'react'; | ||
|
||
const actionTypes = { | ||
PostUserSocialLoginRequest: 'POST_USER_SOCIAL_LOGIN_REQUEST', | ||
PostUserSocialLoginSuccess: 'POST_USER_SOCIAL_LOGIN_SUCCESS', | ||
PostUserSocialLoginFailure: 'POST_USER_SOCIAL_LOGIN_FAILURE', | ||
PostAccountLinkRequest: 'POST_ACCOUNT_LINK_REQUEST', | ||
PostAccountLinkSuccess: 'POST_ACCOUNT_LINK_SUCCESS', | ||
PostAccountLinkFailure: 'POST_ACCOUNT_LINK_FAILURE', | ||
} as const; | ||
|
||
type State = { | ||
isLoading: boolean; | ||
error: BlackoutError | null; | ||
result?: object; | ||
currentRequestId: number; | ||
} | null; | ||
|
||
type FetchActionBase = { | ||
meta: { | ||
requestId: number; | ||
}; | ||
}; | ||
|
||
type PostUserSocialLoginRequestAction = FetchActionBase & { | ||
type: typeof actionTypes.PostUserSocialLoginRequest; | ||
}; | ||
|
||
type PostUserSocialLoginSuccessAction = FetchActionBase & { | ||
type: typeof actionTypes.PostUserSocialLoginSuccess; | ||
payload: object; | ||
}; | ||
|
||
type PostUserSocialLoginFailureAction = FetchActionBase & { | ||
type: typeof actionTypes.PostUserSocialLoginFailure; | ||
payload: BlackoutError; | ||
}; | ||
|
||
type PostAccountLinkRequestAction = FetchActionBase & { | ||
type: typeof actionTypes.PostAccountLinkRequest; | ||
}; | ||
|
||
type PostAccountLinkSuccessAction = FetchActionBase & { | ||
type: typeof actionTypes.PostAccountLinkSuccess; | ||
payload: object; | ||
}; | ||
|
||
type PostAccountLinkFailureAction = FetchActionBase & { | ||
type: typeof actionTypes.PostAccountLinkFailure; | ||
payload: BlackoutError; | ||
}; | ||
|
||
type Action = | ||
| PostUserSocialLoginFailureAction | ||
| PostUserSocialLoginRequestAction | ||
| PostUserSocialLoginSuccessAction | ||
| PostAccountLinkFailureAction | ||
| PostAccountLinkRequestAction | ||
| PostAccountLinkSuccessAction; | ||
|
||
const initialState: State = null; | ||
|
||
function reducer(state: State, action: Action) { | ||
switch (action.type) { | ||
case actionTypes.PostUserSocialLoginRequest: { | ||
const newState = { | ||
isLoading: true, | ||
currentRequestId: action.meta.requestId, | ||
error: null, | ||
result: state?.result, | ||
}; | ||
|
||
return newState; | ||
} | ||
case actionTypes.PostUserSocialLoginSuccess: { | ||
if (!state || state.currentRequestId !== action.meta.requestId) { | ||
return state; | ||
} | ||
|
||
const newState = { | ||
...state, | ||
isLoading: false, | ||
result: action.payload, | ||
}; | ||
|
||
return newState; | ||
} | ||
case actionTypes.PostUserSocialLoginFailure: { | ||
if (!state || state.currentRequestId !== action.meta.requestId) { | ||
return state; | ||
} | ||
|
||
const newState = { | ||
...state, | ||
isLoading: false, | ||
error: action.payload, | ||
}; | ||
|
||
return newState; | ||
} | ||
case actionTypes.PostAccountLinkRequest: { | ||
const newState = { | ||
isLoading: true, | ||
currentRequestId: action.meta.requestId, | ||
error: null, | ||
result: state?.result, | ||
}; | ||
|
||
return newState; | ||
} | ||
case actionTypes.PostAccountLinkSuccess: { | ||
if (!state || state.currentRequestId !== action.meta.requestId) { | ||
return state; | ||
} | ||
|
||
const newState = { | ||
...state, | ||
isLoading: false, | ||
result: action.payload, | ||
}; | ||
|
||
return newState; | ||
} | ||
case actionTypes.PostAccountLinkFailure: { | ||
if (!state || state.currentRequestId !== action.meta.requestId) { | ||
return state; | ||
} | ||
|
||
const newState = { | ||
...state, | ||
isLoading: false, | ||
error: action.payload, | ||
}; | ||
|
||
return newState; | ||
} | ||
default: | ||
return state; | ||
} | ||
} | ||
|
||
function useSocialLogin() { | ||
const currentRequestId = useRef(0); | ||
const [state, dispatch] = useReducer(reducer, initialState); | ||
const isLoading = state?.isLoading; | ||
const error = state?.error; | ||
const data = state?.result; | ||
const login = useCallback( | ||
async (data: PostSocialLoginData, config?: Config) => { | ||
if (!data) { | ||
return Promise.reject(new Error('No data was specified.')); | ||
} | ||
|
||
const requestId = currentRequestId.current++; | ||
const actionMetadata = { requestId }; | ||
|
||
dispatch({ | ||
type: actionTypes.PostUserSocialLoginRequest, | ||
meta: actionMetadata, | ||
}); | ||
|
||
return await postSocialLogin(data, config).then( | ||
socialLogin => { | ||
dispatch({ | ||
type: actionTypes.PostUserSocialLoginSuccess, | ||
meta: actionMetadata, | ||
payload: socialLogin, | ||
}); | ||
|
||
return socialLogin; | ||
}, | ||
e => { | ||
dispatch({ | ||
type: actionTypes.PostUserSocialLoginFailure, | ||
meta: actionMetadata, | ||
payload: e, | ||
}); | ||
}, | ||
); | ||
}, | ||
[], | ||
); | ||
|
||
const accountLink = useCallback( | ||
async (data: PostAccountLinkData, config?: Config) => { | ||
if (!data) { | ||
return Promise.reject(new Error('No data was specified.')); | ||
} | ||
|
||
const requestId = currentRequestId.current++; | ||
const actionMetadata = { requestId }; | ||
|
||
dispatch({ | ||
type: actionTypes.PostUserSocialLoginRequest, | ||
meta: actionMetadata, | ||
}); | ||
|
||
return await postAccountLink(data, config).then( | ||
accountlink => { | ||
dispatch({ | ||
type: actionTypes.PostUserSocialLoginSuccess, | ||
meta: actionMetadata, | ||
payload: accountlink, | ||
}); | ||
|
||
return accountlink; | ||
}, | ||
e => { | ||
dispatch({ | ||
type: actionTypes.PostUserSocialLoginFailure, | ||
meta: actionMetadata, | ||
payload: e, | ||
}); | ||
}, | ||
); | ||
}, | ||
[], | ||
); | ||
|
||
return { | ||
isLoading, | ||
error, | ||
data, | ||
actions: { | ||
login, | ||
accountLink, | ||
}, | ||
}; | ||
} | ||
|
||
export default useSocialLogin; |