Merge branch 'rm-pepe' into 'main'

Remove Truth Social registration

See merge request soapbox-pub/soapbox!2708
environments/review-main-yi2y9f/deployments/3942
Alex Gleason 2023-09-19 03:42:35 +00:00
commit 73ae9ae4c4
35 zmienionych plików z 16 dodań i 2622 usunięć

Wyświetl plik

@ -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",

Wyświetl plik

@ -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,
};

Wyświetl plik

@ -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';

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;

Wyświetl plik

@ -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} />

Wyświetl plik

@ -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} />

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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();
});
});

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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,
};

Wyświetl plik

@ -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;

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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');
});
});
});

Wyświetl plik

@ -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();
});
});
});

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;

Wyświetl plik

@ -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: () => {},
},
);
});
});

Wyświetl plik

@ -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');
});
});
});
});

Wyświetl plik

@ -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.');
});
});
});
});

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;

Wyświetl plik

@ -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 };

Wyświetl plik

@ -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;

Wyświetl plik

@ -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,
});
});
});

Wyświetl plik

@ -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,
};
};

Wyświetl plik

@ -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,
};

Wyświetl plik

@ -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);
});
});
});

Wyświetl plik

@ -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`

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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"