Merge branch 'rm-pepe' into 'main'
Remove Truth Social registration See merge request soapbox-pub/soapbox!2708
This commit is contained in:
commit
73ae9ae4c4
35 changed files with 16 additions and 2622 deletions
|
@ -114,7 +114,6 @@
|
|||
"intl-messageformat-parser": "^6.0.0",
|
||||
"intl-pluralrules": "^1.3.1",
|
||||
"leaflet": "^1.8.0",
|
||||
"libphonenumber-js": "^1.10.8",
|
||||
"line-awesome": "^1.3.0",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.7.11",
|
||||
|
@ -136,7 +135,6 @@
|
|||
"react-inlinesvg": "^3.0.0",
|
||||
"react-intl": "^5.0.0",
|
||||
"react-motion": "^0.5.2",
|
||||
"react-otp-input": "^2.4.0",
|
||||
"react-overlays": "^0.9.0",
|
||||
"react-popper": "^2.3.0",
|
||||
"react-redux": "^8.0.0",
|
||||
|
|
|
@ -1,427 +0,0 @@
|
|||
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,
|
||||
};
|
|
@ -38,7 +38,6 @@ export {
|
|||
MenuList,
|
||||
} from './menu/menu';
|
||||
export { default as Modal } from './modal/modal';
|
||||
export { default as PhoneInput } from './phone-input/phone-input';
|
||||
export { default as Popover } from './popover/popover';
|
||||
export { default as Portal } from './portal/portal';
|
||||
export { default as ProgressBar } from './progress-bar/progress-bar';
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { COUNTRY_CODES, CountryCode } from 'soapbox/utils/phone';
|
||||
|
||||
interface ICountryCodeDropdown {
|
||||
countryCode: CountryCode
|
||||
onChange(countryCode: CountryCode): void
|
||||
}
|
||||
|
||||
/** Dropdown menu to select a country code. */
|
||||
const CountryCodeDropdown: React.FC<ICountryCodeDropdown> = ({ countryCode, onChange }) => {
|
||||
return (
|
||||
<select
|
||||
value={countryCode}
|
||||
className='h-full rounded-md border-transparent bg-transparent py-0 pl-3 pr-7 text-base focus:outline-none focus:ring-primary-500 dark:text-white sm:text-sm'
|
||||
onChange={(event) => onChange(event.target.value as any)}
|
||||
>
|
||||
{COUNTRY_CODES.map((code) => (
|
||||
<option value={code} key={code}>+{code}</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
export default CountryCodeDropdown;
|
|
@ -1,81 +0,0 @@
|
|||
import { parsePhoneNumber, AsYouType } from 'libphonenumber-js';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import { CountryCode } from 'soapbox/utils/phone';
|
||||
|
||||
import Input from '../input/input';
|
||||
|
||||
import CountryCodeDropdown from './country-code-dropdown';
|
||||
|
||||
interface IPhoneInput extends Pick<React.InputHTMLAttributes<HTMLInputElement>, 'required' | 'autoFocus'> {
|
||||
/** E164 phone number. */
|
||||
value?: string
|
||||
/** Change handler which receives the E164 phone string. */
|
||||
onChange?: (phone: string | undefined) => void
|
||||
/** Country code that's selected on mount. */
|
||||
defaultCountryCode?: CountryCode
|
||||
}
|
||||
|
||||
/** Internationalized phone input with country code picker. */
|
||||
const PhoneInput: React.FC<IPhoneInput> = (props) => {
|
||||
const { value, onChange, defaultCountryCode = '1', ...rest } = props;
|
||||
|
||||
const [countryCode, setCountryCode] = useState<CountryCode>(defaultCountryCode);
|
||||
const [nationalNumber, setNationalNumber] = useState<string>('');
|
||||
|
||||
const handleChange: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
|
||||
// HACK: AsYouType is not meant to be used this way. But it works!
|
||||
const asYouType = new AsYouType({ defaultCallingCode: countryCode });
|
||||
const formatted = asYouType.input(target.value);
|
||||
|
||||
// If the new value is the same as before, we might be backspacing,
|
||||
// so use the actual event value instead of the formatted value.
|
||||
if (formatted === nationalNumber && target.value !== nationalNumber) {
|
||||
setNationalNumber(target.value);
|
||||
} else {
|
||||
setNationalNumber(formatted);
|
||||
}
|
||||
};
|
||||
|
||||
// When the internal state changes, update the external state.
|
||||
useEffect(() => {
|
||||
if (onChange) {
|
||||
try {
|
||||
const opts = { defaultCallingCode: countryCode, extract: false } as any;
|
||||
const result = parsePhoneNumber(nationalNumber, opts);
|
||||
|
||||
// Throw if the number is invalid, but catch it below.
|
||||
// We'll only ever call `onChange` with a valid E164 string or `undefined`.
|
||||
if (!result.isPossible()) {
|
||||
throw result;
|
||||
}
|
||||
|
||||
onChange(result.format('E.164'));
|
||||
} catch (e) {
|
||||
// The value returned is always a valid E164 string.
|
||||
// If it's not valid, it'll return undefined.
|
||||
onChange(undefined);
|
||||
}
|
||||
}
|
||||
}, [countryCode, nationalNumber]);
|
||||
|
||||
useEffect(() => {
|
||||
handleChange({ target: { value: nationalNumber } } as any);
|
||||
}, [countryCode, nationalNumber]);
|
||||
|
||||
return (
|
||||
<Input
|
||||
onChange={handleChange}
|
||||
value={nationalNumber}
|
||||
prepend={
|
||||
<CountryCodeDropdown
|
||||
countryCode={countryCode}
|
||||
onChange={setCountryCode}
|
||||
/>
|
||||
}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PhoneInput;
|
|
@ -13,8 +13,7 @@ import { ScrollContext } from 'react-router-scroll-4';
|
|||
|
||||
import { loadInstance } from 'soapbox/actions/instance';
|
||||
import { fetchMe } from 'soapbox/actions/me';
|
||||
import { loadSoapboxConfig, getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { fetchVerificationConfig } from 'soapbox/actions/verification';
|
||||
import { loadSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import * as BuildConfig from 'soapbox/build-config';
|
||||
import GdprBanner from 'soapbox/components/gdpr-banner';
|
||||
import Helmet from 'soapbox/components/helmet';
|
||||
|
@ -27,7 +26,6 @@ import BundleContainer from 'soapbox/features/ui/containers/bundle-container';
|
|||
import {
|
||||
ModalContainer,
|
||||
OnboardingWizard,
|
||||
WaitlistPage,
|
||||
} from 'soapbox/features/ui/util/async-components';
|
||||
import { createGlobals } from 'soapbox/globals';
|
||||
import {
|
||||
|
@ -40,7 +38,6 @@ import {
|
|||
useTheme,
|
||||
useLocale,
|
||||
useInstance,
|
||||
useRegistrationStatus,
|
||||
} from 'soapbox/hooks';
|
||||
import MESSAGES from 'soapbox/messages';
|
||||
import { normalizeSoapboxConfig } from 'soapbox/normalizers';
|
||||
|
@ -73,14 +70,6 @@ const loadInitial = () => {
|
|||
await dispatch(loadInstance());
|
||||
// Await for configuration
|
||||
await dispatch(loadSoapboxConfig());
|
||||
|
||||
const state = getState();
|
||||
const soapboxConfig = getSoapboxConfig(state);
|
||||
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
|
||||
|
||||
if (pepeEnabled && !state.me) {
|
||||
await dispatch(fetchVerificationConfig());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -93,11 +82,9 @@ const SoapboxMount = () => {
|
|||
const { account } = useOwnAccount();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
const features = useFeatures();
|
||||
const { pepeEnabled } = useRegistrationStatus();
|
||||
|
||||
const waitlisted = account && account.source?.approved === false;
|
||||
const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding);
|
||||
const showOnboarding = account && !waitlisted && needsOnboarding;
|
||||
const showOnboarding = account && needsOnboarding;
|
||||
const { redirectRootNoLogin } = soapboxConfig;
|
||||
|
||||
// @ts-ignore: I don't actually know what these should be, lol
|
||||
|
@ -115,25 +102,6 @@ const SoapboxMount = () => {
|
|||
/** Render the auth layout or UI. */
|
||||
const renderSwitch = () => (
|
||||
<Switch>
|
||||
<Redirect from='/v1/verify_email/:token' to='/verify/email/:token' />
|
||||
|
||||
{/* Redirect signup route depending on Pepe enablement. */}
|
||||
{/* We should prefer using /signup in components. */}
|
||||
{pepeEnabled ? (
|
||||
<Redirect from='/signup' to='/verify' />
|
||||
) : (
|
||||
<Redirect from='/verify' to='/signup' />
|
||||
)}
|
||||
|
||||
{waitlisted && (
|
||||
<Route render={(props) => (
|
||||
<BundleContainer fetchComponent={WaitlistPage} loading={LoadingScreen}>
|
||||
{(Component) => <Component {...props} account={account} />}
|
||||
</BundleContainer>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!me && (redirectRootNoLogin
|
||||
? <Redirect exact from='/' to={redirectRootNoLogin} />
|
||||
: <Route exact path='/' component={PublicLayout} />)}
|
||||
|
@ -149,10 +117,6 @@ const SoapboxMount = () => {
|
|||
<Route exact path='/signup' component={AuthLayout} />
|
||||
)}
|
||||
|
||||
{pepeEnabled && (
|
||||
<Route path='/verify' component={AuthLayout} />
|
||||
)}
|
||||
|
||||
<Route path='/reset-password' component={AuthLayout} />
|
||||
<Route path='/edit-password' component={AuthLayout} />
|
||||
<Route path='/invite/:token' component={AuthLayout} />
|
||||
|
|
|
@ -14,8 +14,6 @@ import RegistrationForm from '../auth-login/components/registration-form';
|
|||
import ExternalLoginForm from '../external-login/components/external-login-form';
|
||||
import Footer from '../public-layout/components/footer';
|
||||
import RegisterInvite from '../register-invite';
|
||||
import Verification from '../verification';
|
||||
import EmailPassthru from '../verification/email-passthru';
|
||||
|
||||
const messages = defineMessages({
|
||||
register: { id: 'auth_layout.register', defaultMessage: 'Create an account' },
|
||||
|
@ -65,8 +63,6 @@ const AuthLayout = () => {
|
|||
{/* If already logged in, redirect home. */}
|
||||
{account && <Redirect from='/login' to='/' exact />}
|
||||
|
||||
<Route exact path='/verify' component={Verification} />
|
||||
<Route exact path='/verify/email/:token' component={EmailPassthru} />
|
||||
<Route exact path='/login/external' component={ExternalLoginForm} />
|
||||
<Route exact path='/login/add' component={LoginPage} />
|
||||
<Route exact path='/login' component={LoginPage} />
|
||||
|
|
|
@ -4,8 +4,7 @@ import { Redirect } from 'react-router-dom';
|
|||
|
||||
import { resetPasswordConfirm } from 'soapbox/actions/security';
|
||||
import { Button, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui';
|
||||
import PasswordIndicator from 'soapbox/features/verification/components/password-indicator';
|
||||
import { useAppDispatch, useFeatures } from 'soapbox/hooks';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
const token = new URLSearchParams(window.location.search).get('reset_password_token');
|
||||
|
||||
|
@ -24,11 +23,9 @@ const Statuses = {
|
|||
const PasswordResetConfirm = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const { passwordRequirements } = useFeatures();
|
||||
|
||||
const [password, setPassword] = React.useState('');
|
||||
const [status, setStatus] = React.useState(Statuses.IDLE);
|
||||
const [hasValidPassword, setHasValidPassword] = React.useState<boolean>(passwordRequirements ? false : true);
|
||||
|
||||
const isLoading = status === Statuses.LOADING;
|
||||
|
||||
|
@ -75,14 +72,10 @@ const PasswordResetConfirm = () => {
|
|||
onChange={onChange}
|
||||
required
|
||||
/>
|
||||
|
||||
{passwordRequirements && (
|
||||
<PasswordIndicator password={password} onChange={setHasValidPassword} />
|
||||
)}
|
||||
</FormGroup>
|
||||
|
||||
<FormActions>
|
||||
<Button type='submit' theme='primary' disabled={isLoading || !hasValidPassword}>
|
||||
<Button type='submit' theme='primary' disabled={isLoading}>
|
||||
<FormattedMessage id='password_reset.reset' defaultMessage='Reset password' />
|
||||
</Button>
|
||||
</FormActions>
|
||||
|
|
|
@ -3,11 +3,9 @@ import { defineMessages, useIntl } from 'react-intl';
|
|||
|
||||
import { changePassword } from 'soapbox/actions/security';
|
||||
import { Button, Column, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useFeatures } from 'soapbox/hooks';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
import PasswordIndicator from '../verification/components/password-indicator';
|
||||
|
||||
const messages = defineMessages({
|
||||
updatePasswordSuccess: { id: 'security.update_password.success', defaultMessage: 'Password successfully updated.' },
|
||||
updatePasswordFail: { id: 'security.update_password.fail', defaultMessage: 'Update password failed.' },
|
||||
|
@ -24,11 +22,9 @@ const initialState = { currentPassword: '', newPassword: '', newPasswordConfirma
|
|||
const EditPassword = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const { passwordRequirements } = useFeatures();
|
||||
|
||||
const [state, setState] = React.useState(initialState);
|
||||
const [isLoading, setLoading] = React.useState(false);
|
||||
const [hasValidPassword, setHasValidPassword] = React.useState<boolean>(passwordRequirements ? false : true);
|
||||
|
||||
const { currentPassword, newPassword, newPasswordConfirmation } = state;
|
||||
|
||||
|
@ -73,10 +69,6 @@ const EditPassword = () => {
|
|||
onChange={handleInputChange}
|
||||
value={newPassword}
|
||||
/>
|
||||
|
||||
{passwordRequirements && (
|
||||
<PasswordIndicator password={newPassword} onChange={setHasValidPassword} />
|
||||
)}
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup labelText={intl.formatMessage(messages.confirmationFieldLabel)}>
|
||||
|
@ -93,7 +85,7 @@ const EditPassword = () => {
|
|||
{intl.formatMessage(messages.cancel)}
|
||||
</Button>
|
||||
|
||||
<Button type='submit' theme='primary' disabled={isLoading || !hasValidPassword}>
|
||||
<Button type='submit' theme='primary' disabled={isLoading}>
|
||||
{intl.formatMessage(messages.submit)}
|
||||
</Button>
|
||||
</FormActions>
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
import { rememberInstance } from 'soapbox/actions/instance';
|
||||
import { render, screen, rootReducer } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import LandingPage from '..';
|
||||
import { rememberInstance } from '../../../actions/instance';
|
||||
import { SOAPBOX_CONFIG_REMEMBER_SUCCESS } from '../../../actions/soapbox';
|
||||
import { PEPE_FETCH_INSTANCE_SUCCESS } from '../../../actions/verification';
|
||||
import { render, screen, rootReducer, applyActions } from '../../../jest/test-helpers';
|
||||
|
||||
describe('<LandingPage />', () => {
|
||||
it('renders a RegistrationForm for an open Pleroma instance', () => {
|
||||
|
@ -21,7 +20,6 @@ describe('<LandingPage />', () => {
|
|||
|
||||
expect(screen.queryByTestId('registrations-open')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('registrations-closed')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('registrations-pepe')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders "closed" message for a closed Pleroma instance', () => {
|
||||
|
@ -38,53 +36,5 @@ describe('<LandingPage />', () => {
|
|||
|
||||
expect(screen.queryByTestId('registrations-closed')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('registrations-open')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('registrations-pepe')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Pepe flow if Pepe extension is enabled', () => {
|
||||
|
||||
const state = applyActions(undefined, [{
|
||||
type: SOAPBOX_CONFIG_REMEMBER_SUCCESS,
|
||||
soapboxConfig: {
|
||||
extensions: {
|
||||
pepe: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
type: PEPE_FETCH_INSTANCE_SUCCESS,
|
||||
instance: {
|
||||
registrations: true,
|
||||
},
|
||||
}], rootReducer);
|
||||
|
||||
render(<LandingPage />, undefined, state);
|
||||
|
||||
expect(screen.queryByTestId('registrations-pepe')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('registrations-open')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('registrations-closed')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders "closed" message for a Truth Social instance with Pepe closed', () => {
|
||||
|
||||
const state = applyActions(undefined, [{
|
||||
type: rememberInstance.fulfilled.type,
|
||||
payload: {
|
||||
version: '3.4.1 (compatible; TruthSocial 1.0.0)',
|
||||
registrations: false,
|
||||
},
|
||||
}, {
|
||||
type: PEPE_FETCH_INSTANCE_SUCCESS,
|
||||
instance: {
|
||||
registrations: false,
|
||||
},
|
||||
}], rootReducer);
|
||||
|
||||
render(<LandingPage />, undefined, state);
|
||||
|
||||
expect(screen.queryByTestId('registrations-closed')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('registrations-pepe')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('registrations-open')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,16 +4,14 @@ import { FormattedMessage } from 'react-intl';
|
|||
import { prepareRequest } from 'soapbox/actions/consumer-auth';
|
||||
import Markup from 'soapbox/components/markup';
|
||||
import { Button, Card, CardBody, Stack, Text } from 'soapbox/components/ui';
|
||||
import VerificationBadge from 'soapbox/components/verification-badge';
|
||||
import RegistrationForm from 'soapbox/features/auth-login/components/registration-form';
|
||||
import { useAppDispatch, useFeatures, useInstance, useRegistrationStatus, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useFeatures, useInstance, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { capitalize } from 'soapbox/utils/strings';
|
||||
|
||||
const LandingPage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
const { pepeEnabled, pepeOpen } = useRegistrationStatus();
|
||||
const instance = useInstance();
|
||||
|
||||
/** Registrations are closed */
|
||||
|
@ -65,34 +63,10 @@ const LandingPage = () => {
|
|||
);
|
||||
};
|
||||
|
||||
/** Pepe API registrations are open */
|
||||
const renderPepe = () => {
|
||||
return (
|
||||
<Stack space={3} data-testid='registrations-pepe'>
|
||||
<VerificationBadge className='mx-auto h-16 w-16' />
|
||||
|
||||
<Stack>
|
||||
<Text size='2xl' weight='bold' align='center'>
|
||||
<FormattedMessage id='registrations.get_started' defaultMessage="Let's get started!" />
|
||||
</Text>
|
||||
<Text theme='muted' align='center'>
|
||||
<FormattedMessage id='registrations.tagline' defaultMessage='Social Media Without Discrimination' />
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Button to='/verify' theme='primary' block>
|
||||
<FormattedMessage id='registrations.create_account' defaultMessage='Create an account' />
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
// Render registration flow depending on features
|
||||
const renderBody = () => {
|
||||
if (soapboxConfig.authProvider) {
|
||||
return renderProvider();
|
||||
} else if (pepeEnabled && pepeOpen) {
|
||||
return renderPepe();
|
||||
} else if (features.accountCreation && instance.registrations) {
|
||||
return renderOpen();
|
||||
} else {
|
||||
|
|
|
@ -36,7 +36,6 @@ import {
|
|||
ReplyMentionsModal,
|
||||
ReportModal,
|
||||
UnauthorizedModal,
|
||||
VerifySmsModal,
|
||||
VideoModal,
|
||||
} from 'soapbox/features/ui/util/async-components';
|
||||
|
||||
|
@ -82,7 +81,6 @@ const MODAL_COMPONENTS = {
|
|||
'REPLY_MENTIONS': ReplyMentionsModal,
|
||||
'REPORT': ReportModal,
|
||||
'UNAUTHORIZED': UnauthorizedModal,
|
||||
'VERIFY_SMS': VerifySmsModal,
|
||||
'VIDEO': VideoModal,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,228 +0,0 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import OtpInput from 'react-otp-input';
|
||||
|
||||
import { verifyCredentials } from 'soapbox/actions/auth';
|
||||
import { closeModal } from 'soapbox/actions/modals';
|
||||
import { reConfirmPhoneVerification, reRequestPhoneVerification } from 'soapbox/actions/verification';
|
||||
import { FormGroup, PhoneInput, Modal, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
import { getAccessToken } from 'soapbox/utils/auth';
|
||||
|
||||
const messages = defineMessages({
|
||||
verificationInvalid: {
|
||||
id: 'sms_verification.invalid',
|
||||
defaultMessage: 'Please enter a valid phone number.',
|
||||
},
|
||||
verificationSuccess: {
|
||||
id: 'sms_verification.success',
|
||||
defaultMessage: 'A verification code has been sent to your phone number.',
|
||||
},
|
||||
verificationFail: {
|
||||
id: 'sms_verification.fail',
|
||||
defaultMessage: 'Failed to send SMS message to your phone number.',
|
||||
},
|
||||
verificationExpired: {
|
||||
id: 'sms_verification.expired',
|
||||
defaultMessage: 'Your SMS token has expired.',
|
||||
},
|
||||
verifySms: {
|
||||
id: 'sms_verification.modal.verify_sms',
|
||||
defaultMessage: 'Verify SMS',
|
||||
},
|
||||
verifyNumber: {
|
||||
id: 'sms_verification.modal.verify_number',
|
||||
defaultMessage: 'Verify phone number',
|
||||
},
|
||||
verifyCode: {
|
||||
id: 'sms_verification.modal.verify_code',
|
||||
defaultMessage: 'Verify code',
|
||||
},
|
||||
});
|
||||
|
||||
interface IVerifySmsModal {
|
||||
onClose: (type: string) => void
|
||||
}
|
||||
|
||||
enum Statuses {
|
||||
IDLE = 'IDLE',
|
||||
READY = 'READY',
|
||||
REQUESTED = 'REQUESTED',
|
||||
FAIL = 'FAIL',
|
||||
SUCCESS = 'SUCCESS',
|
||||
}
|
||||
|
||||
const VerifySmsModal: React.FC<IVerifySmsModal> = ({ onClose }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const instance = useInstance();
|
||||
const accessToken = useAppSelector((state) => getAccessToken(state));
|
||||
const isLoading = useAppSelector((state) => state.verification.isLoading);
|
||||
|
||||
const [status, setStatus] = useState<Statuses>(Statuses.IDLE);
|
||||
const [phone, setPhone] = useState<string>();
|
||||
const [verificationCode, setVerificationCode] = useState('');
|
||||
const [requestedAnother, setAlreadyRequestedAnother] = useState(false);
|
||||
|
||||
const isValid = !!phone;
|
||||
|
||||
const onChange = useCallback((phone?: string) => {
|
||||
setPhone(phone);
|
||||
}, []);
|
||||
|
||||
const handleSubmit = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!isValid) {
|
||||
setStatus(Statuses.IDLE);
|
||||
toast.error(intl.formatMessage(messages.verificationInvalid));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(reRequestPhoneVerification(phone!)).then(() => {
|
||||
toast.success(
|
||||
intl.formatMessage(messages.verificationSuccess),
|
||||
);
|
||||
})
|
||||
.finally(() => setStatus(Statuses.REQUESTED))
|
||||
.catch(() => {
|
||||
toast.error(intl.formatMessage(messages.verificationFail));
|
||||
});
|
||||
};
|
||||
|
||||
const resendVerificationCode = (event?: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAlreadyRequestedAnother(true);
|
||||
handleSubmit(event as React.MouseEvent<HTMLButtonElement>);
|
||||
};
|
||||
|
||||
const onConfirmationClick = (event: any) => {
|
||||
switch (status) {
|
||||
case Statuses.IDLE:
|
||||
setStatus(Statuses.READY);
|
||||
break;
|
||||
case Statuses.READY:
|
||||
handleSubmit(event);
|
||||
break;
|
||||
case Statuses.REQUESTED:
|
||||
submitVerification();
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
};
|
||||
|
||||
const confirmationText = useMemo(() => {
|
||||
switch (status) {
|
||||
case Statuses.IDLE:
|
||||
return intl.formatMessage(messages.verifySms);
|
||||
case Statuses.READY:
|
||||
return intl.formatMessage(messages.verifyNumber);
|
||||
case Statuses.REQUESTED:
|
||||
return intl.formatMessage(messages.verifyCode);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
const renderModalBody = () => {
|
||||
switch (status) {
|
||||
case Statuses.IDLE:
|
||||
return (
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage
|
||||
id='sms_verification.modal.verify_help_text'
|
||||
defaultMessage='Verify your phone number to start using {instance}.'
|
||||
values={{
|
||||
instance: instance.title,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
);
|
||||
case Statuses.READY:
|
||||
return (
|
||||
<FormGroup labelText={<FormattedMessage id='sms_verification.phone.label' defaultMessage='Phone number' />}>
|
||||
<PhoneInput
|
||||
value={phone}
|
||||
onChange={onChange}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
case Statuses.REQUESTED:
|
||||
return (
|
||||
<>
|
||||
<Text theme='muted' size='sm' align='center'>
|
||||
<FormattedMessage
|
||||
id='sms_verification.modal.enter_code'
|
||||
defaultMessage='We sent you a 6-digit code via SMS. Enter it below.'
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<OtpInput
|
||||
value={verificationCode}
|
||||
onChange={setVerificationCode}
|
||||
numInputs={6}
|
||||
isInputNum
|
||||
shouldAutoFocus
|
||||
isDisabled={isLoading}
|
||||
containerStyle='flex justify-center mt-2 space-x-4'
|
||||
inputStyle='w-10i border-gray-300 rounded-md focus:ring-indigo-500 focus:border-indigo-500'
|
||||
/>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const submitVerification = () => {
|
||||
if (!accessToken) return;
|
||||
// TODO: handle proper validation from Pepe -- expired vs invalid
|
||||
dispatch(reConfirmPhoneVerification(verificationCode))
|
||||
.then(() => {
|
||||
setStatus(Statuses.SUCCESS);
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
dispatch(verifyCredentials(accessToken))
|
||||
.then(() => dispatch(closeModal('VERIFY_SMS')));
|
||||
|
||||
})
|
||||
.catch(() => toast.error(intl.formatMessage(messages.verificationExpired)));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (verificationCode.length === 6) {
|
||||
submitVerification();
|
||||
}
|
||||
}, [verificationCode]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='sms_verification.modal.verify_title'
|
||||
defaultMessage='Verify your phone number'
|
||||
/>
|
||||
}
|
||||
onClose={() => onClose('VERIFY_SMS')}
|
||||
cancelAction={status === Statuses.IDLE ? () => onClose('VERIFY_SMS') : undefined}
|
||||
cancelText='Skip for now'
|
||||
confirmationAction={onConfirmationClick}
|
||||
confirmationText={confirmationText}
|
||||
secondaryAction={status === Statuses.REQUESTED ? resendVerificationCode : undefined}
|
||||
secondaryText={status === Statuses.REQUESTED ? (
|
||||
<FormattedMessage
|
||||
id='sms_verification.modal.resend_code'
|
||||
defaultMessage='Resend verification code?'
|
||||
/>
|
||||
) : undefined}
|
||||
secondaryDisabled={requestedAnother}
|
||||
>
|
||||
<Stack space={4}>
|
||||
{renderModalBody()}
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerifySmsModal;
|
|
@ -219,11 +219,11 @@ export function ListEditor() {
|
|||
}
|
||||
|
||||
export function ListAdder() {
|
||||
return import(/*webpackChunkName: "features/list_adder" */'../../list-adder');
|
||||
return import('../../list-adder');
|
||||
}
|
||||
|
||||
export function Search() {
|
||||
return import(/*webpackChunkName: "features/search" */'../../search');
|
||||
return import('../../search');
|
||||
}
|
||||
|
||||
export function LoginPage() {
|
||||
|
@ -478,24 +478,16 @@ export function OnboardingWizard() {
|
|||
return import('../../onboarding/onboarding-wizard');
|
||||
}
|
||||
|
||||
export function WaitlistPage() {
|
||||
return import('../../verification/waitlist-page');
|
||||
}
|
||||
|
||||
export function CompareHistoryModal() {
|
||||
return import(/*webpackChunkName: "modals/compare_history_modal" */'../components/modals/compare-history-modal');
|
||||
return import('../components/modals/compare-history-modal');
|
||||
}
|
||||
|
||||
export function AuthTokenList() {
|
||||
return import('../../auth-token-list');
|
||||
}
|
||||
|
||||
export function VerifySmsModal() {
|
||||
return import('../components/modals/verify-sms-modal');
|
||||
}
|
||||
|
||||
export function FamiliarFollowersModal() {
|
||||
return import(/*webpackChunkName: "modals/familiar_followers_modal" */'../components/modals/familiar-followers-modal');
|
||||
return import('../components/modals/familiar-followers-modal');
|
||||
}
|
||||
|
||||
export function AnnouncementsPanel() {
|
||||
|
@ -503,7 +495,7 @@ export function AnnouncementsPanel() {
|
|||
}
|
||||
|
||||
export function Quotes() {
|
||||
return import(/*webpackChunkName: "features/quotes" */'../../quotes');
|
||||
return import('../../quotes');
|
||||
}
|
||||
|
||||
export function ComposeEventModal() {
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
import { Map as ImmutableMap, Record as ImmutableRecord } from 'immutable';
|
||||
import React from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
|
||||
import { render, screen } from '../../../jest/test-helpers';
|
||||
import Verification from '../index';
|
||||
|
||||
const TestableComponent = () => (
|
||||
<Switch>
|
||||
<Route path='/verify' exact><Verification /></Route>
|
||||
<Route path='/' exact><span data-testid='home'>Homepage</span></Route>
|
||||
</Switch>
|
||||
);
|
||||
|
||||
const renderComponent = (store: any) => render(
|
||||
<TestableComponent />,
|
||||
{},
|
||||
store,
|
||||
{ initialEntries: ['/verify'] },
|
||||
);
|
||||
|
||||
describe('<Verification />', () => {
|
||||
let store: any;
|
||||
|
||||
beforeEach(() => {
|
||||
store = {
|
||||
verification: ImmutableRecord({
|
||||
instance: ImmutableMap({
|
||||
isReady: true,
|
||||
registrations: true,
|
||||
}),
|
||||
ageMinimum: null,
|
||||
currentChallenge: null,
|
||||
isLoading: false,
|
||||
isComplete: false,
|
||||
token: null,
|
||||
})(),
|
||||
};
|
||||
|
||||
__stub(mock => {
|
||||
mock.onGet('/api/v1/pepe/instance')
|
||||
.reply(200, {
|
||||
age_minimum: 18,
|
||||
approval_required: true,
|
||||
challenges: ['age', 'email', 'sms'],
|
||||
});
|
||||
|
||||
mock.onPost('/api/v1/pepe/registrations')
|
||||
.reply(200, {
|
||||
access_token: 'N-dZmNqNSmTutJLsGjZ5AnJL4sLw_y-N3pn2acSqJY8',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When registration is closed', () => {
|
||||
it('successfully redirects to the homepage', () => {
|
||||
const verification = store.verification.setIn(['instance', 'registrations'], false);
|
||||
store.verification = verification;
|
||||
|
||||
renderComponent(store);
|
||||
expect(screen.getByTestId('home')).toHaveTextContent('Homepage');
|
||||
});
|
||||
});
|
||||
|
||||
describe('When verification is complete', () => {
|
||||
it('successfully renders the Registration component', () => {
|
||||
const verification = store.verification.set('isComplete', true);
|
||||
store.verification = verification;
|
||||
|
||||
renderComponent(store);
|
||||
expect(screen.getByRole('heading')).toHaveTextContent('Register your account');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Switching verification steps', () => {
|
||||
it('successfully renders the Birthday step', () => {
|
||||
const verification = store.verification.set('currentChallenge', 'age');
|
||||
store.verification = verification;
|
||||
|
||||
renderComponent(store);
|
||||
|
||||
expect(screen.getByRole('heading')).toHaveTextContent('Enter your birth date');
|
||||
});
|
||||
|
||||
it('successfully renders the Email step', () => {
|
||||
const verification = store.verification.set('currentChallenge', 'email');
|
||||
store.verification = verification;
|
||||
|
||||
renderComponent(store);
|
||||
|
||||
expect(screen.getByRole('heading')).toHaveTextContent('Enter your email address');
|
||||
});
|
||||
|
||||
it('successfully renders the SMS step', () => {
|
||||
const verification = store.verification.set('currentChallenge', 'sms');
|
||||
store.verification = verification;
|
||||
|
||||
renderComponent(store);
|
||||
|
||||
expect(screen.getByRole('heading')).toHaveTextContent('Enter your phone number');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,117 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
|
||||
import { fireEvent, render, screen, waitFor } from '../../../jest/test-helpers';
|
||||
import Registration from '../registration';
|
||||
|
||||
describe('<Registration />', () => {
|
||||
it('renders', () => {
|
||||
render(<Registration />);
|
||||
|
||||
expect(screen.getByRole('heading')).toHaveTextContent(/register your account/i);
|
||||
});
|
||||
|
||||
describe('with valid data', () => {
|
||||
beforeEach(() => {
|
||||
__stub(mock => {
|
||||
mock.onPost('/api/v1/pepe/accounts').reply(200, {});
|
||||
mock.onPost('/api/v1/apps').reply(200, {});
|
||||
mock.onPost('/oauth/token').reply(200, {});
|
||||
mock.onGet('/api/v1/accounts/verify_credentials').reply(200, { id: '123' });
|
||||
mock.onGet('/api/v1/instance').reply(200, {});
|
||||
});
|
||||
});
|
||||
|
||||
it('handles successful submission', async() => {
|
||||
render(<Registration />);
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.submit(screen.getByTestId('button'), { preventDefault: () => {} });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('toast')).toHaveTextContent(/welcome to/i);
|
||||
});
|
||||
|
||||
expect(screen.queryAllByRole('heading')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with invalid data', () => {
|
||||
it('handles 422 errors', async() => {
|
||||
__stub(mock => {
|
||||
mock.onPost('/api/v1/pepe/accounts').reply(
|
||||
422, {
|
||||
error: 'user_taken',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
render(<Registration />);
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.submit(screen.getByTestId('button'), { preventDefault: () => {} });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('toast')).toHaveTextContent(/this username has already been taken/i);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles 422 errors with messages', async() => {
|
||||
__stub(mock => {
|
||||
mock.onPost('/api/v1/pepe/accounts').reply(
|
||||
422, {
|
||||
error: 'user_vip',
|
||||
message: 'This username is unavailable.',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
render(<Registration />);
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.submit(screen.getByTestId('button'), { preventDefault: () => {} });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('toast')).toHaveTextContent(/this username is unavailable/i);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('handles generic errors', async() => {
|
||||
__stub(mock => {
|
||||
mock.onPost('/api/v1/pepe/accounts').reply(500, {});
|
||||
});
|
||||
|
||||
render(<Registration />);
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.submit(screen.getByTestId('button'), { preventDefault: () => {} });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('toast')).toHaveTextContent(/failed to register your account/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validations', () => {
|
||||
it('should undisable button with valid password', async() => {
|
||||
render(<Registration />);
|
||||
|
||||
expect(screen.getByTestId('button')).toBeDisabled();
|
||||
fireEvent.change(screen.getByTestId('password-input'), { target: { value: 'Password' } });
|
||||
expect(screen.getByTestId('button')).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('should disable button with invalid password', async() => {
|
||||
render(<Registration />);
|
||||
|
||||
fireEvent.change(screen.getByTestId('password-input'), { target: { value: 'Passwor' } });
|
||||
expect(screen.getByTestId('button')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,72 +0,0 @@
|
|||
import React, { useEffect, useMemo } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { Stack } from 'soapbox/components/ui';
|
||||
import ValidationCheckmark from 'soapbox/components/validation-checkmark';
|
||||
|
||||
const messages = defineMessages({
|
||||
minimumCharacters: {
|
||||
id: 'registration.validation.minimum_characters',
|
||||
defaultMessage: '8 characters',
|
||||
},
|
||||
capitalLetter: {
|
||||
id: 'registration.validation.capital_letter',
|
||||
defaultMessage: '1 capital letter',
|
||||
},
|
||||
lowercaseLetter: {
|
||||
id: 'registration.validation.lowercase_letter',
|
||||
defaultMessage: '1 lowercase letter',
|
||||
},
|
||||
});
|
||||
|
||||
const hasUppercaseCharacter = (string: string) => {
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
if (string.charAt(i) === string.charAt(i).toUpperCase() && string.charAt(i).match(/[a-z]/i)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const hasLowercaseCharacter = (string: string) => {
|
||||
return string.toUpperCase() !== string;
|
||||
};
|
||||
|
||||
interface IPasswordIndicator {
|
||||
onChange(isValid: boolean): void
|
||||
password: string
|
||||
}
|
||||
|
||||
const PasswordIndicator = ({ onChange, password }: IPasswordIndicator) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const meetsLengthRequirements = useMemo(() => password.length >= 8, [password]);
|
||||
const meetsCapitalLetterRequirements = useMemo(() => hasUppercaseCharacter(password), [password]);
|
||||
const meetsLowercaseLetterRequirements = useMemo(() => hasLowercaseCharacter(password), [password]);
|
||||
const hasValidPassword = meetsLengthRequirements && meetsCapitalLetterRequirements && meetsLowercaseLetterRequirements;
|
||||
|
||||
useEffect(() => {
|
||||
onChange(hasValidPassword);
|
||||
}, [hasValidPassword]);
|
||||
|
||||
return (
|
||||
<Stack className='mt-2' space={1}>
|
||||
<ValidationCheckmark
|
||||
isValid={meetsLengthRequirements}
|
||||
text={intl.formatMessage(messages.minimumCharacters)}
|
||||
/>
|
||||
|
||||
<ValidationCheckmark
|
||||
isValid={meetsCapitalLetterRequirements}
|
||||
text={intl.formatMessage(messages.capitalLetter)}
|
||||
/>
|
||||
|
||||
<ValidationCheckmark
|
||||
isValid={meetsLowercaseLetterRequirements}
|
||||
text={intl.formatMessage(messages.lowercaseLetter)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordIndicator;
|
|
@ -1,167 +0,0 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
|
||||
import { confirmEmailVerification } from 'soapbox/actions/verification';
|
||||
import { Icon, Spinner, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
import { ChallengeTypes } from './index';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const Statuses = {
|
||||
IDLE: 'IDLE',
|
||||
SUCCESS: 'SUCCESS',
|
||||
GENERIC_FAIL: 'GENERIC_FAIL',
|
||||
TOKEN_NOT_FOUND: 'TOKEN_NOT_FOUND',
|
||||
TOKEN_EXPIRED: 'TOKEN_EXPIRED',
|
||||
};
|
||||
|
||||
const messages = defineMessages({
|
||||
emailConfirmedHeading: { id: 'email_passthru.confirmed.heading', defaultMessage: 'Email Confirmed!' },
|
||||
emailConfirmedBody: { id: 'email_passthru.confirmed.body', defaultMessage: 'Close this tab and continue the registration process on the {bold} from which you sent this email confirmation.' },
|
||||
genericFailHeading: { id: 'email_passthru.generic_fail.heading', defaultMessage: 'Something Went Wrong' },
|
||||
genericFailBody: { id: 'email_passthru.generic_fail.body', defaultMessage: 'Please request a new email confirmation.' },
|
||||
tokenNotFoundHeading: { id: 'email_passthru.token_not_found.heading', defaultMessage: 'Invalid Token' },
|
||||
tokenNotFoundBody: { id: 'email_passthru.token_not_found.body', defaultMessage: 'Your email token was not found. Please request a new email confirmation from the {bold} from which you sent this email confirmation.' },
|
||||
tokenExpiredHeading: { id: 'email_passthru.token_expired.heading', defaultMessage: 'Token Expired' },
|
||||
tokenExpiredBody: { id: 'email_passthru.token_expired.body', defaultMessage: 'Your email token has expired. Please request a new email confirmation from the {bold} from which you sent this email confirmation.' },
|
||||
emailConfirmed: { id: 'email_passthru.success', defaultMessage: 'Your email has been verified!' },
|
||||
genericFail: { id: 'email_passthru.fail.generic', defaultMessage: 'Unable to confirm your email' },
|
||||
tokenExpired: { id: 'email_passthru.fail.expired', defaultMessage: 'Your email token has expired' },
|
||||
tokenNotFound: { id: 'email_passthru.fail.not_found', defaultMessage: 'Your email token is invalid.' },
|
||||
invalidToken: { id: 'email_passthru.fail.invalid_token', defaultMessage: 'Your token is invalid' },
|
||||
});
|
||||
|
||||
const Success = () => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const currentChallenge = useAppSelector((state) => state.verification.currentChallenge as ChallengeTypes);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Bypass the user straight to the next step.
|
||||
if (currentChallenge === ChallengeTypes.SMS) {
|
||||
history.push('/verify');
|
||||
}
|
||||
}, [currentChallenge]);
|
||||
|
||||
return (
|
||||
<Stack space={4} alignItems='center'>
|
||||
<Icon src={require('@tabler/icons/circle-check.svg')} className='h-10 w-10 text-primary-600 dark:text-primary-400' />
|
||||
<Text size='3xl' weight='semibold' align='center'>
|
||||
{intl.formatMessage(messages.emailConfirmedHeading)}
|
||||
</Text>
|
||||
<Text theme='muted' align='center'>
|
||||
{intl.formatMessage(messages.emailConfirmedBody, { bold: <Text tag='span' weight='medium'>same device</Text> })}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const GenericFail = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Stack space={4} alignItems='center'>
|
||||
<Icon src={require('@tabler/icons/circle-x.svg')} className='h-10 w-10 text-danger-600' />
|
||||
<Text size='3xl' weight='semibold' align='center'>
|
||||
{intl.formatMessage(messages.genericFailHeading)}
|
||||
</Text>
|
||||
<Text theme='muted' align='center'>
|
||||
{intl.formatMessage(messages.genericFailBody)}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const TokenNotFound = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Stack space={4} alignItems='center'>
|
||||
<Icon src={require('@tabler/icons/circle-x.svg')} className='h-10 w-10 text-danger-600' />
|
||||
<Text size='3xl' weight='semibold' align='center'>
|
||||
{intl.formatMessage(messages.tokenNotFoundHeading)}
|
||||
</Text>
|
||||
<Text theme='muted' align='center'>
|
||||
{intl.formatMessage(messages.tokenNotFoundBody, { bold: <Text tag='span' weight='medium'>same device</Text> })}
|
||||
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const TokenExpired = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Stack space={4} alignItems='center'>
|
||||
<Icon src={require('@tabler/icons/circle-x.svg')} className='h-10 w-10 text-danger-600' />
|
||||
<Text size='3xl' weight='semibold' align='center'>
|
||||
{intl.formatMessage(messages.tokenExpiredHeading)}
|
||||
</Text>
|
||||
<Text theme='muted' align='center'>
|
||||
{intl.formatMessage(messages.tokenExpiredBody, { bold: <Text tag='span' weight='medium'>same device</Text> })}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const EmailPassThru = () => {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const [status, setStatus] = React.useState(Statuses.IDLE);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (token) {
|
||||
dispatch(confirmEmailVerification(token))
|
||||
.then(() => {
|
||||
setStatus(Statuses.SUCCESS);
|
||||
toast.success(intl.formatMessage(messages.emailConfirmed));
|
||||
})
|
||||
.catch((error: AxiosError<any>) => {
|
||||
const errorKey = error?.response?.data?.error;
|
||||
let message = intl.formatMessage(messages.genericFail);
|
||||
|
||||
if (errorKey) {
|
||||
switch (errorKey) {
|
||||
case 'token_expired':
|
||||
message = intl.formatMessage(messages.tokenExpired);
|
||||
setStatus(Statuses.TOKEN_EXPIRED);
|
||||
break;
|
||||
case 'token_not_found':
|
||||
message = intl.formatMessage(messages.tokenNotFound);
|
||||
message = intl.formatMessage(messages.invalidToken);
|
||||
setStatus(Statuses.TOKEN_NOT_FOUND);
|
||||
break;
|
||||
default:
|
||||
setStatus(Statuses.GENERIC_FAIL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
toast.error(message);
|
||||
});
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
switch (status) {
|
||||
case Statuses.SUCCESS:
|
||||
return <Success />;
|
||||
case Statuses.TOKEN_EXPIRED:
|
||||
return <TokenExpired />;
|
||||
case Statuses.TOKEN_NOT_FOUND:
|
||||
return <TokenNotFound />;
|
||||
case Statuses.GENERIC_FAIL:
|
||||
return <GenericFail />;
|
||||
default:
|
||||
return <Spinner />;
|
||||
}
|
||||
};
|
||||
|
||||
export default EmailPassThru;
|
|
@ -1,56 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { fetchVerificationConfig } from 'soapbox/actions/verification';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import Registration from './registration';
|
||||
import AgeVerification from './steps/age-verification';
|
||||
import EmailVerification from './steps/email-verification';
|
||||
import SmsVerification from './steps/sms-verification';
|
||||
|
||||
export enum ChallengeTypes {
|
||||
EMAIL = 'email',
|
||||
SMS = 'sms',
|
||||
AGE = 'age',
|
||||
}
|
||||
|
||||
const verificationSteps = {
|
||||
email: EmailVerification,
|
||||
sms: SmsVerification,
|
||||
age: AgeVerification,
|
||||
};
|
||||
|
||||
const Verification = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const isInstanceReady = useAppSelector((state) => state.verification.instance.get('isReady') === true);
|
||||
const isRegistrationOpen = useAppSelector(state => state.verification.instance.get('registrations') === true);
|
||||
const currentChallenge = useAppSelector((state) => state.verification.currentChallenge as ChallengeTypes);
|
||||
const isVerificationComplete = useAppSelector((state) => state.verification.isComplete);
|
||||
const StepToRender = verificationSteps[currentChallenge];
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch(fetchVerificationConfig());
|
||||
}, []);
|
||||
|
||||
if (isInstanceReady && !isRegistrationOpen) {
|
||||
return <Redirect to='/' />;
|
||||
}
|
||||
|
||||
if (isVerificationComplete) {
|
||||
return (
|
||||
<Registration />
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentChallenge) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StepToRender />
|
||||
);
|
||||
};
|
||||
|
||||
export default Verification;
|
|
@ -1,161 +0,0 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { logIn, verifyCredentials } from 'soapbox/actions/auth';
|
||||
import { fetchInstance } from 'soapbox/actions/instance';
|
||||
import { startOnboarding } from 'soapbox/actions/onboarding';
|
||||
import { createAccount, removeStoredVerification } from 'soapbox/actions/verification';
|
||||
import { Button, Form, FormGroup, Input, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useInstance, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
import { getRedirectUrl } from 'soapbox/utils/redirect';
|
||||
|
||||
import PasswordIndicator from './components/password-indicator';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const messages = defineMessages({
|
||||
success: { id: 'registrations.success', defaultMessage: 'Welcome to {siteTitle}!' },
|
||||
usernameLabel: { id: 'registrations.username.label', defaultMessage: 'Your username' },
|
||||
usernameHint: { id: 'registrations.username.hint', defaultMessage: 'May only contain A-Z, 0-9, and underscores' },
|
||||
usernameTaken: { id: 'registrations.unprocessable_entity', defaultMessage: 'This username has already been taken.' },
|
||||
passwordLabel: { id: 'registrations.password.label', defaultMessage: 'Password' },
|
||||
error: { id: 'registrations.error', defaultMessage: 'Failed to register your account.' },
|
||||
});
|
||||
|
||||
const initialState = {
|
||||
username: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
const Registration = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const instance = useInstance();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
const { links } = soapboxConfig;
|
||||
|
||||
const isLoading = useAppSelector((state) => state.verification.isLoading as boolean);
|
||||
|
||||
const [state, setState] = React.useState(initialState);
|
||||
const [shouldRedirect, setShouldRedirect] = React.useState<boolean>(false);
|
||||
const [hasValidPassword, setHasValidPassword] = React.useState<boolean>(false);
|
||||
const { username, password } = state;
|
||||
|
||||
const handleSubmit: React.FormEventHandler = React.useCallback((event) => {
|
||||
event.preventDefault();
|
||||
|
||||
dispatch(createAccount(username, password))
|
||||
.then(() => dispatch(logIn(username, password)))
|
||||
.then(({ access_token }: any) => dispatch(verifyCredentials(access_token)))
|
||||
.then(() => dispatch(fetchInstance()))
|
||||
.then(() => {
|
||||
setShouldRedirect(true);
|
||||
removeStoredVerification();
|
||||
dispatch(startOnboarding());
|
||||
toast.success(
|
||||
intl.formatMessage(messages.success, { siteTitle: instance.title }),
|
||||
);
|
||||
})
|
||||
.catch((errorResponse: AxiosError<{ error: string, message: string }>) => {
|
||||
const error = errorResponse.response?.data?.error;
|
||||
|
||||
if (error) {
|
||||
toast.error(errorResponse.response?.data?.message || intl.formatMessage(messages.usernameTaken));
|
||||
} else {
|
||||
toast.error(intl.formatMessage(messages.error));
|
||||
}
|
||||
});
|
||||
}, [username, password]);
|
||||
|
||||
const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((event) => {
|
||||
event.persist();
|
||||
|
||||
setState((prevState) => ({ ...prevState, [event.target.name]: event.target.value }));
|
||||
}, []);
|
||||
|
||||
if (shouldRedirect) {
|
||||
const redirectUri = getRedirectUrl();
|
||||
return <Redirect to={redirectUri} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='-mx-4 mb-4 border-b border-solid border-gray-200 pb-4 dark:border-gray-800 sm:-mx-10 sm:pb-10'>
|
||||
<h1 className='text-center text-2xl font-bold'>
|
||||
<FormattedMessage id='registration.header' defaultMessage='Register your account' />
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className='mx-auto space-y-4 sm:w-2/3 sm:pt-10 md:w-1/2'>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<FormGroup labelText={intl.formatMessage(messages.usernameLabel)} hintText={intl.formatMessage(messages.usernameHint)}>
|
||||
<Input
|
||||
name='username'
|
||||
type='text'
|
||||
value={username}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
icon={require('@tabler/icons/at.svg')}
|
||||
placeholder='LibertyForAll'
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup labelText={intl.formatMessage(messages.passwordLabel)}>
|
||||
<Input
|
||||
name='password'
|
||||
type='password'
|
||||
value={password}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
data-testid='password-input'
|
||||
/>
|
||||
|
||||
<PasswordIndicator password={password} onChange={setHasValidPassword} />
|
||||
</FormGroup>
|
||||
|
||||
<div className='space-y-2 text-center'>
|
||||
<Button
|
||||
block
|
||||
theme='primary'
|
||||
type='submit'
|
||||
disabled={isLoading || !hasValidPassword}
|
||||
>
|
||||
<FormattedMessage id='header.register.label' defaultMessage='Register' />
|
||||
</Button>
|
||||
|
||||
{(links.get('termsOfService') && links.get('privacyPolicy')) ? (
|
||||
<Text theme='muted' size='xs'>
|
||||
<FormattedMessage
|
||||
id='registration.acceptance'
|
||||
defaultMessage='By registering, you agree to the {terms} and {privacy}.'
|
||||
values={{
|
||||
terms: (
|
||||
<a href={links.get('termsOfService')} target='_blank' className='text-primary-600 hover:underline dark:text-primary-400'>
|
||||
<FormattedMessage
|
||||
id='registration.tos'
|
||||
defaultMessage='Terms of Service'
|
||||
/>
|
||||
</a>
|
||||
),
|
||||
privacy: (
|
||||
<a href={links.get('privacyPolicy')} target='_blank' className='text-primary-600 hover:underline dark:text-primary-400'>
|
||||
<FormattedMessage
|
||||
id='registration.privacy'
|
||||
defaultMessage='Privacy Policy'
|
||||
/>
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
) : null}
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Registration;
|
|
@ -1,53 +0,0 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
import React from 'react';
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { fireEvent, render, screen } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import AgeVerification from '../age-verification';
|
||||
|
||||
describe('<AgeVerification />', () => {
|
||||
let store: any;
|
||||
|
||||
beforeEach(() => {
|
||||
store = {
|
||||
verification: ImmutableMap({
|
||||
ageMinimum: 13,
|
||||
}),
|
||||
};
|
||||
|
||||
__stub(mock => {
|
||||
mock.onPost('/api/v1/pepe/verify_age/confirm')
|
||||
.reply(200, {});
|
||||
});
|
||||
});
|
||||
|
||||
it('successfully renders the Birthday step', async() => {
|
||||
render(
|
||||
<AgeVerification />,
|
||||
{},
|
||||
store,
|
||||
);
|
||||
expect(screen.getByRole('heading')).toHaveTextContent('Enter your birth date');
|
||||
});
|
||||
|
||||
it('selects a date', async() => {
|
||||
render(
|
||||
<AgeVerification />,
|
||||
{},
|
||||
store,
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('datepicker-year'),
|
||||
screen.getByRole('option', { name: '2020' }),
|
||||
);
|
||||
|
||||
fireEvent.submit(
|
||||
screen.getByRole('button'), {
|
||||
preventDefault: () => {},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,68 +0,0 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { fireEvent, render, screen, waitFor } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import EmailVerification from '../email-verification';
|
||||
|
||||
describe('<EmailVerification />', () => {
|
||||
it('successfully renders the Email step', async() => {
|
||||
render(<EmailVerification />);
|
||||
expect(screen.getByRole('heading')).toHaveTextContent('Enter your email address');
|
||||
});
|
||||
|
||||
describe('with valid data', () => {
|
||||
beforeEach(() => {
|
||||
__stub(mock => {
|
||||
mock.onPost('/api/v1/pepe/verify_email/request')
|
||||
.reply(200, {});
|
||||
});
|
||||
});
|
||||
|
||||
it('successfully submits', async() => {
|
||||
render(<EmailVerification />);
|
||||
|
||||
await userEvent.type(screen.getByLabelText('E-mail address'), 'foo@bar.com{enter}');
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.submit(
|
||||
screen.getByTestId('button'), {
|
||||
preventDefault: () => {},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('button')).toHaveTextContent('Resend verification email');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with invalid data', () => {
|
||||
beforeEach(() => {
|
||||
__stub(mock => {
|
||||
mock.onPost('/api/v1/pepe/verify_email/request')
|
||||
.reply(422, {
|
||||
error: 'email_taken',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders errors', async() => {
|
||||
render(<EmailVerification />);
|
||||
|
||||
await userEvent.type(screen.getByLabelText('E-mail address'), 'foo@bar.com{enter}');
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.submit(
|
||||
screen.getByTestId('button'), {
|
||||
preventDefault: () => {},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('form-group-error')).toHaveTextContent('is taken');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,120 +0,0 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { fireEvent, render, screen, waitFor } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import SmsVerification from '../sms-verification';
|
||||
|
||||
describe('<SmsVerification />', () => {
|
||||
it('successfully renders the SMS step', async() => {
|
||||
render(<SmsVerification />);
|
||||
expect(screen.getByRole('heading')).toHaveTextContent('Enter your phone number');
|
||||
});
|
||||
|
||||
describe('with valid data', () => {
|
||||
beforeEach(() => {
|
||||
__stub(mock => {
|
||||
mock.onPost('/api/v1/pepe/verify_sms/request').reply(200, {});
|
||||
});
|
||||
});
|
||||
|
||||
it('successfully submits', async() => {
|
||||
__stub(mock => {
|
||||
mock.onPost('/api/v1/pepe/verify_sms/confirm').reply(200, {});
|
||||
});
|
||||
|
||||
render(<SmsVerification />);
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Phone number'), '+1 (555) 555-5555');
|
||||
await waitFor(() => {
|
||||
fireEvent.submit(
|
||||
screen.getByRole('button', { name: 'Next' }), {
|
||||
preventDefault: () => {},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('heading')).toHaveTextContent('Verification code');
|
||||
expect(screen.getByTestId('toast')).toHaveTextContent('A verification code has been sent to your phone number.');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
toast.remove();
|
||||
});
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Please enter verification code. Digit 1'), '1');
|
||||
await userEvent.type(screen.getByLabelText('Digit 2'), '2');
|
||||
await userEvent.type(screen.getByLabelText('Digit 3'), '3');
|
||||
await userEvent.type(screen.getByLabelText('Digit 4'), '4');
|
||||
await userEvent.type(screen.getByLabelText('Digit 5'), '5');
|
||||
await userEvent.type(screen.getByLabelText('Digit 6'), '6');
|
||||
});
|
||||
|
||||
it('handle expired tokens', async() => {
|
||||
__stub(mock => {
|
||||
mock.onPost('/api/v1/pepe/verify_sms/confirm').reply(422, {});
|
||||
});
|
||||
|
||||
render(<SmsVerification />);
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Phone number'), '+1 (555) 555-5555');
|
||||
await waitFor(() => {
|
||||
fireEvent.submit(
|
||||
screen.getByRole('button', { name: 'Next' }), {
|
||||
preventDefault: () => {},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('heading')).toHaveTextContent('Verification code');
|
||||
expect(screen.getByTestId('toast')).toHaveTextContent('A verification code has been sent to your phone number.');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
toast.remove();
|
||||
});
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Please enter verification code. Digit 1'), '1');
|
||||
await userEvent.type(screen.getByLabelText('Digit 2'), '2');
|
||||
await userEvent.type(screen.getByLabelText('Digit 3'), '3');
|
||||
await userEvent.type(screen.getByLabelText('Digit 4'), '4');
|
||||
await userEvent.type(screen.getByLabelText('Digit 5'), '5');
|
||||
await userEvent.type(screen.getByLabelText('Digit 6'), '6');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('toast')).toHaveTextContent('Your SMS token has expired.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with invalid data', () => {
|
||||
beforeEach(() => {
|
||||
__stub(mock => {
|
||||
mock.onPost('/api/v1/pepe/verify_sms/request')
|
||||
.reply(422, {});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders errors', async() => {
|
||||
render(<SmsVerification />);
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Phone number'), '+1 (555) 555-5555');
|
||||
await waitFor(() => {
|
||||
fireEvent.submit(
|
||||
screen.getByRole('button', { name: 'Next' }), {
|
||||
preventDefault: () => {},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('toast')).toHaveTextContent('Failed to send SMS message to your phone number.');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,84 +0,0 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { verifyAge } from 'soapbox/actions/verification';
|
||||
import { Button, Datepicker, Form, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
const messages = defineMessages({
|
||||
fail: {
|
||||
id: 'age_verification.fail',
|
||||
defaultMessage: 'You must be {ageMinimum, plural, one {# year} other {# years}} old or older.',
|
||||
},
|
||||
});
|
||||
|
||||
function meetsAgeMinimum(birthday: Date, ageMinimum: number) {
|
||||
const month = birthday.getUTCMonth();
|
||||
const day = birthday.getUTCDate();
|
||||
const year = birthday.getUTCFullYear();
|
||||
|
||||
return new Date(year + ageMinimum, month, day) <= new Date();
|
||||
}
|
||||
|
||||
const AgeVerification = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const instance = useInstance();
|
||||
|
||||
const isLoading = useAppSelector((state) => state.verification.isLoading) as boolean;
|
||||
const ageMinimum = useAppSelector((state) => state.verification.ageMinimum) as any;
|
||||
|
||||
const [date, setDate] = React.useState<Date>();
|
||||
const isValid = typeof date === 'object';
|
||||
|
||||
const onChange = React.useCallback((date: Date) => setDate(date), []);
|
||||
|
||||
const handleSubmit: React.FormEventHandler = React.useCallback((event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const birthday = new Date(date!);
|
||||
|
||||
if (meetsAgeMinimum(birthday, ageMinimum)) {
|
||||
dispatch(verifyAge(birthday));
|
||||
} else {
|
||||
toast.error(intl.formatMessage(messages.fail, { ageMinimum }));
|
||||
}
|
||||
}, [date, ageMinimum]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='-mx-4 mb-4 border-b border-solid border-gray-200 pb-4 dark:border-gray-800 sm:-mx-10 sm:pb-10'>
|
||||
<h1 className='text-center text-2xl font-bold'>
|
||||
<FormattedMessage id='age_verification.header' defaultMessage='Enter your birth date' />
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className='mx-auto sm:pt-10 md:w-2/3'>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Datepicker onChange={onChange} />
|
||||
|
||||
<Text theme='muted' size='sm'>
|
||||
<FormattedMessage
|
||||
id='age_verification.body'
|
||||
defaultMessage='{siteTitle} requires users to be at least {ageMinimum, plural, one {# year} other {# years}} old to access its platform. Anyone under the age of {ageMinimum, plural, one {# year} other {# years}} old cannot access this platform.'
|
||||
values={{
|
||||
siteTitle: instance.title,
|
||||
ageMinimum,
|
||||
}}
|
||||
/>
|
||||
|
||||
</Text>
|
||||
|
||||
<div className='text-center'>
|
||||
<Button block theme='primary' type='submit' disabled={isLoading || !isValid}>
|
||||
<FormattedMessage id='onboarding.next' defaultMessage='Next' />
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgeVerification;
|
|
@ -1,146 +0,0 @@
|
|||
import { AxiosError } from 'axios';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { checkEmailVerification, postEmailVerification, requestEmailVerification } from 'soapbox/actions/verification';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import { Button, Form, FormGroup, Input, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
const messages = defineMessages({
|
||||
verificationSuccess: { id: 'email_verification.success', defaultMessage: 'Verification email sent successfully.' },
|
||||
verificationFail: { id: 'email_verification.fail', defaultMessage: 'Failed to request email verification.' },
|
||||
verificationFailTakenAlert: { id: 'email_verifilcation.exists', defaultMessage: 'This email has already been taken.' },
|
||||
verificationFailTaken: { id: 'email_verification.taken', defaultMessage: 'is taken' },
|
||||
emailLabel: { id: 'email_verification.email.label', defaultMessage: 'E-mail address' },
|
||||
});
|
||||
|
||||
const Statuses = {
|
||||
IDLE: 'IDLE',
|
||||
REQUESTED: 'REQUESTED',
|
||||
FAIL: 'FAIL',
|
||||
};
|
||||
|
||||
const EMAIL_REGEX = /^[^@\s]+@[^@\s]+$/;
|
||||
|
||||
interface IEmailSent {
|
||||
handleSubmit: React.FormEventHandler
|
||||
}
|
||||
|
||||
const EmailSent: React.FC<IEmailSent> = ({ handleSubmit }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const checkEmailConfirmation = () => {
|
||||
dispatch(checkEmailVerification())
|
||||
.then(() => dispatch(postEmailVerification()))
|
||||
.catch(() => null);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const intervalId = setInterval(() => checkEmailConfirmation(), 2500);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='mx-auto flex flex-col items-center justify-center sm:pt-10'>
|
||||
<Icon src={require('@tabler/icons/send.svg')} className='mb-5 h-12 w-12 text-primary-600 dark:text-primary-400' />
|
||||
|
||||
<div className='mb-4 space-y-2 text-center'>
|
||||
<Text weight='bold' size='3xl'>We sent you an email</Text>
|
||||
<Text theme='muted'>Click on the link in the email to validate your email.</Text>
|
||||
</div>
|
||||
|
||||
<Button theme='tertiary' onClick={handleSubmit}>Resend verification email</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EmailVerification = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const isLoading = useAppSelector((state) => state.verification.isLoading) as boolean;
|
||||
|
||||
const [email, setEmail] = React.useState('');
|
||||
const [status, setStatus] = React.useState(Statuses.IDLE);
|
||||
const [errors, setErrors] = React.useState<Array<string>>([]);
|
||||
|
||||
const isValid = email.length > 0 && EMAIL_REGEX.test(email);
|
||||
|
||||
const onChange: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((event) => {
|
||||
setEmail(event.target.value);
|
||||
}, []);
|
||||
|
||||
const handleSubmit: React.FormEventHandler = React.useCallback((event) => {
|
||||
event.preventDefault();
|
||||
setErrors([]);
|
||||
|
||||
submitEmailForVerification();
|
||||
}, [email]);
|
||||
|
||||
const submitEmailForVerification = () => {
|
||||
return dispatch(requestEmailVerification((email)))
|
||||
.then(() => {
|
||||
setStatus(Statuses.REQUESTED);
|
||||
|
||||
toast.success(intl.formatMessage(messages.verificationSuccess));
|
||||
})
|
||||
.catch((error: AxiosError) => {
|
||||
const errorMessage = (error.response?.data as any)?.error;
|
||||
const isEmailTaken = errorMessage === 'email_taken';
|
||||
let message = intl.formatMessage(messages.verificationFail);
|
||||
|
||||
if (isEmailTaken) {
|
||||
message = intl.formatMessage(messages.verificationFailTakenAlert);
|
||||
} else if (errorMessage) {
|
||||
message = errorMessage;
|
||||
}
|
||||
|
||||
if (isEmailTaken) {
|
||||
setErrors([intl.formatMessage(messages.verificationFailTaken)]);
|
||||
}
|
||||
|
||||
toast.error(message);
|
||||
setStatus(Statuses.FAIL);
|
||||
});
|
||||
};
|
||||
|
||||
if (status === Statuses.REQUESTED) {
|
||||
return <EmailSent handleSubmit={handleSubmit} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='-mx-4 mb-4 border-b border-solid border-gray-200 pb-4 dark:border-gray-800 sm:-mx-10 sm:pb-10'>
|
||||
<h1 className='text-center text-2xl font-bold'>
|
||||
<FormattedMessage id='email_verification.header' defaultMessage='Enter your email address' />
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className='mx-auto sm:w-2/3 sm:pt-10 md:w-1/2'>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<FormGroup labelText={intl.formatMessage(messages.emailLabel)} errors={errors}>
|
||||
<Input
|
||||
type='email'
|
||||
value={email}
|
||||
name='email'
|
||||
onChange={onChange}
|
||||
placeholder='you@email.com'
|
||||
required
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<div className='text-center'>
|
||||
<Button block theme='primary' type='submit' disabled={isLoading || !isValid}>
|
||||
<FormattedMessage id='onboarding.next' defaultMessage='Next' />
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailVerification;
|
|
@ -1,151 +0,0 @@
|
|||
import { AxiosError } from 'axios';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import OtpInput from 'react-otp-input';
|
||||
|
||||
import { confirmPhoneVerification, requestPhoneVerification } from 'soapbox/actions/verification';
|
||||
import { Button, Form, FormGroup, PhoneInput, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
const messages = defineMessages({
|
||||
verificationInvalid: { id: 'sms_verification.invalid', defaultMessage: 'Please enter a valid phone number.' },
|
||||
verificationSuccess: { id: 'sms_verification.success', defaultMessage: 'A verification code has been sent to your phone number.' },
|
||||
verificationFail: { id: 'sms_verification.fail', defaultMessage: 'Failed to send SMS message to your phone number.' },
|
||||
verificationExpired: { id: 'sms_verification.expired', defaultMessage: 'Your SMS token has expired.' },
|
||||
phoneLabel: { id: 'sms_verification.phone.label', defaultMessage: 'Phone number' },
|
||||
});
|
||||
|
||||
const Statuses = {
|
||||
IDLE: 'IDLE',
|
||||
REQUESTED: 'REQUESTED',
|
||||
FAIL: 'FAIL',
|
||||
};
|
||||
|
||||
const SmsVerification = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const isLoading = useAppSelector((state) => state.verification.isLoading) as boolean;
|
||||
|
||||
const [phone, setPhone] = React.useState<string>();
|
||||
const [status, setStatus] = React.useState(Statuses.IDLE);
|
||||
const [verificationCode, setVerificationCode] = React.useState('');
|
||||
const [requestedAnother, setAlreadyRequestedAnother] = React.useState(false);
|
||||
|
||||
const isValid = !!phone;
|
||||
|
||||
const onChange = React.useCallback((phone?: string) => {
|
||||
setPhone(phone);
|
||||
}, []);
|
||||
|
||||
const handleSubmit: React.FormEventHandler = React.useCallback((event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!isValid) {
|
||||
setStatus(Statuses.IDLE);
|
||||
toast.error(intl.formatMessage(messages.verificationInvalid));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(requestPhoneVerification(phone!)).then(() => {
|
||||
toast.success(intl.formatMessage(messages.verificationSuccess));
|
||||
setStatus(Statuses.REQUESTED);
|
||||
}).catch((error: AxiosError) => {
|
||||
const message = (error.response?.data as any)?.message || intl.formatMessage(messages.verificationFail);
|
||||
|
||||
toast.error(message);
|
||||
setStatus(Statuses.FAIL);
|
||||
});
|
||||
}, [phone, isValid]);
|
||||
|
||||
const resendVerificationCode: React.MouseEventHandler = React.useCallback((event) => {
|
||||
setAlreadyRequestedAnother(true);
|
||||
handleSubmit(event);
|
||||
}, [isValid]);
|
||||
|
||||
const submitVerification = () => {
|
||||
// TODO: handle proper validation from Pepe -- expired vs invalid
|
||||
dispatch(confirmPhoneVerification(verificationCode))
|
||||
.catch(() => {
|
||||
toast.error(intl.formatMessage(messages.verificationExpired));
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (verificationCode.length === 6) {
|
||||
submitVerification();
|
||||
}
|
||||
}, [verificationCode]);
|
||||
|
||||
if (status === Statuses.REQUESTED) {
|
||||
return (
|
||||
<div>
|
||||
<div className='-mx-4 mb-4 border-b border-solid border-gray-200 pb-4 dark:border-gray-800 sm:-mx-10 sm:pb-10'>
|
||||
<h1 className='text-center text-2xl font-bold'>
|
||||
<FormattedMessage id='sms_verification.sent.header' defaultMessage='Verification code' />
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className='mx-auto space-y-4 sm:w-2/3 sm:pt-10 md:w-1/2'>
|
||||
<Text theme='muted' size='sm' align='center'>
|
||||
<FormattedMessage id='sms_verification.sent.body' defaultMessage='We sent you a 6-digit code via SMS. Enter it below.' />
|
||||
</Text>
|
||||
|
||||
<OtpInput
|
||||
value={verificationCode}
|
||||
onChange={setVerificationCode}
|
||||
numInputs={6}
|
||||
isInputNum
|
||||
shouldAutoFocus
|
||||
isDisabled={isLoading}
|
||||
containerStyle='flex justify-center mt-2 space-x-4'
|
||||
inputStyle='w-10i border-gray-300 dark:bg-gray-800 dark:border-gray-800 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500'
|
||||
/>
|
||||
|
||||
<div className='text-center'>
|
||||
<Button
|
||||
size='sm'
|
||||
type='button'
|
||||
theme='tertiary'
|
||||
onClick={resendVerificationCode}
|
||||
disabled={requestedAnother}
|
||||
>
|
||||
<FormattedMessage id='sms_verification.sent.actions.resend' defaultMessage='Resend verification code?' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='-mx-4 mb-4 border-b border-solid border-gray-200 pb-4 dark:border-gray-800 sm:-mx-10 sm:pb-10'>
|
||||
<h1 className='text-center text-2xl font-bold'>
|
||||
<FormattedMessage id='sms_verification.header' defaultMessage='Enter your phone number' />
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className='mx-auto sm:w-2/3 sm:pt-10 md:w-1/2'>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<FormGroup labelText={intl.formatMessage(messages.phoneLabel)}>
|
||||
<PhoneInput
|
||||
value={phone}
|
||||
onChange={onChange}
|
||||
required
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<div className='text-center'>
|
||||
<Button block theme='primary' type='submit' disabled={isLoading || !isValid}>
|
||||
<FormattedMessage id='onboarding.next' defaultMessage='Next' />
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { SmsVerification as default };
|
|
@ -1,79 +0,0 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { logOut } from 'soapbox/actions/auth';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import LandingGradient from 'soapbox/components/landing-gradient';
|
||||
import SiteLogo from 'soapbox/components/site-logo';
|
||||
import { Button, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useInstance, useOwnAccount } from 'soapbox/hooks';
|
||||
|
||||
const WaitlistPage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const instance = useInstance();
|
||||
|
||||
const { account: me } = useOwnAccount();
|
||||
const isSmsVerified = me?.source?.sms_verified ?? true;
|
||||
|
||||
const onClickLogOut: React.MouseEventHandler = (event) => {
|
||||
event.preventDefault();
|
||||
dispatch(logOut());
|
||||
};
|
||||
|
||||
const openVerifySmsModal = () => dispatch(openModal('VERIFY_SMS'));
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSmsVerified) {
|
||||
openVerifySmsModal();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LandingGradient />
|
||||
|
||||
<main className='relative mx-auto flex h-screen max-w-7xl flex-col px-2 sm:px-6 lg:px-8'>
|
||||
<header className='relative flex h-16 justify-between'>
|
||||
<div className='relative flex flex-1 items-stretch justify-center'>
|
||||
<Link to='/' className='flex shrink-0 cursor-pointer items-center'>
|
||||
<SiteLogo alt='Logo' className='h-7' />
|
||||
</Link>
|
||||
|
||||
<div className='absolute inset-y-0 right-0 flex items-center pr-2'>
|
||||
<Button onClick={onClickLogOut} theme='primary' to='/logout'>
|
||||
<FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className='-mt-16 flex h-full flex-col items-center justify-center'>
|
||||
<div className='max-w-xl'>
|
||||
<Stack space={4}>
|
||||
<img src='/instance/images/waitlist.png' className='mx-auto h-32 w-32' alt='Waitlisted' />
|
||||
|
||||
<Stack space={2}>
|
||||
<Text size='lg' theme='muted' align='center' weight='medium'>
|
||||
<FormattedMessage
|
||||
id='waitlist.body'
|
||||
defaultMessage='Welcome back to {title}! You were previously placed on our waitlist. Please verify your phone number to receive immediate access to your account!'
|
||||
values={{ title: instance.title }}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<div className='text-center'>
|
||||
<Button onClick={openVerifySmsModal} theme='primary'>
|
||||
<FormattedMessage id='waitlist.actions.verify_number' defaultMessage='Verify phone number' />
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WaitlistPage;
|
|
@ -1,46 +0,0 @@
|
|||
import { storeClosed, storeOpen, storePepeClosed, storePepeOpen } from 'soapbox/jest/mock-stores';
|
||||
import { renderHook } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import { useRegistrationStatus } from '../useRegistrationStatus';
|
||||
|
||||
describe('useRegistrationStatus()', () => {
|
||||
test('Registrations open', () => {
|
||||
const { result } = renderHook(useRegistrationStatus, undefined, storeOpen);
|
||||
|
||||
expect(result.current).toMatchObject({
|
||||
isOpen: true,
|
||||
pepeEnabled: false,
|
||||
pepeOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('Registrations closed', () => {
|
||||
const { result } = renderHook(useRegistrationStatus, undefined, storeClosed);
|
||||
|
||||
expect(result.current).toMatchObject({
|
||||
isOpen: false,
|
||||
pepeEnabled: false,
|
||||
pepeOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('Registrations closed, Pepe enabled & open', () => {
|
||||
const { result } = renderHook(useRegistrationStatus, undefined, storePepeOpen);
|
||||
|
||||
expect(result.current).toMatchObject({
|
||||
isOpen: true,
|
||||
pepeEnabled: true,
|
||||
pepeOpen: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('Registrations closed, Pepe enabled & closed', () => {
|
||||
const { result } = renderHook(useRegistrationStatus, undefined, storePepeClosed);
|
||||
|
||||
expect(result.current).toMatchObject({
|
||||
isOpen: false,
|
||||
pepeEnabled: true,
|
||||
pepeOpen: false,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,22 +1,12 @@
|
|||
import { useAppSelector } from './useAppSelector';
|
||||
import { useFeatures } from './useFeatures';
|
||||
import { useInstance } from './useInstance';
|
||||
import { useSoapboxConfig } from './useSoapboxConfig';
|
||||
|
||||
export const useRegistrationStatus = () => {
|
||||
const instance = useInstance();
|
||||
const features = useFeatures();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
|
||||
const pepeOpen = useAppSelector(state => state.verification.instance.get('registrations') === true);
|
||||
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
|
||||
|
||||
return {
|
||||
/** Registrations are open, either through Pepe or traditional account creation. */
|
||||
isOpen: (features.accountCreation && instance.registrations) || (pepeEnabled && pepeOpen),
|
||||
/** Whether Pepe is open. */
|
||||
pepeOpen,
|
||||
/** Whether Pepe is enabled. */
|
||||
pepeEnabled,
|
||||
isOpen: features.accountCreation && instance.registrations,
|
||||
};
|
||||
};
|
|
@ -1,5 +1,3 @@
|
|||
import { fromJS } from 'immutable';
|
||||
|
||||
import alexJson from 'soapbox/__fixtures__/pleroma-account.json';
|
||||
import { normalizeInstance } from 'soapbox/normalizers';
|
||||
|
||||
|
@ -11,20 +9,6 @@ const storeOpen = { instance: normalizeInstance({ registrations: true }) };
|
|||
/** Store with registrations closed. */
|
||||
const storeClosed = { instance: normalizeInstance({ registrations: false }) };
|
||||
|
||||
/** Store with registrations closed, and Pepe enabled & open. */
|
||||
const storePepeOpen = {
|
||||
instance: normalizeInstance({ registrations: false }),
|
||||
soapbox: fromJS({ extensions: { pepe: { enabled: true } } }),
|
||||
verification: { instance: fromJS({ registrations: true }) },
|
||||
};
|
||||
|
||||
/** Store with registrations closed, and Pepe enabled & closed. */
|
||||
const storePepeClosed = {
|
||||
instance: normalizeInstance({ registrations: false }),
|
||||
soapbox: fromJS({ extensions: { pepe: { enabled: true } } }),
|
||||
verification: { instance: fromJS({ registrations: false }) },
|
||||
};
|
||||
|
||||
/** Store with a logged-in user. */
|
||||
const storeLoggedIn = {
|
||||
me: alexJson.id,
|
||||
|
@ -36,7 +20,5 @@ const storeLoggedIn = {
|
|||
export {
|
||||
storeOpen,
|
||||
storeClosed,
|
||||
storePepeOpen,
|
||||
storePepeClosed,
|
||||
storeLoggedIn,
|
||||
};
|
|
@ -1,177 +0,0 @@
|
|||
import { Map as ImmutableMap, Record as ImmutableRecord } from 'immutable';
|
||||
|
||||
import {
|
||||
Challenge,
|
||||
FETCH_CHALLENGES_SUCCESS,
|
||||
FETCH_TOKEN_SUCCESS,
|
||||
SET_CHALLENGES_COMPLETE,
|
||||
SET_LOADING,
|
||||
SET_NEXT_CHALLENGE,
|
||||
} from 'soapbox/actions/verification';
|
||||
|
||||
import reducer from '../verification';
|
||||
|
||||
describe('verfication reducer', () => {
|
||||
it('returns the initial state', () => {
|
||||
expect(reducer(undefined, {} as any)).toMatchObject({
|
||||
ageMinimum: null,
|
||||
currentChallenge: null,
|
||||
isLoading: false,
|
||||
isComplete: false,
|
||||
token: null,
|
||||
instance: ImmutableMap(),
|
||||
});
|
||||
});
|
||||
|
||||
describe('FETCH_CHALLENGES_SUCCESS', () => {
|
||||
it('sets the state', () => {
|
||||
const state = ImmutableRecord({
|
||||
ageMinimum: null,
|
||||
currentChallenge: null,
|
||||
isLoading: true,
|
||||
isComplete: null,
|
||||
token: null,
|
||||
instance: ImmutableMap<string, any>(),
|
||||
})();
|
||||
const action = {
|
||||
type: FETCH_CHALLENGES_SUCCESS,
|
||||
ageMinimum: 13,
|
||||
currentChallenge: 'email',
|
||||
isComplete: false,
|
||||
};
|
||||
const expected = {
|
||||
ageMinimum: 13,
|
||||
currentChallenge: 'email',
|
||||
isLoading: false,
|
||||
isComplete: false,
|
||||
token: null,
|
||||
instance: ImmutableMap(),
|
||||
};
|
||||
|
||||
expect(reducer(state, action)).toMatchObject(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FETCH_TOKEN_SUCCESS', () => {
|
||||
it('sets the state', () => {
|
||||
const state = ImmutableRecord({
|
||||
ageMinimum: null,
|
||||
currentChallenge: 'email' as Challenge,
|
||||
isLoading: true,
|
||||
isComplete: false,
|
||||
token: null,
|
||||
instance: ImmutableMap<string, any>(),
|
||||
})();
|
||||
const action = { type: FETCH_TOKEN_SUCCESS, value: '123' };
|
||||
const expected = {
|
||||
ageMinimum: null,
|
||||
currentChallenge: 'email',
|
||||
isLoading: false,
|
||||
isComplete: false,
|
||||
token: '123',
|
||||
instance: ImmutableMap(),
|
||||
};
|
||||
|
||||
expect(reducer(state, action)).toMatchObject(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SET_CHALLENGES_COMPLETE', () => {
|
||||
it('sets the state', () => {
|
||||
const state = ImmutableRecord({
|
||||
ageMinimum: null,
|
||||
currentChallenge: null,
|
||||
isLoading: true,
|
||||
isComplete: false,
|
||||
token: null,
|
||||
instance: ImmutableMap<string, any>(),
|
||||
})();
|
||||
const action = { type: SET_CHALLENGES_COMPLETE };
|
||||
const expected = {
|
||||
ageMinimum: null,
|
||||
currentChallenge: null,
|
||||
isLoading: false,
|
||||
isComplete: true,
|
||||
token: null,
|
||||
instance: ImmutableMap(),
|
||||
};
|
||||
|
||||
expect(reducer(state, action)).toMatchObject(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SET_NEXT_CHALLENGE', () => {
|
||||
it('sets the state', () => {
|
||||
const state = ImmutableRecord({
|
||||
ageMinimum: null,
|
||||
currentChallenge: null,
|
||||
isLoading: true,
|
||||
isComplete: false,
|
||||
token: null,
|
||||
instance: ImmutableMap<string, any>(),
|
||||
})();
|
||||
const action = {
|
||||
type: SET_NEXT_CHALLENGE,
|
||||
challenge: 'sms',
|
||||
};
|
||||
const expected = {
|
||||
ageMinimum: null,
|
||||
currentChallenge: 'sms',
|
||||
isLoading: false,
|
||||
isComplete: false,
|
||||
token: null,
|
||||
instance: ImmutableMap(),
|
||||
};
|
||||
|
||||
expect(reducer(state, action)).toMatchObject(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SET_LOADING with no value', () => {
|
||||
it('sets the state', () => {
|
||||
const state = ImmutableRecord({
|
||||
ageMinimum: null,
|
||||
currentChallenge: null,
|
||||
isLoading: false,
|
||||
isComplete: false,
|
||||
token: null,
|
||||
instance: ImmutableMap<string, any>(),
|
||||
})();
|
||||
const action = { type: SET_LOADING };
|
||||
const expected = {
|
||||
ageMinimum: null,
|
||||
currentChallenge: null,
|
||||
isLoading: true,
|
||||
isComplete: false,
|
||||
token: null,
|
||||
instance: ImmutableMap(),
|
||||
};
|
||||
|
||||
expect(reducer(state, action)).toMatchObject(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SET_LOADING with a value', () => {
|
||||
it('sets the state', () => {
|
||||
const state = ImmutableRecord({
|
||||
ageMinimum: null,
|
||||
currentChallenge: null,
|
||||
isLoading: true,
|
||||
isComplete: false,
|
||||
token: null,
|
||||
instance: ImmutableMap<string, any>(),
|
||||
})();
|
||||
const action = { type: SET_LOADING, value: false };
|
||||
const expected = {
|
||||
ageMinimum: null,
|
||||
currentChallenge: null,
|
||||
isLoading: false,
|
||||
isComplete: false,
|
||||
token: null,
|
||||
instance: ImmutableMap(),
|
||||
};
|
||||
|
||||
expect(reducer(state, action)).toMatchObject(expected);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -64,7 +64,6 @@ import timelines from './timelines';
|
|||
import trending_statuses from './trending-statuses';
|
||||
import trends from './trends';
|
||||
import user_lists from './user-lists';
|
||||
import verification from './verification';
|
||||
|
||||
const reducers = {
|
||||
accounts_meta,
|
||||
|
@ -127,7 +126,6 @@ const reducers = {
|
|||
trending_statuses,
|
||||
trends,
|
||||
user_lists,
|
||||
verification,
|
||||
};
|
||||
|
||||
// Build a default state from all reducers: it has the key and `undefined`
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
import { Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
|
||||
|
||||
import {
|
||||
PEPE_FETCH_INSTANCE_SUCCESS,
|
||||
FETCH_CHALLENGES_SUCCESS,
|
||||
FETCH_TOKEN_SUCCESS,
|
||||
SET_CHALLENGES_COMPLETE,
|
||||
SET_LOADING,
|
||||
SET_NEXT_CHALLENGE,
|
||||
Challenge,
|
||||
} from '../actions/verification';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
const ReducerRecord = ImmutableRecord({
|
||||
ageMinimum: null as string | null,
|
||||
currentChallenge: null as Challenge | null,
|
||||
isLoading: false,
|
||||
isComplete: false as boolean | null,
|
||||
token: null as string | null,
|
||||
instance: ImmutableMap<string, any>(),
|
||||
});
|
||||
|
||||
export default function verification(state = ReducerRecord(), action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case PEPE_FETCH_INSTANCE_SUCCESS:
|
||||
return state.set('instance', ImmutableMap(fromJS(action.instance)));
|
||||
case FETCH_CHALLENGES_SUCCESS:
|
||||
return state
|
||||
.set('ageMinimum', action.ageMinimum)
|
||||
.set('currentChallenge', action.currentChallenge)
|
||||
.set('isLoading', false)
|
||||
.set('isComplete', action.isComplete);
|
||||
case FETCH_TOKEN_SUCCESS:
|
||||
return state
|
||||
.set('isLoading', false)
|
||||
.set('token', action.value);
|
||||
case SET_CHALLENGES_COMPLETE:
|
||||
return state
|
||||
.set('isLoading', false)
|
||||
.set('isComplete', true);
|
||||
case SET_NEXT_CHALLENGE:
|
||||
return state
|
||||
.set('currentChallenge', action.challenge)
|
||||
.set('isLoading', false);
|
||||
case SET_LOADING:
|
||||
return state.set('isLoading', typeof action.value === 'boolean' ? action.value : true);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -734,14 +734,6 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
*/
|
||||
paginatedContext: v.software === TRUTHSOCIAL,
|
||||
|
||||
/**
|
||||
* Require minimum password requirements.
|
||||
* - 8 characters
|
||||
* - 1 uppercase
|
||||
* - 1 lowercase
|
||||
*/
|
||||
passwordRequirements: v.software === TRUTHSOCIAL,
|
||||
|
||||
/**
|
||||
* Displays a form to follow a user when logged out.
|
||||
* @see POST /main/ostatus
|
||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -5905,11 +5905,6 @@ li@^1.3.0:
|
|||
resolved "https://registry.yarnpkg.com/li/-/li-1.3.0.tgz#22c59bcaefaa9a8ef359cf759784e4bf106aea1b"
|
||||
integrity sha512-z34TU6GlMram52Tss5mt1m//ifRIpKH5Dqm7yUVOdHI+BQCs9qGPHFaCUTIzsWX7edN30aa2WrPwR7IO10FHaw==
|
||||
|
||||
libphonenumber-js@^1.10.8:
|
||||
version "1.10.8"
|
||||
resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.8.tgz#21925db0f16d4f1553dff2bbc62afdaeb03f21f0"
|
||||
integrity sha512-MGgHrKRGE7sg7y0DikHybRDgTXcYv4HL+WwhDm5UAiChCNb5tcy5OEaU8XTTt5bDBwhZGCJNxoGMVBpZ4RfhIg==
|
||||
|
||||
lie@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
|
||||
|
@ -7330,11 +7325,6 @@ react-onclickoutside@^6.12.0:
|
|||
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.12.1.tgz#92dddd28f55e483a1838c5c2930e051168c1e96b"
|
||||
integrity sha512-a5Q7CkWznBRUWPmocCvE8b6lEYw1s6+opp/60dCunhO+G6E4tDTO2Sd2jKE+leEnnrLAE2Wj5DlDHNqj5wPv1Q==
|
||||
|
||||
react-otp-input@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/react-otp-input/-/react-otp-input-2.4.0.tgz#0f0a3de1d8c8d564e2e4fbe5d6b7b56e29e3a6e6"
|
||||
integrity sha512-AIgl7u4sS9BTNCxX1xlaS5fPWay/Zml8Ho5LszXZKXrH1C/TiFsTQGmtl13UecQYO3mSF3HUzG2rrDf0sjEFmg==
|
||||
|
||||
react-overlays@^0.9.0:
|
||||
version "0.9.3"
|
||||
resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-0.9.3.tgz#5bac8c1e9e7e057a125181dee2d784864dd62902"
|
||||
|
|
Loading…
Reference in a new issue