diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 89c441e981..f8f651305d 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -1,4 +1,10 @@
-import {Insets} from 'react-native'
+import {Insets, Platform} from 'react-native'
+
+export const LOCAL_DEV_SERVICE =
+ Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583'
+export const STAGING_SERVICE = 'https://staging.bsky.dev'
+export const PROD_SERVICE = 'https://bsky.social'
+export const DEFAULT_SERVICE = PROD_SERVICE
const HELP_DESK_LANG = 'en-us'
export const HELP_DESK_URL = `https://blueskyweb.zendesk.com/hc/${HELP_DESK_LANG}`
diff --git a/src/state/models/ui/create-account.ts b/src/state/models/ui/create-account.ts
deleted file mode 100644
index 60f4fc1844..0000000000
--- a/src/state/models/ui/create-account.ts
+++ /dev/null
@@ -1,223 +0,0 @@
-import {makeAutoObservable} from 'mobx'
-import {RootStoreModel} from '../root-store'
-import {ServiceDescription} from '../session'
-import {DEFAULT_SERVICE} from 'state/index'
-import {ComAtprotoServerCreateAccount} from '@atproto/api'
-import * as EmailValidator from 'email-validator'
-import {createFullHandle} from 'lib/strings/handles'
-import {cleanError} from 'lib/strings/errors'
-import {getAge} from 'lib/strings/time'
-import {track} from 'lib/analytics/analytics'
-import {logger} from '#/logger'
-import {DispatchContext as OnboardingDispatchContext} from '#/state/shell/onboarding'
-import {ApiContext as SessionApiContext} from '#/state/session'
-
-const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago
-
-export class CreateAccountModel {
- step: number = 1
- isProcessing = false
- isFetchingServiceDescription = false
- didServiceDescriptionFetchFail = false
- error = ''
-
- serviceUrl = DEFAULT_SERVICE
- serviceDescription: ServiceDescription | undefined = undefined
- userDomain = ''
- inviteCode = ''
- email = ''
- password = ''
- handle = ''
- birthDate = DEFAULT_DATE
-
- constructor(public rootStore: RootStoreModel) {
- makeAutoObservable(this, {}, {autoBind: true})
- }
-
- get isAge13() {
- return getAge(this.birthDate) >= 13
- }
-
- get isAge18() {
- return getAge(this.birthDate) >= 18
- }
-
- // form state controls
- // =
-
- next() {
- this.error = ''
- if (this.step === 2) {
- if (!this.isAge13) {
- this.error =
- 'Unfortunately, you do not meet the requirements to create an account.'
- return
- }
- }
- this.step++
- }
-
- back() {
- this.error = ''
- this.step--
- }
-
- setStep(v: number) {
- this.step = v
- }
-
- async fetchServiceDescription() {
- this.setError('')
- this.setIsFetchingServiceDescription(true)
- this.setDidServiceDescriptionFetchFail(false)
- this.setServiceDescription(undefined)
- if (!this.serviceUrl) {
- return
- }
- try {
- const desc = await this.rootStore.session.describeService(this.serviceUrl)
- this.setServiceDescription(desc)
- this.setUserDomain(desc.availableUserDomains[0])
- } catch (err: any) {
- logger.warn(
- `Failed to fetch service description for ${this.serviceUrl}`,
- {error: err},
- )
- this.setError(
- 'Unable to contact your service. Please check your Internet connection.',
- )
- this.setDidServiceDescriptionFetchFail(true)
- } finally {
- this.setIsFetchingServiceDescription(false)
- }
- }
-
- async submit({
- createAccount,
- onboardingDispatch,
- }: {
- createAccount: SessionApiContext['createAccount']
- onboardingDispatch: OnboardingDispatchContext
- }) {
- if (!this.email) {
- this.setStep(2)
- return this.setError('Please enter your email.')
- }
- if (!EmailValidator.validate(this.email)) {
- this.setStep(2)
- return this.setError('Your email appears to be invalid.')
- }
- if (!this.password) {
- this.setStep(2)
- return this.setError('Please choose your password.')
- }
- if (!this.handle) {
- this.setStep(3)
- return this.setError('Please choose your handle.')
- }
- this.setError('')
- this.setIsProcessing(true)
-
- try {
- onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view
- await createAccount({
- service: this.serviceUrl,
- email: this.email,
- handle: createFullHandle(this.handle, this.userDomain),
- password: this.password,
- inviteCode: this.inviteCode.trim(),
- })
- track('Create Account')
- } catch (e: any) {
- onboardingDispatch({type: 'skip'}) // undo starting the onboard
- let errMsg = e.toString()
- if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) {
- errMsg =
- 'Invite code not accepted. Check that you input it correctly and try again.'
- }
- logger.error('Failed to create account', {error: e})
- this.setIsProcessing(false)
- this.setError(cleanError(errMsg))
- throw e
- }
- }
-
- // form state accessors
- // =
-
- get canBack() {
- return this.step > 1
- }
-
- get canNext() {
- if (this.step === 1) {
- return !!this.serviceDescription
- } else if (this.step === 2) {
- return (
- (!this.isInviteCodeRequired || this.inviteCode) &&
- !!this.email &&
- !!this.password
- )
- }
- return !!this.handle
- }
-
- get isServiceDescribed() {
- return !!this.serviceDescription
- }
-
- get isInviteCodeRequired() {
- return this.serviceDescription?.inviteCodeRequired
- }
-
- // setters
- // =
-
- setIsProcessing(v: boolean) {
- this.isProcessing = v
- }
-
- setIsFetchingServiceDescription(v: boolean) {
- this.isFetchingServiceDescription = v
- }
-
- setDidServiceDescriptionFetchFail(v: boolean) {
- this.didServiceDescriptionFetchFail = v
- }
-
- setError(v: string) {
- this.error = v
- }
-
- setServiceUrl(v: string) {
- this.serviceUrl = v
- }
-
- setServiceDescription(v: ServiceDescription | undefined) {
- this.serviceDescription = v
- }
-
- setUserDomain(v: string) {
- this.userDomain = v
- }
-
- setInviteCode(v: string) {
- this.inviteCode = v
- }
-
- setEmail(v: string) {
- this.email = v
- }
-
- setPassword(v: string) {
- this.password = v
- }
-
- setHandle(v: string) {
- this.handle = v
- }
-
- setBirthDate(v: Date) {
- this.birthDate = v
- }
-}
diff --git a/src/state/queries/service.ts b/src/state/queries/service.ts
index df12d6cbc8..5f7e10778b 100644
--- a/src/state/queries/service.ts
+++ b/src/state/queries/service.ts
@@ -1,16 +1,26 @@
+import {BskyAgent} from '@atproto/api'
import {useQuery} from '@tanstack/react-query'
-import {useSession} from '#/state/session'
-
export const RQKEY = (serviceUrl: string) => ['service', serviceUrl]
-export function useServiceQuery() {
- const {agent} = useSession()
+export function useServiceQuery(serviceUrl: string) {
return useQuery({
- queryKey: RQKEY(agent.service.toString()),
+ queryKey: RQKEY(serviceUrl),
queryFn: async () => {
+ const agent = new BskyAgent({service: serviceUrl})
const res = await agent.com.atproto.server.describeServer()
return res.data
},
+ enabled: isValidUrl(serviceUrl),
})
}
+
+function isValidUrl(url: string) {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const urlp = new URL(url)
+ return true
+ } catch {
+ return false
+ }
+}
diff --git a/src/view/com/auth/create/CreateAccount.tsx b/src/view/com/auth/create/CreateAccount.tsx
index 0f3ff41af8..ab6d34584a 100644
--- a/src/view/com/auth/create/CreateAccount.tsx
+++ b/src/view/com/auth/create/CreateAccount.tsx
@@ -7,18 +7,17 @@ import {
TouchableOpacity,
View,
} from 'react-native'
-import {observer} from 'mobx-react-lite'
import {useAnalytics} from 'lib/analytics/analytics'
import {Text} from '../../util/text/Text'
import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout'
import {s} from 'lib/styles'
-import {useStores} from 'state/index'
-import {CreateAccountModel} from 'state/models/ui/create-account'
import {usePalette} from 'lib/hooks/usePalette'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useOnboardingDispatch} from '#/state/shell'
import {useSessionApi} from '#/state/session'
+import {useCreateAccount, submit} from './state'
+import {useServiceQuery} from '#/state/queries/service'
import {
usePreferencesSetBirthDateMutation,
useSetSaveFeedsMutation,
@@ -30,16 +29,11 @@ import {Step1} from './Step1'
import {Step2} from './Step2'
import {Step3} from './Step3'
-export const CreateAccount = observer(function CreateAccountImpl({
- onPressBack,
-}: {
- onPressBack: () => void
-}) {
+export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
const {track, screen} = useAnalytics()
const pal = usePalette('default')
- const store = useStores()
- const model = React.useMemo(() => new CreateAccountModel(store), [store])
const {_} = useLingui()
+ const [uiState, uiDispatch] = useCreateAccount()
const onboardingDispatch = useOnboardingDispatch()
const {createAccount} = useSessionApi()
const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation()
@@ -49,39 +43,59 @@ export const CreateAccount = observer(function CreateAccountImpl({
screen('CreateAccount')
}, [screen])
- React.useEffect(() => {
- model.fetchServiceDescription()
- }, [model])
+ // fetch service info
+ // =
- const onPressRetryConnect = React.useCallback(
- () => model.fetchServiceDescription(),
- [model],
- )
+ const {
+ data: serviceInfo,
+ isFetching: serviceInfoIsFetching,
+ error: serviceInfoError,
+ refetch: refetchServiceInfo,
+ } = useServiceQuery(uiState.serviceUrl)
+
+ React.useEffect(() => {
+ if (serviceInfo) {
+ uiDispatch({type: 'set-service-description', value: serviceInfo})
+ uiDispatch({type: 'set-error', value: ''})
+ } else if (serviceInfoError) {
+ uiDispatch({
+ type: 'set-error',
+ value: _(
+ msg`Unable to contact your service. Please check your Internet connection.`,
+ ),
+ })
+ }
+ }, [_, uiDispatch, serviceInfo, serviceInfoError])
+
+ // event handlers
+ // =
const onPressBackInner = React.useCallback(() => {
- if (model.canBack) {
- model.back()
+ if (uiState.canBack) {
+ uiDispatch({type: 'back'})
} else {
onPressBack()
}
- }, [model, onPressBack])
+ }, [uiState, uiDispatch, onPressBack])
const onPressNext = React.useCallback(async () => {
- if (!model.canNext) {
+ if (!uiState.canNext) {
return
}
- if (model.step < 3) {
- model.next()
+ if (uiState.step < 3) {
+ uiDispatch({type: 'next'})
} else {
try {
- await model.submit({
+ await submit({
onboardingDispatch,
createAccount,
+ uiState,
+ uiDispatch,
+ _,
})
-
- setBirthDate({birthDate: model.birthDate})
-
- if (IS_PROD(model.serviceUrl)) {
+ track('Create Account')
+ setBirthDate({birthDate: uiState.birthDate})
+ if (IS_PROD(uiState.serviceUrl)) {
setSavedFeeds(DEFAULT_PROD_FEEDS)
}
} catch {
@@ -91,25 +105,36 @@ export const CreateAccount = observer(function CreateAccountImpl({
}
}
}, [
- model,
+ uiState,
+ uiDispatch,
track,
onboardingDispatch,
createAccount,
setBirthDate,
setSavedFeeds,
+ _,
])
+ // rendering
+ // =
+
return (
- {model.step === 1 && }
- {model.step === 2 && }
- {model.step === 3 && }
+ {uiState.step === 1 && (
+
+ )}
+ {uiState.step === 2 && (
+
+ )}
+ {uiState.step === 3 && (
+
+ )}
- {model.canNext ? (
+ {uiState.canNext ? (
- {model.isProcessing ? (
+ {uiState.isProcessing ? (
) : (
@@ -134,19 +159,19 @@ export const CreateAccount = observer(function CreateAccountImpl({
)}
- ) : model.didServiceDescriptionFetchFail ? (
+ ) : serviceInfoError ? (
refetchServiceInfo()}
accessibilityRole="button"
accessibilityLabel={_(msg`Retry`)}
- accessibilityHint="Retries account creation"
+ accessibilityHint=""
accessibilityLiveRegion="polite">
Retry
- ) : model.isFetchingServiceDescription ? (
+ ) : serviceInfoIsFetching ? (
<>
@@ -160,7 +185,7 @@ export const CreateAccount = observer(function CreateAccountImpl({
)
-})
+}
const styles = StyleSheet.create({
stepContainer: {
diff --git a/src/view/com/auth/create/Policies.tsx b/src/view/com/auth/create/Policies.tsx
index 8eb669bcf6..7d10f32fc1 100644
--- a/src/view/com/auth/create/Policies.tsx
+++ b/src/view/com/auth/create/Policies.tsx
@@ -93,7 +93,7 @@ function validWebLink(url?: string): string | undefined {
const styles = StyleSheet.create({
policies: {
- flexDirection: 'row',
+ flexDirection: 'column',
gap: 8,
},
errorIcon: {
diff --git a/src/view/com/auth/create/Step1.tsx b/src/view/com/auth/create/Step1.tsx
index 7e3ea062dc..ab47b411f1 100644
--- a/src/view/com/auth/create/Step1.tsx
+++ b/src/view/com/auth/create/Step1.tsx
@@ -1,10 +1,8 @@
import React from 'react'
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
-import debounce from 'lodash.debounce'
import {Text} from 'view/com/util/text/Text'
import {StepHeader} from './StepHeader'
-import {CreateAccountModel} from 'state/models/ui/create-account'
+import {CreateAccountState, CreateAccountDispatch} from './state'
import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles'
@@ -22,10 +20,12 @@ import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags'
* @field Bluesky (default)
* @field Other (staging, local dev, your own PDS, etc.)
*/
-export const Step1 = observer(function Step1Impl({
- model,
+export function Step1({
+ uiState,
+ uiDispatch,
}: {
- model: CreateAccountModel
+ uiState: CreateAccountState
+ uiDispatch: CreateAccountDispatch
}) {
const pal = usePalette('default')
const [isDefaultSelected, setIsDefaultSelected] = React.useState(true)
@@ -33,35 +33,19 @@ export const Step1 = observer(function Step1Impl({
const onPressDefault = React.useCallback(() => {
setIsDefaultSelected(true)
- model.setServiceUrl(PROD_SERVICE)
- model.fetchServiceDescription()
- }, [setIsDefaultSelected, model])
+ uiDispatch({type: 'set-service-url', value: PROD_SERVICE})
+ }, [setIsDefaultSelected, uiDispatch])
const onPressOther = React.useCallback(() => {
setIsDefaultSelected(false)
- model.setServiceUrl('https://')
- model.setServiceDescription(undefined)
- }, [setIsDefaultSelected, model])
-
- const fetchServiceDescription = React.useMemo(
- () => debounce(() => model.fetchServiceDescription(), 1e3), // debouce for 1 second (1e3 = 1000ms)
- [model],
- )
+ uiDispatch({type: 'set-service-url', value: 'https://'})
+ }, [setIsDefaultSelected, uiDispatch])
const onChangeServiceUrl = React.useCallback(
(v: string) => {
- model.setServiceUrl(v)
- fetchServiceDescription()
+ uiDispatch({type: 'set-service-url', value: v})
},
- [model, fetchServiceDescription],
- )
-
- const onDebugChangeServiceUrl = React.useCallback(
- (v: string) => {
- model.setServiceUrl(v)
- model.fetchServiceDescription()
- },
- [model],
+ [uiDispatch],
)
return (
@@ -90,7 +74,7 @@ export const Step1 = observer(function Step1Impl({
testID="customServerInput"
icon="globe"
placeholder={_(msg`Hosting provider address`)}
- value={model.serviceUrl}
+ value={uiState.serviceUrl}
editable
onChange={onChangeServiceUrl}
accessibilityHint="Input hosting provider address"
@@ -104,26 +88,26 @@ export const Step1 = observer(function Step1Impl({
type="default"
style={s.mr5}
label={_(msg`Staging`)}
- onPress={() => onDebugChangeServiceUrl(STAGING_SERVICE)}
+ onPress={() => onChangeServiceUrl(STAGING_SERVICE)}
/>