Skip to content

Commit

Permalink
feat(react): add useSocialLogin hook
Browse files Browse the repository at this point in the history
  • Loading branch information
jpklzm committed Aug 4, 2023
1 parent f3df72a commit c93e1dd
Show file tree
Hide file tree
Showing 3 changed files with 326 additions and 0 deletions.
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();
});
});
});
});
1 change: 1 addition & 0 deletions packages/react/src/authentication/hooks/index.ts
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 packages/react/src/authentication/hooks/useSocialLogin.ts
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;

0 comments on commit c93e1dd

Please sign in to comment.