import React, {
createContext,
useContext,
useState,
useEffect,
useReducer,
} from "react";
import {Copy} from "utils";
import {
PATRON_SOCIAL_SIGN_IN,
PATRON_SIGN_IN,
PATRON_VALIDATE,
HANDLE_ERROR,
PATRON_AUTH_PINCODE,
UPDATE_PATRON,
PATRON_SEND_PINCODE_BY_ACCOUNT,
GET_FACEBOOK_USER_ID,
} from "utils/api";
import {GlobalConfig} from "components/providers";
import {usePatronContext} from "components/providers/Patron/PatronContext";
import {ACCOUNT_TYPES, SOCIAL_PLATFORMS} from "utils/constants";
import {getNestedValue, mapSocialAuthData, AUTHSTEPS} from "./utils";
const TypeContext = createContext();
TypeContext.displayName = "Controller1Provider";
const handleError = (e) => {
throw HANDLE_ERROR(e);
};
const stepReducer = (state, {type, payload}) => {
switch (type) {
case "NEXT": {
return [...state, payload];
}
case "PREVIOUS": {
return state.slice(state.length - 1);
}
case "REPLACE": {
const newState = [...state];
newState.pop();
newState.push(payload);
return newState;
}
case "RESET": {
return [-1];
}
default:
return state;
}
};
const checkMissingData = (data) => {
if (!data.phone || !data.phone.isVerified) {
return {
message: Copy.LOGIN_SIGNUP_STATIC.TYPE1_UNVERIFIED_PHONE_MESSAGE,
step: AUTHSTEPS.UPDATE_PHONE,
};
}
if (!data.email || (Object.keys(data.email).length && !data.email.value)) {
return {
message: Copy.LOGIN_SIGNUP_STATIC.TYPE1_NO_EMAIL_MESSAGE,
step: AUTHSTEPS.UPDATE_EMAIL,
};
}
if (data.isMigrating) {
return {step: AUTHSTEPS.UPDATE_PASSWORD};
}
return null;
};
const mapAuthToken = (data, authToken) => {
if (data?.userIdApple) {
return data?.userIdApple;
}
return authToken;
};
const defaultLoginPatron = {
account: "",
accountType: "",
isMigrating: null,
};
/**
* Controller logic for login type 1. Tries to encapsulate as much as it should
*
* @param {object} props
* @param {object} props.children - Render Function or React element
* @param {Function} props.onLoginComplete - Function to run when login completes
* @param {Function} props.onSignupComplete - Function to run when signup completes
* @param {Function} props.onComplete - Function to run when entire process completes
* @param {Function} props.onNextStep - callback to run when the step changes
*/
const Controller1Provider = ({
children,
onLoginComplete,
onSignupComplete,
onComplete,
onNextStep,
}) => {
const {
isAppleSignUp,
appleCredentials,
unsetLastAppleLocation,
isFacebookSignUp,
facebookCredentials,
unsetLastFacebookLocation,
} = useContext(GlobalConfig.GlobalConfigContext);
const {login} = usePatronContext();
const [loginPatron, setLoginPatron] = useState(defaultLoginPatron);
const [message, setMessage] = useState("");
const [patron, setPatron] = useState({});
const [steps, dispatch] = useReducer(stepReducer, []);
const [fetchingSocial, setFetchingSocial] = useState(false);
const reset = () => {
dispatch({type: "RESET"});
setMessage("");
setLoginPatron(defaultLoginPatron);
setPatron({});
};
const onLoginOrSignUpComplete = (data) => {
if (onSignupComplete) {
onSignupComplete();
dispatch({payload: AUTHSTEPS.SIGN_UP_CONFIRMATION, type: "NEXT"});
} else if (onLoginComplete) {
onLoginComplete();
reset();
} else if (onComplete) {
onComplete(data);
reset();
}
};
const valiatePatronData = (data) => {
const isMissingData = checkMissingData(data);
if (isMissingData) {
dispatch({payload: isMissingData.step, type: "REPLACE"});
setMessage(isMissingData.message);
} else {
login(data);
onLoginOrSignUpComplete(data);
}
};
const valiateSocialPatronData = (data) => {
const isMissingData = checkMissingData(data);
if (isMissingData) {
dispatch({payload: AUTHSTEPS.SIGN_UP, type: "REPLACE"});
setMessage(isMissingData.message);
} else {
login(data);
onLoginOrSignUpComplete(data);
}
};
const validatePatron = (account) =>
PATRON_VALIDATE({account})
.then(({data}) => {
setLoginPatron(data);
if (data.isMigrating) {
dispatch({payload: AUTHSTEPS.PINCODE, type: "NEXT"});
setMessage(
Copy.LOGIN_SIGNUP_STATIC.TYPE1_ACCOUNT_VERIFICATION_MESSAGE,
);
return PATRON_SEND_PINCODE_BY_ACCOUNT(data.account);
}
})
.catch(async (err) => {
/**
* 404 meaning there is no account, we push them to sign up
* NOTE: We are explicitly sending the payload as an object with the
* email which is the only information that the user entered in the field
*
* @todo update this with the social login buttons to add the information we
* get back e.g. phone, name
*/
if (err.message.includes(404)) {
dispatch({payload: {email: account}, type: "NEXT"});
} else {
handleError(err);
}
});
const verifyPincode = async (pincode) => {
try {
const {data} = await PATRON_AUTH_PINCODE({
pinCode: pincode,
[loginPatron.accountType]: loginPatron.account,
});
const isMissingData = checkMissingData(data);
setPatron(data);
if (isMissingData) {
dispatch({payload: AUTHSTEPS.UPDATE_PASSWORD, type: "REPLACE"});
setMessage(
Copy.LOGIN_SIGNUP_STATIC.TYPE1_PASSWORD_UPDATE_NOTICE_MESSAGE,
);
} else {
login(data);
onLoginOrSignUpComplete(data);
}
} catch (e) {
handleError(e);
}
};
const onUpdatePhone = () => {
dispatch({payload: AUTHSTEPS.UPDATE_PHONE, type: "NEXT"});
};
const onSocialLogin = (data) => {
const {type, authToken, authData} = mapSocialAuthData(data);
setFetchingSocial(true);
return PATRON_SOCIAL_SIGN_IN({
authData,
authToken,
type,
})
.then(({data}) => {
const mappedData = {
authToken: mapAuthToken(data, authToken),
email: getNestedValue(data, ACCOUNT_TYPES.EMAIL),
firstName: data.firstName,
lastName: data.lastName,
phone: getNestedValue(data, ACCOUNT_TYPES.PHONE),
type,
};
if (data.token) {
setPatron(data);
valiatePatronData(data);
} else {
setPatron(mappedData);
valiateSocialPatronData(data);
}
})
.catch(handleError)
.finally(() => {
setFetchingSocial(false);
});
};
const onLogin = (password) =>
PATRON_SIGN_IN({
account: loginPatron.account,
password,
})
.then(({data}) => {
setPatron(data);
valiatePatronData(data);
})
.catch(handleError);
const onUpdate = (values) =>
UPDATE_PATRON(
values,
patron.token ? {headers: {authorization: patron.token}} : undefined,
)
.then(({data}) => {
valiatePatronData({...patron, ...data});
})
.catch(handleError);
const onSignupSuccess = (data) => {
setPatron(data);
setLoginPatron({
account: getNestedValue(data, ACCOUNT_TYPES.PHONE),
accountType: ACCOUNT_TYPES.PHONE,
isMigrating: false,
});
dispatch({payload: AUTHSTEPS.PINCODE, type: "NEXT"});
};
const onConfirmSignup = () => {
if (onComplete) {
onComplete(patron);
}
};
const onUpdateSuccess = (data) => {
valiatePatronData({...patron, ...data, isMigrating: false});
};
const onForgotPasswordSuccess = (data) => {
valiatePatronData({token: patron.token, ...data});
};
useEffect(() => {
if (isAppleSignUp) {
onSocialLogin({
...appleCredentials,
type: SOCIAL_PLATFORMS.APPLE,
});
unsetLastAppleLocation();
} else if (isFacebookSignUp) {
GET_FACEBOOK_USER_ID(facebookCredentials.accessToken).then(({userID}) => {
onSocialLogin({
...facebookCredentials,
type: SOCIAL_PLATFORMS.FACEBOOK,
userID,
});
});
unsetLastFacebookLocation();
}
}, [isAppleSignUp, isFacebookSignUp]);
useEffect(() => {
if (steps.length) {
onNextStep(steps[steps.length - 1]);
}
}, [steps]);
const contextValues = {
account: loginPatron.account,
accountType: loginPatron.accountType,
afterLogin: onForgotPasswordSuccess,
email: getNestedValue(patron, ACCOUNT_TYPES.EMAIL),
fetchingSocial,
isMigrating: loginPatron.isMigrating,
message,
onConfirmSignup,
onForgotPasswordSuccess,
onLogin,
onSignupSuccess,
onSocialLogin,
onUpdate,
onUpdatePhone,
onUpdateSuccess,
patron,
phone: getNestedValue(patron, ACCOUNT_TYPES.PHONE),
token: patron.token,
validatePatron,
verifyPincode,
};
return (
<TypeContext.Provider>
{typeof children === "function"
? React.Children.only(children(contextValues))
: React.Children.only(children)}
</TypeContext.Provider>
);
};
const useType1Context = () => {
const contextValues = useContext(TypeContext);
if (!contextValues) {
throw new Error("useType1Context is being accessed outside a Type1Context");
}
return contextValues;
};
export {useType1Context};
export default {
Context: TypeContext,
Provider: Controller1Provider,
};