diff options
Diffstat (limited to 'src/view/com/auth')
-rw-r--r-- | src/view/com/auth/create/CaptchaWebView.tsx | 86 | ||||
-rw-r--r-- | src/view/com/auth/create/CaptchaWebView.web.tsx | 61 | ||||
-rw-r--r-- | src/view/com/auth/create/Policies.tsx | 97 | ||||
-rw-r--r-- | src/view/com/auth/create/state.ts | 298 |
4 files changed, 0 insertions, 542 deletions
diff --git a/src/view/com/auth/create/CaptchaWebView.tsx b/src/view/com/auth/create/CaptchaWebView.tsx deleted file mode 100644 index 06b605e4d..000000000 --- a/src/view/com/auth/create/CaptchaWebView.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react' -import {WebView, WebViewNavigation} from 'react-native-webview' -import {ShouldStartLoadRequest} from 'react-native-webview/lib/WebViewTypes' -import {StyleSheet} from 'react-native' -import {SignupState} from '#/screens/Signup/state' - -const ALLOWED_HOSTS = [ - 'bsky.social', - 'bsky.app', - 'staging.bsky.app', - 'staging.bsky.dev', - 'js.hcaptcha.com', - 'newassets.hcaptcha.com', - 'api2.hcaptcha.com', -] - -export function CaptchaWebView({ - url, - stateParam, - state, - onSuccess, - onError, -}: { - url: string - stateParam: string - state?: SignupState - onSuccess: (code: string) => void - onError: () => void -}) { - const redirectHost = React.useMemo(() => { - if (!state?.serviceUrl) return 'bsky.app' - - return state?.serviceUrl && - new URL(state?.serviceUrl).host === 'staging.bsky.dev' - ? 'staging.bsky.app' - : 'bsky.app' - }, [state?.serviceUrl]) - - const wasSuccessful = React.useRef(false) - - const onShouldStartLoadWithRequest = React.useCallback( - (event: ShouldStartLoadRequest) => { - const urlp = new URL(event.url) - return ALLOWED_HOSTS.includes(urlp.host) - }, - [], - ) - - const onNavigationStateChange = React.useCallback( - (e: WebViewNavigation) => { - if (wasSuccessful.current) return - - const urlp = new URL(e.url) - if (urlp.host !== redirectHost) return - - const code = urlp.searchParams.get('code') - if (urlp.searchParams.get('state') !== stateParam || !code) { - onError() - return - } - - wasSuccessful.current = true - onSuccess(code) - }, - [redirectHost, stateParam, onSuccess, onError], - ) - - return ( - <WebView - source={{uri: url}} - javaScriptEnabled - style={styles.webview} - onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} - onNavigationStateChange={onNavigationStateChange} - scrollEnabled={false} - /> - ) -} - -const styles = StyleSheet.create({ - webview: { - flex: 1, - backgroundColor: 'transparent', - borderRadius: 10, - }, -}) diff --git a/src/view/com/auth/create/CaptchaWebView.web.tsx b/src/view/com/auth/create/CaptchaWebView.web.tsx deleted file mode 100644 index 7791a58dd..000000000 --- a/src/view/com/auth/create/CaptchaWebView.web.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react' -import {StyleSheet} from 'react-native' - -// @ts-ignore web only, we will always redirect to the app on web (CORS) -const REDIRECT_HOST = new URL(window.location.href).host - -export function CaptchaWebView({ - url, - stateParam, - onSuccess, - onError, -}: { - url: string - stateParam: string - onSuccess: (code: string) => void - onError: () => void -}) { - const onLoad = React.useCallback(() => { - // @ts-ignore web - const frame: HTMLIFrameElement = document.getElementById( - 'captcha-iframe', - ) as HTMLIFrameElement - - try { - // @ts-ignore web - const href = frame?.contentWindow?.location.href - if (!href) return - const urlp = new URL(href) - - // This shouldn't happen with CORS protections, but for good measure - if (urlp.host !== REDIRECT_HOST) return - - const code = urlp.searchParams.get('code') - if (urlp.searchParams.get('state') !== stateParam || !code) { - onError() - return - } - onSuccess(code) - } catch (e) { - // We don't need to handle this - } - }, [stateParam, onSuccess, onError]) - - return ( - <iframe - src={url} - style={styles.iframe} - id="captcha-iframe" - onLoad={onLoad} - /> - ) -} - -const styles = StyleSheet.create({ - iframe: { - flex: 1, - borderWidth: 0, - borderRadius: 10, - backgroundColor: 'transparent', - }, -}) diff --git a/src/view/com/auth/create/Policies.tsx b/src/view/com/auth/create/Policies.tsx deleted file mode 100644 index 8a656203f..000000000 --- a/src/view/com/auth/create/Policies.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react' -import {View} from 'react-native' -import {ComAtprotoServerDescribeServer} from '@atproto/api' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {atoms as a, useTheme} from '#/alf' -import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' -import {InlineLink} from '#/components/Link' -import {Text} from '#/components/Typography' - -export const Policies = ({ - serviceDescription, - needsGuardian, - under13, -}: { - serviceDescription: ComAtprotoServerDescribeServer.OutputSchema - needsGuardian: boolean - under13: boolean -}) => { - const t = useTheme() - const {_} = useLingui() - - if (!serviceDescription) { - return <View /> - } - - const tos = validWebLink(serviceDescription.links?.termsOfService) - const pp = validWebLink(serviceDescription.links?.privacyPolicy) - - if (!tos && !pp) { - return ( - <View style={[a.flex_row, a.align_center, a.gap_xs]}> - <CircleInfo size="md" fill={t.atoms.text_contrast_low.color} /> - - <Text style={[t.atoms.text_contrast_medium]}> - <Trans> - This service has not provided terms of service or a privacy policy. - </Trans> - </Text> - </View> - ) - } - - const els = [] - if (tos) { - els.push( - <InlineLink key="tos" to={tos}> - {_(msg`Terms of Service`)} - </InlineLink>, - ) - } - if (pp) { - els.push( - <InlineLink key="pp" to={pp}> - {_(msg`Privacy Policy`)} - </InlineLink>, - ) - } - if (els.length === 2) { - els.splice( - 1, - 0, - <Text key="and" style={[t.atoms.text_contrast_medium]}> - {' '} - and{' '} - </Text>, - ) - } - - return ( - <View style={[a.gap_sm]}> - <Text style={[a.leading_snug, t.atoms.text_contrast_medium]}> - <Trans>By creating an account you agree to the {els}.</Trans> - </Text> - - {under13 ? ( - <Text style={[a.font_bold, a.leading_snug, t.atoms.text_contrast_high]}> - You must be 13 years of age or older to sign up. - </Text> - ) : needsGuardian ? ( - <Text style={[a.font_bold, a.leading_snug, t.atoms.text_contrast_high]}> - <Trans> - If you are not yet an adult according to the laws of your country, - your parent or legal guardian must read these Terms on your behalf. - </Trans> - </Text> - ) : undefined} - </View> - ) -} - -function validWebLink(url?: string): string | undefined { - return url && (url.startsWith('http://') || url.startsWith('https://')) - ? url - : undefined -} diff --git a/src/view/com/auth/create/state.ts b/src/view/com/auth/create/state.ts deleted file mode 100644 index 840084dcb..000000000 --- a/src/view/com/auth/create/state.ts +++ /dev/null @@ -1,298 +0,0 @@ -import {useCallback, useReducer} from 'react' -import { - ComAtprotoServerDescribeServer, - ComAtprotoServerCreateAccount, -} from '@atproto/api' -import {I18nContext, useLingui} from '@lingui/react' -import {msg} from '@lingui/macro' -import * as EmailValidator from 'email-validator' -import {getAge} from 'lib/strings/time' -import {logger} from '#/logger' -import {createFullHandle, validateHandle} from '#/lib/strings/handles' -import {cleanError} from '#/lib/strings/errors' -import {useOnboardingDispatch} from '#/state/shell/onboarding' -import {useSessionApi} from '#/state/session' -import {DEFAULT_SERVICE, IS_TEST_USER} from '#/lib/constants' -import { - DEFAULT_PROD_FEEDS, - usePreferencesSetBirthDateMutation, - useSetSaveFeedsMutation, -} from 'state/queries/preferences' - -export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema -const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago - -export type CreateAccountAction = - | {type: 'set-step'; value: number} - | {type: 'set-error'; value: string | undefined} - | {type: 'set-processing'; value: boolean} - | {type: 'set-service-url'; value: string} - | {type: 'set-service-description'; value: ServiceDescription | undefined} - | {type: 'set-user-domain'; value: string} - | {type: 'set-invite-code'; value: string} - | {type: 'set-email'; value: string} - | {type: 'set-password'; value: string} - | {type: 'set-handle'; value: string} - | {type: 'set-birth-date'; value: Date} - | {type: 'next'} - | {type: 'back'} - -export interface CreateAccountState { - // state - step: number - error: string | undefined - isProcessing: boolean - serviceUrl: string - serviceDescription: ServiceDescription | undefined - userDomain: string - inviteCode: string - email: string - password: string - handle: string - birthDate: Date - - // computed - canBack: boolean - canNext: boolean - isInviteCodeRequired: boolean - isCaptchaRequired: boolean -} - -export type CreateAccountDispatch = (action: CreateAccountAction) => void - -export function useCreateAccount() { - const {_} = useLingui() - - return useReducer(createReducer({_}), { - step: 1, - error: undefined, - isProcessing: false, - serviceUrl: DEFAULT_SERVICE, - serviceDescription: undefined, - userDomain: '', - inviteCode: '', - email: '', - password: '', - handle: '', - birthDate: DEFAULT_DATE, - - canBack: false, - canNext: false, - isInviteCodeRequired: false, - isCaptchaRequired: false, - }) -} - -export function useSubmitCreateAccount( - uiState: CreateAccountState, - uiDispatch: CreateAccountDispatch, -) { - const {_} = useLingui() - const {createAccount} = useSessionApi() - const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation() - const {mutate: setSavedFeeds} = useSetSaveFeedsMutation() - const onboardingDispatch = useOnboardingDispatch() - - return useCallback( - async (verificationCode?: string) => { - if (!uiState.email) { - uiDispatch({type: 'set-step', value: 1}) - console.log('no email?') - return uiDispatch({ - type: 'set-error', - value: _(msg`Please enter your email.`), - }) - } - if (!EmailValidator.validate(uiState.email)) { - uiDispatch({type: 'set-step', value: 1}) - return uiDispatch({ - type: 'set-error', - value: _(msg`Your email appears to be invalid.`), - }) - } - if (!uiState.password) { - uiDispatch({type: 'set-step', value: 1}) - return uiDispatch({ - type: 'set-error', - value: _(msg`Please choose your password.`), - }) - } - if (!uiState.handle) { - uiDispatch({type: 'set-step', value: 2}) - return uiDispatch({ - type: 'set-error', - value: _(msg`Please choose your handle.`), - }) - } - if (uiState.isCaptchaRequired && !verificationCode) { - uiDispatch({type: 'set-step', value: 3}) - return uiDispatch({ - type: 'set-error', - value: _(msg`Please complete the verification captcha.`), - }) - } - uiDispatch({type: 'set-error', value: ''}) - uiDispatch({type: 'set-processing', value: true}) - - try { - onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view - await createAccount({ - service: uiState.serviceUrl, - email: uiState.email, - handle: createFullHandle(uiState.handle, uiState.userDomain), - password: uiState.password, - inviteCode: uiState.inviteCode.trim(), - verificationCode: uiState.isCaptchaRequired - ? verificationCode - : undefined, - }) - setBirthDate({birthDate: uiState.birthDate}) - if (!IS_TEST_USER(uiState.handle)) { - setSavedFeeds(DEFAULT_PROD_FEEDS) - } - } catch (e: any) { - onboardingDispatch({type: 'skip'}) // undo starting the onboard - let errMsg = e.toString() - if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) { - errMsg = _( - msg`Invite code not accepted. Check that you input it correctly and try again.`, - ) - uiDispatch({type: 'set-step', value: 1}) - } - - if ([400, 429].includes(e.status)) { - logger.warn('Failed to create account', {message: e}) - } else { - logger.error(`Failed to create account (${e.status} status)`, { - message: e, - }) - } - - const error = cleanError(errMsg) - const isHandleError = error.toLowerCase().includes('handle') - - uiDispatch({type: 'set-processing', value: false}) - uiDispatch({type: 'set-error', value: cleanError(errMsg)}) - uiDispatch({type: 'set-step', value: isHandleError ? 2 : 1}) - } - }, - [ - uiState.email, - uiState.password, - uiState.handle, - uiState.isCaptchaRequired, - uiState.serviceUrl, - uiState.userDomain, - uiState.inviteCode, - uiState.birthDate, - uiDispatch, - _, - onboardingDispatch, - createAccount, - setBirthDate, - setSavedFeeds, - ], - ) -} - -export function is13(state: CreateAccountState) { - return getAge(state.birthDate) >= 13 -} - -export function is18(state: CreateAccountState) { - return getAge(state.birthDate) >= 18 -} - -function createReducer({_}: {_: I18nContext['_']}) { - return function reducer( - state: CreateAccountState, - action: CreateAccountAction, - ): CreateAccountState { - switch (action.type) { - case 'set-step': { - return compute({...state, step: action.value}) - } - case 'set-error': { - return compute({...state, error: action.value}) - } - case 'set-processing': { - return compute({...state, isProcessing: action.value}) - } - case 'set-service-url': { - return compute({ - ...state, - serviceUrl: action.value, - serviceDescription: - state.serviceUrl !== action.value - ? undefined - : state.serviceDescription, - }) - } - case 'set-service-description': { - return compute({ - ...state, - serviceDescription: action.value, - userDomain: action.value?.availableUserDomains[0] || '', - }) - } - case 'set-user-domain': { - return compute({...state, userDomain: action.value}) - } - case 'set-invite-code': { - return compute({...state, inviteCode: action.value}) - } - case 'set-email': { - return compute({...state, email: action.value}) - } - case 'set-password': { - return compute({...state, password: action.value}) - } - case 'set-handle': { - return compute({...state, handle: action.value}) - } - case 'set-birth-date': { - return compute({...state, birthDate: action.value}) - } - case 'next': { - if (state.step === 1) { - if (!is13(state)) { - return compute({ - ...state, - error: _( - msg`Unfortunately, you do not meet the requirements to create an account.`, - ), - }) - } - } - return compute({...state, error: '', step: state.step + 1}) - } - case 'back': { - return compute({...state, error: '', step: state.step - 1}) - } - } - } -} - -function compute(state: CreateAccountState): CreateAccountState { - let canNext = true - if (state.step === 1) { - canNext = - !!state.serviceDescription && - (!state.isInviteCodeRequired || !!state.inviteCode) && - !!state.email && - !!state.password - } else if (state.step === 2) { - canNext = - !!state.handle && validateHandle(state.handle, state.userDomain).overall - } else if (state.step === 3) { - // Step 3 will automatically redirect as soon as the captcha completes - canNext = false - } - return { - ...state, - canBack: state.step > 1, - canNext, - isInviteCodeRequired: !!state.serviceDescription?.inviteCodeRequired, - isCaptchaRequired: !!state.serviceDescription?.phoneVerificationRequired, - } -} |