diff --git a/package.json b/package.json index e655c8658..86f8d0b73 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/actions/verification.ts b/src/actions/verification.ts deleted file mode 100644 index 2273bb499..000000000 --- a/src/actions/verification.ts +++ /dev/null @@ -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, -}; diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index d66c98195..c565641f4 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -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'; diff --git a/src/components/ui/phone-input/country-code-dropdown.tsx b/src/components/ui/phone-input/country-code-dropdown.tsx deleted file mode 100644 index 208db6952..000000000 --- a/src/components/ui/phone-input/country-code-dropdown.tsx +++ /dev/null @@ -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 = ({ countryCode, onChange }) => { - return ( - - ); -}; - -export default CountryCodeDropdown; diff --git a/src/components/ui/phone-input/phone-input.tsx b/src/components/ui/phone-input/phone-input.tsx deleted file mode 100644 index d3072f2a1..000000000 --- a/src/components/ui/phone-input/phone-input.tsx +++ /dev/null @@ -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, '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 = (props) => { - const { value, onChange, defaultCountryCode = '1', ...rest } = props; - - const [countryCode, setCountryCode] = useState(defaultCountryCode); - const [nationalNumber, setNationalNumber] = useState(''); - - const handleChange: React.ChangeEventHandler = ({ 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 ( - - } - {...rest} - /> - ); -}; - -export default PhoneInput; diff --git a/src/containers/soapbox.tsx b/src/containers/soapbox.tsx index 9f8081c9f..92911789d 100644 --- a/src/containers/soapbox.tsx +++ b/src/containers/soapbox.tsx @@ -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 = () => ( - - - {/* Redirect signup route depending on Pepe enablement. */} - {/* We should prefer using /signup in components. */} - {pepeEnabled ? ( - - ) : ( - - )} - - {waitlisted && ( - ( - - {(Component) => } - - )} - /> - )} - {!me && (redirectRootNoLogin ? : )} @@ -149,10 +117,6 @@ const SoapboxMount = () => { )} - {pepeEnabled && ( - - )} - diff --git a/src/features/auth-layout/index.tsx b/src/features/auth-layout/index.tsx index 9ad78c184..608e8fe83 100644 --- a/src/features/auth-layout/index.tsx +++ b/src/features/auth-layout/index.tsx @@ -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 && } - - diff --git a/src/features/auth-login/components/password-reset-confirm.tsx b/src/features/auth-login/components/password-reset-confirm.tsx index c931ac788..78ef26fd7 100644 --- a/src/features/auth-login/components/password-reset-confirm.tsx +++ b/src/features/auth-login/components/password-reset-confirm.tsx @@ -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(passwordRequirements ? false : true); const isLoading = status === Statuses.LOADING; @@ -75,14 +72,10 @@ const PasswordResetConfirm = () => { onChange={onChange} required /> - - {passwordRequirements && ( - - )} - diff --git a/src/features/edit-password/index.tsx b/src/features/edit-password/index.tsx index 026515d53..7761f77a2 100644 --- a/src/features/edit-password/index.tsx +++ b/src/features/edit-password/index.tsx @@ -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(passwordRequirements ? false : true); const { currentPassword, newPassword, newPasswordConfirmation } = state; @@ -73,10 +69,6 @@ const EditPassword = () => { onChange={handleInputChange} value={newPassword} /> - - {passwordRequirements && ( - - )} @@ -93,7 +85,7 @@ const EditPassword = () => { {intl.formatMessage(messages.cancel)} - diff --git a/src/features/landing-page/__tests__/landing-page.test.tsx b/src/features/landing-page/__tests__/landing-page.test.tsx index 6e96672ba..bbc96deeb 100644 --- a/src/features/landing-page/__tests__/landing-page.test.tsx +++ b/src/features/landing-page/__tests__/landing-page.test.tsx @@ -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('', () => { it('renders a RegistrationForm for an open Pleroma instance', () => { @@ -21,7 +20,6 @@ describe('', () => { 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('', () => { 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(, 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(, undefined, state); - - expect(screen.queryByTestId('registrations-closed')).toBeInTheDocument(); - expect(screen.queryByTestId('registrations-pepe')).not.toBeInTheDocument(); - expect(screen.queryByTestId('registrations-open')).not.toBeInTheDocument(); }); }); diff --git a/src/features/landing-page/index.tsx b/src/features/landing-page/index.tsx index ce1180063..a31d7d313 100644 --- a/src/features/landing-page/index.tsx +++ b/src/features/landing-page/index.tsx @@ -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 ( - - - - - - - - - - - - - - - ); - }; - // 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 { diff --git a/src/features/ui/components/modal-root.tsx b/src/features/ui/components/modal-root.tsx index 86d1b55cc..f43e2ce03 100644 --- a/src/features/ui/components/modal-root.tsx +++ b/src/features/ui/components/modal-root.tsx @@ -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, }; diff --git a/src/features/ui/components/modals/verify-sms-modal.tsx b/src/features/ui/components/modals/verify-sms-modal.tsx deleted file mode 100644 index b092763a4..000000000 --- a/src/features/ui/components/modals/verify-sms-modal.tsx +++ /dev/null @@ -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 = ({ 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.IDLE); - const [phone, setPhone] = useState(); - 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) => { - setAlreadyRequestedAnother(true); - handleSubmit(event as React.MouseEvent); - }; - - 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 ( - - - - ); - case Statuses.READY: - return ( - }> - - - ); - case Statuses.REQUESTED: - return ( - <> - - - - - - - ); - 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 ( - - } - 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 ? ( - - ) : undefined} - secondaryDisabled={requestedAnother} - > - - {renderModalBody()} - - - ); -}; - -export default VerifySmsModal; diff --git a/src/features/ui/util/async-components.ts b/src/features/ui/util/async-components.ts index a364bc33a..19a7c3b36 100644 --- a/src/features/ui/util/async-components.ts +++ b/src/features/ui/util/async-components.ts @@ -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() { diff --git a/src/features/verification/__tests__/index.test.tsx b/src/features/verification/__tests__/index.test.tsx deleted file mode 100644 index e24aee442..000000000 --- a/src/features/verification/__tests__/index.test.tsx +++ /dev/null @@ -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 = () => ( - - - Homepage - -); - -const renderComponent = (store: any) => render( - , - {}, - store, - { initialEntries: ['/verify'] }, -); - -describe('', () => { - 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'); - }); - }); -}); diff --git a/src/features/verification/__tests__/registration.test.tsx b/src/features/verification/__tests__/registration.test.tsx deleted file mode 100644 index 8dfe3083f..000000000 --- a/src/features/verification/__tests__/registration.test.tsx +++ /dev/null @@ -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('', () => { - it('renders', () => { - render(); - - 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(); - - 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(); - - 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(); - - 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(); - - 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(); - - 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(); - - fireEvent.change(screen.getByTestId('password-input'), { target: { value: 'Passwor' } }); - expect(screen.getByTestId('button')).toBeDisabled(); - }); - }); -}); diff --git a/src/features/verification/components/password-indicator.tsx b/src/features/verification/components/password-indicator.tsx deleted file mode 100644 index 7b804d3d6..000000000 --- a/src/features/verification/components/password-indicator.tsx +++ /dev/null @@ -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 ( - - - - - - - - ); -}; - -export default PasswordIndicator; diff --git a/src/features/verification/email-passthru.tsx b/src/features/verification/email-passthru.tsx deleted file mode 100644 index 86152f12d..000000000 --- a/src/features/verification/email-passthru.tsx +++ /dev/null @@ -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 ( - - - - {intl.formatMessage(messages.emailConfirmedHeading)} - - - {intl.formatMessage(messages.emailConfirmedBody, { bold: same device })} - - - ); -}; - -const GenericFail = () => { - const intl = useIntl(); - - return ( - - - - {intl.formatMessage(messages.genericFailHeading)} - - - {intl.formatMessage(messages.genericFailBody)} - - - ); -}; - -const TokenNotFound = () => { - const intl = useIntl(); - - return ( - - - - {intl.formatMessage(messages.tokenNotFoundHeading)} - - - {intl.formatMessage(messages.tokenNotFoundBody, { bold: same device })} - - - - ); -}; - -const TokenExpired = () => { - const intl = useIntl(); - - return ( - - - - {intl.formatMessage(messages.tokenExpiredHeading)} - - - {intl.formatMessage(messages.tokenExpiredBody, { bold: same device })} - - - ); -}; - -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) => { - 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 ; - case Statuses.TOKEN_EXPIRED: - return ; - case Statuses.TOKEN_NOT_FOUND: - return ; - case Statuses.GENERIC_FAIL: - return ; - default: - return ; - } -}; - -export default EmailPassThru; diff --git a/src/features/verification/index.tsx b/src/features/verification/index.tsx deleted file mode 100644 index 88c9dc435..000000000 --- a/src/features/verification/index.tsx +++ /dev/null @@ -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 ; - } - - if (isVerificationComplete) { - return ( - - ); - } - - if (!currentChallenge) { - return null; - } - - return ( - - ); -}; - -export default Verification; diff --git a/src/features/verification/registration.tsx b/src/features/verification/registration.tsx deleted file mode 100644 index bee939ec4..000000000 --- a/src/features/verification/registration.tsx +++ /dev/null @@ -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(false); - const [hasValidPassword, setHasValidPassword] = React.useState(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 = React.useCallback((event) => { - event.persist(); - - setState((prevState) => ({ ...prevState, [event.target.name]: event.target.value })); - }, []); - - if (shouldRedirect) { - const redirectUri = getRedirectUrl(); - return ; - } - - return ( -
-
-

- -

-
- -
-
- - - - - - - - - - -
- - - {(links.get('termsOfService') && links.get('privacyPolicy')) ? ( - - - - - ), - privacy: ( - - - - ), - }} - /> - - ) : null} -
-
-
-
- ); -}; - -export default Registration; diff --git a/src/features/verification/steps/__tests__/age-verification.test.tsx b/src/features/verification/steps/__tests__/age-verification.test.tsx deleted file mode 100644 index f310a0741..000000000 --- a/src/features/verification/steps/__tests__/age-verification.test.tsx +++ /dev/null @@ -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('', () => { - 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( - , - {}, - store, - ); - expect(screen.getByRole('heading')).toHaveTextContent('Enter your birth date'); - }); - - it('selects a date', async() => { - render( - , - {}, - store, - ); - - await userEvent.selectOptions( - screen.getByTestId('datepicker-year'), - screen.getByRole('option', { name: '2020' }), - ); - - fireEvent.submit( - screen.getByRole('button'), { - preventDefault: () => {}, - }, - ); - }); -}); diff --git a/src/features/verification/steps/__tests__/email-verification.test.tsx b/src/features/verification/steps/__tests__/email-verification.test.tsx deleted file mode 100644 index 38e7cf8a7..000000000 --- a/src/features/verification/steps/__tests__/email-verification.test.tsx +++ /dev/null @@ -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('', () => { - it('successfully renders the Email step', async() => { - render(); - 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(); - - 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(); - - 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'); - }); - }); - }); -}); diff --git a/src/features/verification/steps/__tests__/sms-verification.test.tsx b/src/features/verification/steps/__tests__/sms-verification.test.tsx deleted file mode 100644 index d837f46b7..000000000 --- a/src/features/verification/steps/__tests__/sms-verification.test.tsx +++ /dev/null @@ -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('', () => { - it('successfully renders the SMS step', async() => { - render(); - 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(); - - 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(); - - 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(); - - 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.'); - }); - }); - }); -}); diff --git a/src/features/verification/steps/age-verification.tsx b/src/features/verification/steps/age-verification.tsx deleted file mode 100644 index 5129f5464..000000000 --- a/src/features/verification/steps/age-verification.tsx +++ /dev/null @@ -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(); - 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 ( -
-
-

- -

-
- -
-
- - - - - - - -
- -
- -
-
- ); -}; - -export default AgeVerification; diff --git a/src/features/verification/steps/email-verification.tsx b/src/features/verification/steps/email-verification.tsx deleted file mode 100644 index a7c15d686..000000000 --- a/src/features/verification/steps/email-verification.tsx +++ /dev/null @@ -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 = ({ handleSubmit }) => { - const dispatch = useAppDispatch(); - - const checkEmailConfirmation = () => { - dispatch(checkEmailVerification()) - .then(() => dispatch(postEmailVerification())) - .catch(() => null); - }; - - React.useEffect(() => { - const intervalId = setInterval(() => checkEmailConfirmation(), 2500); - - return () => clearInterval(intervalId); - }, []); - - return ( -
- - -
- We sent you an email - Click on the link in the email to validate your email. -
- - -
- ); -}; - -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>([]); - - const isValid = email.length > 0 && EMAIL_REGEX.test(email); - - const onChange: React.ChangeEventHandler = 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 ; - } - - return ( -
-
-

- -

-
- -
-
- - - - -
- -
-
-
-
- ); -}; - -export default EmailVerification; diff --git a/src/features/verification/steps/sms-verification.tsx b/src/features/verification/steps/sms-verification.tsx deleted file mode 100644 index 0266b1581..000000000 --- a/src/features/verification/steps/sms-verification.tsx +++ /dev/null @@ -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(); - 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 ( -
-
-

- -

-
- -
- - - - - - -
- -
-
-
- ); - } - - return ( -
-
-

- -

-
- -
-
- - - - -
- -
-
-
-
- ); -}; - -export { SmsVerification as default }; diff --git a/src/features/verification/waitlist-page.tsx b/src/features/verification/waitlist-page.tsx deleted file mode 100644 index 431c76698..000000000 --- a/src/features/verification/waitlist-page.tsx +++ /dev/null @@ -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 ( -
- - -
-
-
- - - - -
- -
-
-
- -
-
- - Waitlisted - - - - - - -
- -
-
-
-
-
-
-
- ); -}; - -export default WaitlistPage; diff --git a/src/hooks/__tests__/useRegistrationStatus.test.ts b/src/hooks/__tests__/useRegistrationStatus.test.ts deleted file mode 100644 index 465ca3992..000000000 --- a/src/hooks/__tests__/useRegistrationStatus.test.ts +++ /dev/null @@ -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, - }); - }); -}); diff --git a/src/hooks/useRegistrationStatus.ts b/src/hooks/useRegistrationStatus.ts index 6ede86941..0f1242d1e 100644 --- a/src/hooks/useRegistrationStatus.ts +++ b/src/hooks/useRegistrationStatus.ts @@ -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, }; }; \ No newline at end of file diff --git a/src/jest/mock-stores.tsx b/src/jest/mock-stores.tsx index e8969780b..0dc4bae79 100644 --- a/src/jest/mock-stores.tsx +++ b/src/jest/mock-stores.tsx @@ -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, }; \ No newline at end of file diff --git a/src/reducers/__tests__/verification.test.ts b/src/reducers/__tests__/verification.test.ts deleted file mode 100644 index f503de443..000000000 --- a/src/reducers/__tests__/verification.test.ts +++ /dev/null @@ -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(), - })(); - 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(), - })(); - 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(), - })(); - 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(), - })(); - 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(), - })(); - 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(), - })(); - 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); - }); - }); -}); diff --git a/src/reducers/index.ts b/src/reducers/index.ts index 0c567df5b..a30369561 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -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` diff --git a/src/reducers/verification.ts b/src/reducers/verification.ts deleted file mode 100644 index 94abdeeb1..000000000 --- a/src/reducers/verification.ts +++ /dev/null @@ -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(), -}); - -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; - } -} diff --git a/src/utils/features.ts b/src/utils/features.ts index 9fb04b394..dc1a049c1 100644 --- a/src/utils/features.ts +++ b/src/utils/features.ts @@ -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 diff --git a/yarn.lock b/yarn.lock index 22e364c14..77639f1f5 100644 --- a/yarn.lock +++ b/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"