import api from '../api'; import type { AppDispatch, RootState } from 'soapbox/store'; /** * LocalStorage 'soapbox:verification' * * { * token: String, * challenges: { * email: Number (0 = incomplete, 1 = complete), * sms: Number, * age: Number * } * } */ const LOCAL_STORAGE_VERIFICATION_KEY = 'soapbox:verification'; const PEPE_FETCH_INSTANCE_SUCCESS = 'PEPE_FETCH_INSTANCE_SUCCESS'; const FETCH_CHALLENGES_SUCCESS = 'FETCH_CHALLENGES_SUCCESS'; const FETCH_TOKEN_SUCCESS = 'FETCH_TOKEN_SUCCESS'; const SET_NEXT_CHALLENGE = 'SET_NEXT_CHALLENGE'; const SET_CHALLENGES_COMPLETE = 'SET_CHALLENGES_COMPLETE'; const SET_LOADING = 'SET_LOADING'; const EMAIL: Challenge = 'email'; const SMS: Challenge = 'sms'; const AGE: Challenge = 'age'; export type Challenge = 'age' | 'sms' | 'email' type Challenges = { email?: 0 | 1, sms?: 0 | 1, age?: 0 | 1, } type Verification = { token?: string, challenges?: Challenges, challengeTypes?: Array<'age' | 'sms' | 'email'> }; /** * Fetch the state of the user's verification in local storage. */ const fetchStoredVerification = (): Verification | null => { try { return JSON.parse(localStorage.getItem(LOCAL_STORAGE_VERIFICATION_KEY) as string); } catch { return null; } }; /** * Remove the state of the user's verification from local storage. */ const removeStoredVerification = () => { localStorage.removeItem(LOCAL_STORAGE_VERIFICATION_KEY); }; /** * Fetch and return the Registration token for Pepe. */ const fetchStoredToken = () => { try { const verification: Verification | null = fetchStoredVerification(); return verification!.token; } catch { return null; } }; /** * Fetch and return the state of the verification challenges. */ const fetchStoredChallenges = () => { try { const verification: Verification | null = fetchStoredVerification(); return verification!.challenges; } catch { return null; } }; /** * Fetch and return the state of the verification challenge types. */ const fetchStoredChallengeTypes = () => { try { const verification: Verification | null = fetchStoredVerification(); return verification!.challengeTypes; } catch { return null; } }; /** * Update the verification object in local storage. * * @param {*} verification object */ const updateStorage = ({ ...updatedVerification }: Verification) => { const verification = fetchStoredVerification(); localStorage.setItem( LOCAL_STORAGE_VERIFICATION_KEY, JSON.stringify({ ...verification, ...updatedVerification }), ); }; /** * Fetch Pepe challenges and registration token */ const fetchVerificationConfig = () => async(dispatch: AppDispatch) => { await dispatch(fetchPepeInstance()); dispatch(fetchRegistrationToken()); }; /** * Save the challenges in localStorage. * * - If the API removes a challenge after the client has stored it, remove that * challenge from localStorage. * - If the API adds a challenge after the client has stored it, add that * challenge to localStorage. * - Don't overwrite a challenge that has already been completed. * - Update localStorage to the new set of challenges. */ function saveChallenges(challenges: Array<'age' | 'sms' | 'email'>) { const currentChallenges: Challenges = fetchStoredChallenges() || {}; const challengesToRemove = Object.keys(currentChallenges).filter((currentChallenge) => !challenges.includes(currentChallenge as Challenge)) as Challenge[]; challengesToRemove.forEach((challengeToRemove) => delete currentChallenges[challengeToRemove]); for (let i = 0; i < challenges.length; i++) { const challengeName = challenges[i]; if (typeof currentChallenges[challengeName] !== 'number') { currentChallenges[challengeName] = 0; } } updateStorage({ challenges: currentChallenges, challengeTypes: challenges, }); } /** * Finish a challenge. */ function finishChallenge(challenge: Challenge) { const currentChallenges: Challenges = fetchStoredChallenges() || {}; // Set challenge to "complete" currentChallenges[challenge] = 1; updateStorage({ challenges: currentChallenges }); } /** * Fetch the next challenge */ const fetchNextChallenge = (): Challenge => { const currentChallenges: Challenges = fetchStoredChallenges() || {}; return Object.keys(currentChallenges).find((challenge) => currentChallenges[challenge as Challenge] === 0) as Challenge; }; /** * Dispatch the next challenge or set to complete if all challenges are completed. */ const dispatchNextChallenge = (dispatch: AppDispatch) => { const nextChallenge = fetchNextChallenge(); if (nextChallenge) { dispatch({ type: SET_NEXT_CHALLENGE, challenge: nextChallenge }); } else { dispatch({ type: SET_CHALLENGES_COMPLETE }); } }; /** * Fetch the challenges and age mininum from Pepe */ const fetchPepeInstance = () => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: SET_LOADING }); return api(getState).get('/api/v1/pepe/instance').then(response => { const { challenges, age_minimum: ageMinimum } = response.data; saveChallenges(challenges); const currentChallenge = fetchNextChallenge(); dispatch({ type: PEPE_FETCH_INSTANCE_SUCCESS, instance: { isReady: true, ...response.data } }); dispatch({ type: FETCH_CHALLENGES_SUCCESS, ageMinimum, currentChallenge, isComplete: !currentChallenge, }); }) .finally(() => dispatch({ type: SET_LOADING, value: false })); }; /** * Fetch the regristration token from Pepe unless it's already been stored locally */ const fetchRegistrationToken = () => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: SET_LOADING }); const token = fetchStoredToken(); if (token) { dispatch({ type: FETCH_TOKEN_SUCCESS, value: token, }); return null; } return api(getState).post('/api/v1/pepe/registrations') .then(response => { updateStorage({ token: response.data.access_token }); return dispatch({ type: FETCH_TOKEN_SUCCESS, value: response.data.access_token, }); }) .finally(() => dispatch({ type: SET_LOADING, value: false })); }; const checkEmailAvailability = (email: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: SET_LOADING }); const token = fetchStoredToken(); return api(getState).get(`/api/v1/pepe/account/exists?email=${email}`, { headers: { Authorization: `Bearer ${token}` }, }) .catch(() => {}) .then(() => dispatch({ type: SET_LOADING, value: false })); }; /** * Send the user's email to Pepe to request confirmation */ const requestEmailVerification = (email: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: SET_LOADING }); const token = fetchStoredToken(); return api(getState).post('/api/v1/pepe/verify_email/request', { email }, { headers: { Authorization: `Bearer ${token}` }, }) .finally(() => dispatch({ type: SET_LOADING, value: false })); }; const checkEmailVerification = () => (dispatch: AppDispatch, getState: () => RootState) => { const token = fetchStoredToken(); return api(getState).get('/api/v1/pepe/verify_email', { headers: { Authorization: `Bearer ${token}` }, }); }; /** * Confirm the user's email with Pepe */ const confirmEmailVerification = (emailToken: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: SET_LOADING }); const token = fetchStoredToken(); return api(getState).post('/api/v1/pepe/verify_email/confirm', { token: emailToken }, { headers: { Authorization: `Bearer ${token}` }, }) .then((response) => { updateStorageFromEmailConfirmation(dispatch, response.data.token); }) .finally(() => dispatch({ type: SET_LOADING, value: false })); }; const updateStorageFromEmailConfirmation = (dispatch: AppDispatch, token: string) => { const challengeTypes = fetchStoredChallengeTypes(); if (!challengeTypes) { return; } const indexOfEmail = challengeTypes.indexOf('email'); const challenges: Challenges = {}; challengeTypes?.forEach((challengeType, idx) => { const value = idx <= indexOfEmail ? 1 : 0; challenges[challengeType] = value; }); updateStorage({ token, challengeTypes, challenges }); dispatchNextChallenge(dispatch); }; const postEmailVerification = () => (dispatch: AppDispatch) => { finishChallenge(EMAIL); dispatchNextChallenge(dispatch); }; /** * Send the user's phone number to Pepe to request confirmation */ const requestPhoneVerification = (phone: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: SET_LOADING }); const token = fetchStoredToken(); return api(getState).post('/api/v1/pepe/verify_sms/request', { phone }, { headers: { Authorization: `Bearer ${token}` }, }) .finally(() => dispatch({ type: SET_LOADING, value: false })); }; /** * Send the user's phone number to Pepe to re-request confirmation */ const reRequestPhoneVerification = (phone: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: SET_LOADING }); return api(getState).post('/api/v1/pepe/reverify_sms/request', { phone }) .finally(() => dispatch({ type: SET_LOADING, value: false })); }; /** * Confirm the user's phone number with Pepe */ const confirmPhoneVerification = (code: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: SET_LOADING }); const token = fetchStoredToken(); return api(getState).post('/api/v1/pepe/verify_sms/confirm', { code }, { headers: { Authorization: `Bearer ${token}` }, }) .then(() => { finishChallenge(SMS); dispatchNextChallenge(dispatch); }) .finally(() => dispatch({ type: SET_LOADING, value: false })); }; /** * Re-Confirm the user's phone number with Pepe */ const reConfirmPhoneVerification = (code: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: SET_LOADING }); return api(getState).post('/api/v1/pepe/reverify_sms/confirm', { code }) .finally(() => dispatch({ type: SET_LOADING, value: false })); }; /** * Confirm the user's age with Pepe */ const verifyAge = (birthday: Date) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: SET_LOADING }); const token = fetchStoredToken(); return api(getState).post('/api/v1/pepe/verify_age/confirm', { birthday }, { headers: { Authorization: `Bearer ${token}` }, }) .then(() => { finishChallenge(AGE); dispatchNextChallenge(dispatch); }) .finally(() => dispatch({ type: SET_LOADING, value: false })); }; /** * Create the user's account with Pepe */ const createAccount = (username: string, password: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: SET_LOADING }); const token = fetchStoredToken(); return api(getState).post('/api/v1/pepe/accounts', { username, password }, { headers: { Authorization: `Bearer ${token}` }, }).finally(() => dispatch({ type: SET_LOADING, value: false })); }; export { PEPE_FETCH_INSTANCE_SUCCESS, FETCH_CHALLENGES_SUCCESS, FETCH_TOKEN_SUCCESS, LOCAL_STORAGE_VERIFICATION_KEY, SET_CHALLENGES_COMPLETE, SET_LOADING, SET_NEXT_CHALLENGE, checkEmailAvailability, confirmEmailVerification, confirmPhoneVerification, createAccount, fetchStoredChallenges, fetchVerificationConfig, fetchRegistrationToken, removeStoredVerification, requestEmailVerification, checkEmailVerification, postEmailVerification, reConfirmPhoneVerification, requestPhoneVerification, reRequestPhoneVerification, verifyAge, };