diff options
author | Eric Bailey <git@esb.lol> | 2023-11-09 20:35:17 -0600 |
---|---|---|
committer | Eric Bailey <git@esb.lol> | 2023-11-09 20:35:17 -0600 |
commit | ab878ba9a6afaa57805aeab988b01c5b47bc9286 (patch) | |
tree | 266f68581a5d237b0d22b4b94fb92e0e45c0993b /src | |
parent | 487d871cfd89948f4db9944c4bb414d268a56537 (diff) | |
download | voidsky-ab878ba9a6afaa57805aeab988b01c5b47bc9286.tar.zst |
Web login/signup and shell
Diffstat (limited to 'src')
-rw-r--r-- | src/data/useGetProfile.ts | 33 | ||||
-rw-r--r-- | src/lib/hooks/useAccountSwitcher.ts | 57 | ||||
-rw-r--r-- | src/state/models/ui/create-account.ts | 11 | ||||
-rw-r--r-- | src/state/persisted/schema.ts | 2 | ||||
-rw-r--r-- | src/state/session/index.tsx | 178 | ||||
-rw-r--r-- | src/view/com/auth/LoggedOut.tsx | 7 | ||||
-rw-r--r-- | src/view/com/auth/create/CreateAccount.tsx | 10 | ||||
-rw-r--r-- | src/view/com/auth/login/ChooseAccountForm.tsx | 113 | ||||
-rw-r--r-- | src/view/com/auth/login/ForgotPasswordForm.tsx | 2 | ||||
-rw-r--r-- | src/view/com/auth/login/Login.tsx | 8 | ||||
-rw-r--r-- | src/view/com/auth/login/LoginForm.tsx | 8 | ||||
-rw-r--r-- | src/view/com/auth/login/SetNewPasswordForm.tsx | 2 | ||||
-rw-r--r-- | src/view/com/auth/withAuthRequired.tsx | 8 | ||||
-rw-r--r-- | src/view/com/modals/ChangeEmail.tsx | 25 | ||||
-rw-r--r-- | src/view/com/modals/SwitchAccount.tsx | 147 | ||||
-rw-r--r-- | src/view/com/modals/VerifyEmail.tsx | 20 | ||||
-rw-r--r-- | src/view/screens/Settings.tsx | 236 | ||||
-rw-r--r-- | src/view/shell/desktop/LeftNav.tsx | 63 | ||||
-rw-r--r-- | src/view/shell/desktop/RightNav.tsx | 10 | ||||
-rw-r--r-- | src/view/shell/index.tsx | 7 | ||||
-rw-r--r-- | src/view/shell/index.web.tsx | 6 |
21 files changed, 580 insertions, 373 deletions
diff --git a/src/data/useGetProfile.ts b/src/data/useGetProfile.ts new file mode 100644 index 000000000..58f24a4e8 --- /dev/null +++ b/src/data/useGetProfile.ts @@ -0,0 +1,33 @@ +import React from 'react' +import {useQuery} from '@tanstack/react-query' +import {BskyAgent} from '@atproto/api' + +import {useSession} from '#/state/session' + +export function useGetProfile({did}: {did: string}) { + const {accounts} = useSession() + const account = React.useMemo( + () => accounts.find(a => a.did === did), + [did, accounts], + ) + + return useQuery({ + enabled: !!account, + queryKey: ['getProfile', account], + queryFn: async () => { + if (!account) { + throw new Error(`useGetProfile: local account not found for ${did}`) + } + + const agent = new BskyAgent({ + // needs to be public data, so remap PDS URLs to App View for now + service: account.service.includes('bsky.social') + ? 'https://api.bsky.app' + : account.service, + }) + + const res = await agent.getProfile({actor: did}) + return res.data + }, + }) +} diff --git a/src/lib/hooks/useAccountSwitcher.ts b/src/lib/hooks/useAccountSwitcher.ts index b165fddb5..838536735 100644 --- a/src/lib/hooks/useAccountSwitcher.ts +++ b/src/lib/hooks/useAccountSwitcher.ts @@ -1,46 +1,43 @@ -import {useCallback, useState} from 'react' -import {useStores} from 'state/index' -import {useAnalytics} from 'lib/analytics/analytics' -import {StackActions, useNavigation} from '@react-navigation/native' -import {NavigationProp} from 'lib/routes/types' -import {AccountData} from 'state/models/session' -import {reset as resetNavigation} from '../../Navigation' -import * as Toast from 'view/com/util/Toast' +import {useCallback} from 'react' + +import {useAnalytics} from '#/lib/analytics/analytics' +import {useStores} from '#/state/index' import {useSetDrawerOpen} from '#/state/shell/drawer-open' import {useModalControls} from '#/state/modals' +import {useSessionApi, SessionAccount} from '#/state/session' +import * as Toast from '#/view/com/util/Toast' -export function useAccountSwitcher(): [ - boolean, - (v: boolean) => void, - (acct: AccountData) => Promise<void>, -] { +export function useAccountSwitcher() { const {track} = useAnalytics() const store = useStores() const setDrawerOpen = useSetDrawerOpen() const {closeModal} = useModalControls() - const [isSwitching, setIsSwitching] = useState(false) - const navigation = useNavigation<NavigationProp>() + const {selectAccount, clearCurrentAccount} = useSessionApi() const onPressSwitchAccount = useCallback( - async (acct: AccountData) => { + async (acct: SessionAccount) => { track('Settings:SwitchAccountButtonClicked') - setIsSwitching(true) - const success = await store.session.resumeSession(acct) - setDrawerOpen(false) - closeModal() - store.shell.closeAllActiveElements() - if (success) { - resetNavigation() - Toast.show(`Signed in as ${acct.displayName || acct.handle}`) - } else { + + try { + await selectAccount(acct) + setDrawerOpen(false) + closeModal() + store.shell.closeAllActiveElements() + Toast.show(`Signed in as ${acct.handle}`) + } catch (e) { Toast.show('Sorry! We need you to enter your password.') - navigation.navigate('HomeTab') - navigation.dispatch(StackActions.popToTop()) - store.session.clear() + clearCurrentAccount() // back user out to login } }, - [track, setIsSwitching, navigation, store, setDrawerOpen, closeModal], + [ + track, + store, + setDrawerOpen, + closeModal, + clearCurrentAccount, + selectAccount, + ], ) - return [isSwitching, setIsSwitching, onPressSwitchAccount] + return {onPressSwitchAccount} } diff --git a/src/state/models/ui/create-account.ts b/src/state/models/ui/create-account.ts index 39c881db6..6d76784c1 100644 --- a/src/state/models/ui/create-account.ts +++ b/src/state/models/ui/create-account.ts @@ -10,6 +10,7 @@ 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 @@ -91,7 +92,13 @@ export class CreateAccountModel { } } - async submit(onboardingDispatch: OnboardingDispatchContext) { + async submit({ + createAccount, + onboardingDispatch, + }: { + createAccount: SessionApiContext['createAccount'] + onboardingDispatch: OnboardingDispatchContext + }) { if (!this.email) { this.setStep(2) return this.setError('Please enter your email.') @@ -113,7 +120,7 @@ export class CreateAccountModel { try { onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view - await this.rootStore.session.createAccount({ + await createAccount({ service: this.serviceUrl, email: this.email, handle: createFullHandle(this.handle, this.userDomain), diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index a510262fb..93547aa5b 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -7,6 +7,8 @@ const accountSchema = z.object({ service: z.string(), did: z.string(), handle: z.string(), + email: z.string(), + emailConfirmed: z.boolean(), refreshJwt: z.string().optional(), // optional because it can expire accessJwt: z.string().optional(), // optional because it can expire // displayName: z.string().optional(), diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 8e1f9c1a1..668d9d8ca 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -8,8 +8,9 @@ import * as persisted from '#/state/persisted' export type SessionAccount = persisted.PersistedAccount export type StateContext = { - isInitialLoad: boolean agent: BskyAgent + isInitialLoad: boolean + isSwitchingAccounts: boolean accounts: persisted.PersistedAccount[] currentAccount: persisted.PersistedAccount | undefined hasSession: boolean @@ -33,9 +34,13 @@ export type ApiContext = { removeAccount: ( account: Partial<Pick<persisted.PersistedAccount, 'handle' | 'did'>>, ) => void + selectAccount: (account: persisted.PersistedAccount) => Promise<void> updateCurrentAccount: ( - account: Pick<persisted.PersistedAccount, 'handle'>, + account: Partial< + Pick<persisted.PersistedAccount, 'handle' | 'email' | 'emailConfirmed'> + >, ) => void + clearCurrentAccount: () => void } export const PUBLIC_BSKY_AGENT = new BskyAgent({ @@ -43,11 +48,12 @@ export const PUBLIC_BSKY_AGENT = new BskyAgent({ }) const StateContext = React.createContext<StateContext>({ + agent: PUBLIC_BSKY_AGENT, hasSession: false, isInitialLoad: true, + isSwitchingAccounts: false, accounts: [], currentAccount: undefined, - agent: PUBLIC_BSKY_AGENT, }) const ApiContext = React.createContext<ApiContext>({ @@ -57,7 +63,9 @@ const ApiContext = React.createContext<ApiContext>({ initSession: async () => {}, resumeSession: async () => {}, removeAccount: () => {}, + selectAccount: async () => {}, updateCurrentAccount: () => {}, + clearCurrentAccount: () => {}, }) function createPersistSessionHandler( @@ -73,15 +81,21 @@ function createPersistSessionHandler( service: account.service, did: session?.did || account.did, handle: session?.handle || account.handle, + email: session?.email || account.email, + emailConfirmed: session?.emailConfirmed || account.emailConfirmed, refreshJwt: session?.refreshJwt, // undefined when expired or creation fails accessJwt: session?.accessJwt, // undefined when expired or creation fails } - logger.debug(`session: BskyAgent.persistSession`, { - expired, - did: refreshedAccount.did, - handle: refreshedAccount.handle, - }) + logger.debug( + `session: BskyAgent.persistSession`, + { + expired, + did: refreshedAccount.did, + handle: refreshedAccount.handle, + }, + logger.DebugContext.session, + ) persistSessionCallback({ expired, @@ -92,11 +106,12 @@ function createPersistSessionHandler( export function Provider({children}: React.PropsWithChildren<{}>) { const [state, setState] = React.useState<StateContext>({ + agent: PUBLIC_BSKY_AGENT, hasSession: false, isInitialLoad: true, // try to resume the session first + isSwitchingAccounts: false, accounts: persisted.get('session').accounts, currentAccount: undefined, // assume logged out to start - agent: PUBLIC_BSKY_AGENT, }) const upsertAccount = React.useCallback( @@ -115,10 +130,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) { // TODO have not connected this yet const createAccount = React.useCallback<ApiContext['createAccount']>( async ({service, email, password, handle, inviteCode}: any) => { - logger.debug(`session: creating account`, { - service, - handle, - }) + logger.debug( + `session: creating account`, + { + service, + handle, + }, + logger.DebugContext.session, + ) const agent = new BskyAgent({service}) @@ -136,9 +155,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const account: persisted.PersistedAccount = { service, did: agent.session.did, + handle: agent.session.handle, + email: agent.session.email!, // TODO this is always defined? + emailConfirmed: false, refreshJwt: agent.session.refreshJwt, accessJwt: agent.session.accessJwt, - handle: agent.session.handle, } agent.setPersistSessionHandler( @@ -149,20 +170,28 @@ export function Provider({children}: React.PropsWithChildren<{}>) { upsertAccount(account) - logger.debug(`session: created account`, { - service, - handle, - }) + logger.debug( + `session: created account`, + { + service, + handle, + }, + logger.DebugContext.session, + ) }, [upsertAccount], ) const login = React.useCallback<ApiContext['login']>( async ({service, identifier, password}) => { - logger.debug(`session: login`, { - service, - identifier, - }) + logger.debug( + `session: login`, + { + service, + identifier, + }, + logger.DebugContext.session, + ) const agent = new BskyAgent({service}) @@ -175,9 +204,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const account: persisted.PersistedAccount = { service, did: agent.session.did, + handle: agent.session.handle, + email: agent.session.email!, // TODO this is always defined? + emailConfirmed: agent.session.emailConfirmed || false, refreshJwt: agent.session.refreshJwt, accessJwt: agent.session.accessJwt, - handle: agent.session.handle, } agent.setPersistSessionHandler( @@ -189,16 +220,20 @@ export function Provider({children}: React.PropsWithChildren<{}>) { setState(s => ({...s, agent})) upsertAccount(account) - logger.debug(`session: logged in`, { - service, - identifier, - }) + logger.debug( + `session: logged in`, + { + service, + identifier, + }, + logger.DebugContext.session, + ) }, [upsertAccount], ) const logout = React.useCallback<ApiContext['logout']>(async () => { - logger.debug(`session: logout`) + logger.debug(`session: logout`, {}, logger.DebugContext.session) setState(s => { return { ...s, @@ -215,10 +250,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const initSession = React.useCallback<ApiContext['initSession']>( async account => { - logger.debug(`session: initSession`, { - did: account.did, - handle: account.handle, - }) + logger.debug( + `session: initSession`, + { + did: account.did, + handle: account.handle, + }, + logger.DebugContext.session, + ) const agent = new BskyAgent({ service: account.service, @@ -289,19 +328,50 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const updatedAccount = { ...currentAccount, - handle: account.handle, // only update handle rn + handle: account.handle || currentAccount.handle, + email: account.email || currentAccount.email, + emailConfirmed: + account.emailConfirmed !== undefined + ? account.emailConfirmed + : currentAccount.emailConfirmed, } return { ...s, currentAccount: updatedAccount, - accounts: s.accounts.filter(a => a.did !== currentAccount.did), + accounts: [ + updatedAccount, + ...s.accounts.filter(a => a.did !== currentAccount.did), + ], } }) }, [setState], ) + const selectAccount = React.useCallback<ApiContext['selectAccount']>( + async account => { + setState(s => ({...s, isSwitchingAccounts: true})) + try { + await initSession(account) + setState(s => ({...s, isSwitchingAccounts: false})) + } catch (e) { + // reset this in case of error + setState(s => ({...s, isSwitchingAccounts: false})) + // but other listeners need a throw + throw e + } + }, + [setState, initSession], + ) + + const clearCurrentAccount = React.useCallback(() => { + setState(s => ({ + ...s, + currentAccount: undefined, + })) + }, [setState]) + React.useEffect(() => { persisted.write('session', { accounts: state.accounts, @@ -313,28 +383,36 @@ export function Provider({children}: React.PropsWithChildren<{}>) { return persisted.onUpdate(() => { const session = persisted.get('session') - logger.debug(`session: onUpdate`) + logger.debug(`session: onUpdate`, {}, logger.DebugContext.session) if (session.currentAccount) { if (session.currentAccount?.did !== state.currentAccount?.did) { - logger.debug(`session: switching account`, { - from: { - did: state.currentAccount?.did, - handle: state.currentAccount?.handle, + logger.debug( + `session: switching account`, + { + from: { + did: state.currentAccount?.did, + handle: state.currentAccount?.handle, + }, + to: { + did: session.currentAccount.did, + handle: session.currentAccount.handle, + }, }, - to: { - did: session.currentAccount.did, - handle: session.currentAccount.handle, - }, - }) + logger.DebugContext.session, + ) initSession(session.currentAccount) } } else if (!session.currentAccount && state.currentAccount) { - logger.debug(`session: logging out`, { - did: state.currentAccount?.did, - handle: state.currentAccount?.handle, - }) + logger.debug( + `session: logging out`, + { + did: state.currentAccount?.did, + handle: state.currentAccount?.handle, + }, + logger.DebugContext.session, + ) logout() } @@ -357,7 +435,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { initSession, resumeSession, removeAccount, + selectAccount, updateCurrentAccount, + clearCurrentAccount, }), [ createAccount, @@ -366,7 +446,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { initSession, resumeSession, removeAccount, + selectAccount, updateCurrentAccount, + clearCurrentAccount, ], ) diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx index 3e2c9c1bf..0d8172964 100644 --- a/src/view/com/auth/LoggedOut.tsx +++ b/src/view/com/auth/LoggedOut.tsx @@ -6,7 +6,6 @@ import {CreateAccount} from 'view/com/auth/create/CreateAccount' import {ErrorBoundary} from 'view/com/util/ErrorBoundary' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' -import {useStores} from 'state/index' import {useAnalytics} from 'lib/analytics/analytics' import {SplashScreen} from './SplashScreen' import {useSetMinimalShellMode} from '#/state/shell/minimal-mode' @@ -19,7 +18,6 @@ enum ScreenState { export const LoggedOut = observer(function LoggedOutImpl() { const pal = usePalette('default') - const store = useStores() const setMinimalShellMode = useSetMinimalShellMode() const {screen} = useAnalytics() const [screenState, setScreenState] = React.useState<ScreenState>( @@ -31,10 +29,7 @@ export const LoggedOut = observer(function LoggedOutImpl() { setMinimalShellMode(true) }, [screen, setMinimalShellMode]) - if ( - store.session.isResumingSession || - screenState === ScreenState.S_LoginOrCreateAccount - ) { + if (screenState === ScreenState.S_LoginOrCreateAccount) { return ( <SplashScreen onPressSignin={() => setScreenState(ScreenState.S_Login)} diff --git a/src/view/com/auth/create/CreateAccount.tsx b/src/view/com/auth/create/CreateAccount.tsx index 8e2bbed85..0f56755df 100644 --- a/src/view/com/auth/create/CreateAccount.tsx +++ b/src/view/com/auth/create/CreateAccount.tsx @@ -18,6 +18,7 @@ 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 {Step1} from './Step1' import {Step2} from './Step2' @@ -34,6 +35,7 @@ export const CreateAccount = observer(function CreateAccountImpl({ const model = React.useMemo(() => new CreateAccountModel(store), [store]) const {_} = useLingui() const onboardingDispatch = useOnboardingDispatch() + const {createAccount} = useSessionApi() React.useEffect(() => { screen('CreateAccount') @@ -64,14 +66,18 @@ export const CreateAccount = observer(function CreateAccountImpl({ model.next() } else { try { - await model.submit(onboardingDispatch) + console.log('BEFORE') + await model.submit({ + onboardingDispatch, + createAccount, + }) } catch { // dont need to handle here } finally { track('Try Create Account') } } - }, [model, track, onboardingDispatch]) + }, [model, track, onboardingDispatch, createAccount]) return ( <LoggedOutLayout diff --git a/src/view/com/auth/login/ChooseAccountForm.tsx b/src/view/com/auth/login/ChooseAccountForm.tsx index 596a8e411..38c13ba09 100644 --- a/src/view/com/auth/login/ChooseAccountForm.tsx +++ b/src/view/com/auth/login/ChooseAccountForm.tsx @@ -1,52 +1,93 @@ import React from 'react' -import { - ActivityIndicator, - ScrollView, - TouchableOpacity, - View, -} from 'react-native' +import {ScrollView, TouchableOpacity, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {useAnalytics} from 'lib/analytics/analytics' import {Text} from '../../util/text/Text' import {UserAvatar} from '../../util/UserAvatar' import {s} from 'lib/styles' -import {RootStoreModel} from 'state/index' import {AccountData} from 'state/models/session' import {usePalette} from 'lib/hooks/usePalette' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {styles} from './styles' +import {useSession, useSessionApi, SessionAccount} from '#/state/session' +import {useGetProfile} from '#/data/useGetProfile' +function AccountItem({ + account, + onSelect, +}: { + account: SessionAccount + onSelect: (account: SessionAccount) => void +}) { + const pal = usePalette('default') + const {_} = useLingui() + const {isError, data} = useGetProfile({did: account.did}) + + const onPress = React.useCallback(() => { + onSelect(account) + }, [account, onSelect]) + + if (isError) return null + + return ( + <TouchableOpacity + testID={`chooseAccountBtn-${account.handle}`} + key={account.did} + style={[pal.view, pal.border, styles.account]} + onPress={onPress} + accessibilityRole="button" + accessibilityLabel={_(msg`Sign in as ${account.handle}`)} + accessibilityHint="Double tap to sign in"> + <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> + <View style={s.p10}> + <UserAvatar avatar={data?.avatar} size={30} /> + </View> + <Text style={styles.accountText}> + <Text type="lg-bold" style={pal.text}> + {data?.displayName || account.handle}{' '} + </Text> + <Text type="lg" style={[pal.textLight]}> + {account.handle} + </Text> + </Text> + <FontAwesomeIcon + icon="angle-right" + size={16} + style={[pal.text, s.mr10]} + /> + </View> + </TouchableOpacity> + ) +} export const ChooseAccountForm = ({ - store, onSelectAccount, onPressBack, }: { - store: RootStoreModel onSelectAccount: (account?: AccountData) => void onPressBack: () => void }) => { const {track, screen} = useAnalytics() const pal = usePalette('default') - const [isProcessing, setIsProcessing] = React.useState(false) const {_} = useLingui() + const {accounts} = useSession() + const {initSession} = useSessionApi() React.useEffect(() => { screen('Choose Account') }, [screen]) - const onTryAccount = async (account: AccountData) => { - if (account.accessJwt && account.refreshJwt) { - setIsProcessing(true) - if (await store.session.resumeSession(account)) { + const onSelect = React.useCallback( + async (account: SessionAccount) => { + if (account.accessJwt) { + await initSession(account) track('Sign In', {resumedSession: true}) - setIsProcessing(false) - return + } else { + onSelectAccount(account) } - setIsProcessing(false) - } - onSelectAccount(account) - } + }, + [track, initSession, onSelectAccount], + ) return ( <ScrollView testID="chooseAccountForm" style={styles.maxHeight}> @@ -55,35 +96,8 @@ export const ChooseAccountForm = ({ style={[pal.text, styles.groupLabel, s.mt5, s.mb10]}> <Trans>Sign in as...</Trans> </Text> - {store.session.accounts.map(account => ( - <TouchableOpacity - testID={`chooseAccountBtn-${account.handle}`} - key={account.did} - style={[pal.view, pal.border, styles.account]} - onPress={() => onTryAccount(account)} - accessibilityRole="button" - accessibilityLabel={_(msg`Sign in as ${account.handle}`)} - accessibilityHint="Double tap to sign in"> - <View - style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> - <View style={s.p10}> - <UserAvatar avatar={account.aviUrl} size={30} /> - </View> - <Text style={styles.accountText}> - <Text type="lg-bold" style={pal.text}> - {account.displayName || account.handle}{' '} - </Text> - <Text type="lg" style={[pal.textLight]}> - {account.handle} - </Text> - </Text> - <FontAwesomeIcon - icon="angle-right" - size={16} - style={[pal.text, s.mr10]} - /> - </View> - </TouchableOpacity> + {accounts.map(account => ( + <AccountItem key={account.did} account={account} onSelect={onSelect} /> ))} <TouchableOpacity testID="chooseNewAccountBtn" @@ -112,7 +126,6 @@ export const ChooseAccountForm = ({ </Text> </TouchableOpacity> <View style={s.flex1} /> - {isProcessing && <ActivityIndicator />} </View> </ScrollView> ) diff --git a/src/view/com/auth/login/ForgotPasswordForm.tsx b/src/view/com/auth/login/ForgotPasswordForm.tsx index 9bfab18b5..a794665c9 100644 --- a/src/view/com/auth/login/ForgotPasswordForm.tsx +++ b/src/view/com/auth/login/ForgotPasswordForm.tsx @@ -15,7 +15,6 @@ import {useAnalytics} from 'lib/analytics/analytics' import {Text} from '../../util/text/Text' import {s} from 'lib/styles' import {toNiceDomain} from 'lib/strings/url-helpers' -import {RootStoreModel} from 'state/index' import {ServiceDescription} from 'state/models/session' import {isNetworkError} from 'lib/strings/errors' import {usePalette} from 'lib/hooks/usePalette' @@ -36,7 +35,6 @@ export const ForgotPasswordForm = ({ onPressBack, onEmailSent, }: { - store: RootStoreModel error: string serviceUrl: string serviceDescription: ServiceDescription | undefined diff --git a/src/view/com/auth/login/Login.tsx b/src/view/com/auth/login/Login.tsx index 401b7d980..de00d6ca2 100644 --- a/src/view/com/auth/login/Login.tsx +++ b/src/view/com/auth/login/Login.tsx @@ -14,6 +14,7 @@ import {SetNewPasswordForm} from './SetNewPasswordForm' import {PasswordUpdatedForm} from './PasswordUpdatedForm' import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' +import {useSession} from '#/state/session' enum Forms { Login, @@ -26,6 +27,7 @@ enum Forms { export const Login = ({onPressBack}: {onPressBack: () => void}) => { const pal = usePalette('default') const store = useStores() + const {accounts} = useSession() const {track} = useAnalytics() const {_} = useLingui() const [error, setError] = useState<string>('') @@ -36,7 +38,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { >(undefined) const [initialHandle, setInitialHandle] = useState<string>('') const [currentForm, setCurrentForm] = useState<Forms>( - store.session.hasAccounts ? Forms.ChooseAccount : Forms.Login, + accounts.length ? Forms.ChooseAccount : Forms.Login, ) const onSelectAccount = (account?: AccountData) => { @@ -95,7 +97,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { title={_(msg`Sign in`)} description={_(msg`Enter your username and password`)}> <LoginForm - store={store} error={error} serviceUrl={serviceUrl} serviceDescription={serviceDescription} @@ -114,7 +115,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { title={_(msg`Sign in as...`)} description={_(msg`Select from an existing account`)}> <ChooseAccountForm - store={store} onSelectAccount={onSelectAccount} onPressBack={onPressBack} /> @@ -126,7 +126,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { title={_(msg`Forgot Password`)} description={_(msg`Let's get your password reset!`)}> <ForgotPasswordForm - store={store} error={error} serviceUrl={serviceUrl} serviceDescription={serviceDescription} @@ -143,7 +142,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { title={_(msg`Forgot Password`)} description={_(msg`Let's get your password reset!`)}> <SetNewPasswordForm - store={store} error={error} serviceUrl={serviceUrl} setError={setError} diff --git a/src/view/com/auth/login/LoginForm.tsx b/src/view/com/auth/login/LoginForm.tsx index 166a7cbd8..be3a95131 100644 --- a/src/view/com/auth/login/LoginForm.tsx +++ b/src/view/com/auth/login/LoginForm.tsx @@ -15,7 +15,6 @@ import {Text} from '../../util/text/Text' import {s} from 'lib/styles' import {createFullHandle} from 'lib/strings/handles' import {toNiceDomain} from 'lib/strings/url-helpers' -import {RootStoreModel} from 'state/index' import {ServiceDescription} from 'state/models/session' import {isNetworkError} from 'lib/strings/errors' import {usePalette} from 'lib/hooks/usePalette' @@ -29,7 +28,6 @@ import {useLingui} from '@lingui/react' import {useModalControls} from '#/state/modals' export const LoginForm = ({ - store, error, serviceUrl, serviceDescription, @@ -40,7 +38,6 @@ export const LoginForm = ({ onPressBack, onPressForgotPassword, }: { - store: RootStoreModel error: string serviceUrl: string serviceDescription: ServiceDescription | undefined @@ -106,11 +103,6 @@ export const LoginForm = ({ identifier: fullIdent, password, }) - await store.session.login({ - service: serviceUrl, - identifier: fullIdent, - password, - }) } catch (e: any) { const errMsg = e.toString() logger.warn('Failed to login', {error: e}) diff --git a/src/view/com/auth/login/SetNewPasswordForm.tsx b/src/view/com/auth/login/SetNewPasswordForm.tsx index 04eaa2842..2bb614df2 100644 --- a/src/view/com/auth/login/SetNewPasswordForm.tsx +++ b/src/view/com/auth/login/SetNewPasswordForm.tsx @@ -10,7 +10,6 @@ import {BskyAgent} from '@atproto/api' import {useAnalytics} from 'lib/analytics/analytics' import {Text} from '../../util/text/Text' import {s} from 'lib/styles' -import {RootStoreModel} from 'state/index' import {isNetworkError} from 'lib/strings/errors' import {usePalette} from 'lib/hooks/usePalette' import {useTheme} from 'lib/ThemeContext' @@ -27,7 +26,6 @@ export const SetNewPasswordForm = ({ onPressBack, onPasswordSet, }: { - store: RootStoreModel error: string serviceUrl: string setError: (v: string) => void diff --git a/src/view/com/auth/withAuthRequired.tsx b/src/view/com/auth/withAuthRequired.tsx index 898f81051..4b8b31d6c 100644 --- a/src/view/com/auth/withAuthRequired.tsx +++ b/src/view/com/auth/withAuthRequired.tsx @@ -6,7 +6,6 @@ import { TouchableOpacity, } from 'react-native' import {observer} from 'mobx-react-lite' -import {useStores} from 'state/index' import {CenteredView} from '../util/Views' import {LoggedOut} from './LoggedOut' import {Onboarding} from './Onboarding' @@ -14,17 +13,18 @@ import {Text} from '../util/text/Text' import {usePalette} from 'lib/hooks/usePalette' import {STATUS_PAGE_URL} from 'lib/constants' import {useOnboardingState} from '#/state/shell' +import {useSession} from '#/state/session' export const withAuthRequired = <P extends object>( Component: React.ComponentType<P>, ): React.FC<P> => observer(function AuthRequired(props: P) { - const store = useStores() + const {isInitialLoad, hasSession} = useSession() const onboardingState = useOnboardingState() - if (store.session.isResumingSession) { + if (isInitialLoad) { return <Loading /> } - if (!store.session.hasSession) { + if (!hasSession) { return <LoggedOut /> } if (onboardingState.isActive) { diff --git a/src/view/com/modals/ChangeEmail.tsx b/src/view/com/modals/ChangeEmail.tsx index 710c0588e..6f7a92102 100644 --- a/src/view/com/modals/ChangeEmail.tsx +++ b/src/view/com/modals/ChangeEmail.tsx @@ -6,7 +6,6 @@ import {Text} from '../util/text/Text' import {Button} from '../util/forms/Button' import {ErrorMessage} from '../util/error/ErrorMessage' import * as Toast from '../util/Toast' -import {useStores} from 'state/index' import {s, colors} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {isWeb} from 'platform/detection' @@ -15,6 +14,7 @@ import {cleanError} from 'lib/strings/errors' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useModalControls} from '#/state/modals' +import {useSession, useSessionApi} from '#/state/session' enum Stages { InputEmail, @@ -26,12 +26,11 @@ export const snapPoints = ['90%'] export const Component = observer(function Component({}: {}) { const pal = usePalette('default') - const store = useStores() + const {agent, currentAccount} = useSession() + const {updateCurrentAccount} = useSessionApi() const {_} = useLingui() const [stage, setStage] = useState<Stages>(Stages.InputEmail) - const [email, setEmail] = useState<string>( - store.session.currentSession?.email || '', - ) + const [email, setEmail] = useState<string>(currentAccount?.email || '') const [confirmationCode, setConfirmationCode] = useState<string>('') const [isProcessing, setIsProcessing] = useState<boolean>(false) const [error, setError] = useState<string>('') @@ -39,19 +38,19 @@ export const Component = observer(function Component({}: {}) { const {openModal, closeModal} = useModalControls() const onRequestChange = async () => { - if (email === store.session.currentSession?.email) { + if (email === currentAccount?.email) { setError('Enter your new email above') return } setError('') setIsProcessing(true) try { - const res = await store.agent.com.atproto.server.requestEmailUpdate() + const res = await agent.com.atproto.server.requestEmailUpdate() if (res.data.tokenRequired) { setStage(Stages.ConfirmCode) } else { - await store.agent.com.atproto.server.updateEmail({email: email.trim()}) - store.session.updateLocalAccountData({ + await agent.com.atproto.server.updateEmail({email: email.trim()}) + updateCurrentAccount({ email: email.trim(), emailConfirmed: false, }) @@ -79,11 +78,11 @@ export const Component = observer(function Component({}: {}) { setError('') setIsProcessing(true) try { - await store.agent.com.atproto.server.updateEmail({ + await agent.com.atproto.server.updateEmail({ email: email.trim(), token: confirmationCode.trim(), }) - store.session.updateLocalAccountData({ + updateCurrentAccount({ email: email.trim(), emailConfirmed: false, }) @@ -120,8 +119,8 @@ export const Component = observer(function Component({}: {}) { ) : stage === Stages.ConfirmCode ? ( <Trans> An email has been sent to your previous address,{' '} - {store.session.currentSession?.email || ''}. It includes a - confirmation code which you can enter below. + {currentAccount?.email || ''}. It includes a confirmation code + which you can enter below. </Trans> ) : ( <Trans> diff --git a/src/view/com/modals/SwitchAccount.tsx b/src/view/com/modals/SwitchAccount.tsx index 1d9457995..2ff70eea4 100644 --- a/src/view/com/modals/SwitchAccount.tsx +++ b/src/view/com/modals/SwitchAccount.tsx @@ -6,7 +6,6 @@ import { View, } from 'react-native' import {Text} from '../util/text/Text' -import {useStores} from 'state/index' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {useAnalytics} from 'lib/analytics/analytics' @@ -19,26 +18,94 @@ import {BottomSheetScrollView} from '@gorhom/bottom-sheet' import {Haptics} from 'lib/haptics' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useSession, useSessionApi, SessionAccount} from '#/state/session' +import {useGetProfile} from '#/data/useGetProfile' export const snapPoints = ['40%', '90%'] -export function Component({}: {}) { +function SwitchAccountCard({account}: {account: SessionAccount}) { const pal = usePalette('default') + const {_} = useLingui() const {track} = useAnalytics() - const {_: _lingui} = useLingui() + const {isSwitchingAccounts, currentAccount} = useSession() + const {logout} = useSessionApi() + const {isError, data: profile} = useGetProfile({did: account.did}) + const isCurrentAccount = account.did === currentAccount?.did + const {onPressSwitchAccount} = useAccountSwitcher() + + const onPressSignout = React.useCallback(() => { + track('Settings:SignOutButtonClicked') + logout() + }, [track, logout]) + + // TODO + if (isError || !currentAccount) return null + + const contents = ( + <View style={[pal.view, styles.linkCard]}> + <View style={styles.avi}> + <UserAvatar size={40} avatar={profile?.avatar} /> + </View> + <View style={[s.flex1]}> + <Text type="md-bold" style={pal.text} numberOfLines={1}> + {profile?.displayName || currentAccount.handle} + </Text> + <Text type="sm" style={pal.textLight} numberOfLines={1}> + {currentAccount.handle} + </Text> + </View> + + {isCurrentAccount ? ( + <TouchableOpacity + testID="signOutBtn" + onPress={isSwitchingAccounts ? undefined : onPressSignout} + accessibilityRole="button" + accessibilityLabel={_(msg`Sign out`)} + accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}> + <Text type="lg" style={pal.link}> + <Trans>Sign out</Trans> + </Text> + </TouchableOpacity> + ) : ( + <AccountDropdownBtn handle={account.handle} /> + )} + </View> + ) - const store = useStores() - const [isSwitching, _, onPressSwitchAccount] = useAccountSwitcher() + return isCurrentAccount ? ( + <Link + href={makeProfileLink({ + did: currentAccount.did, + handle: currentAccount.handle, + })} + title="Your profile" + noFeedback> + {contents} + </Link> + ) : ( + <TouchableOpacity + testID={`switchToAccountBtn-${account.handle}`} + key={account.did} + style={[isSwitchingAccounts && styles.dimmed]} + onPress={ + isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account) + } + accessibilityRole="button" + accessibilityLabel={`Switch to ${account.handle}`} + accessibilityHint="Switches the account you are logged in to"> + {contents} + </TouchableOpacity> + ) +} + +export function Component({}: {}) { + const pal = usePalette('default') + const {isSwitchingAccounts, currentAccount, accounts} = useSession() React.useEffect(() => { Haptics.default() }) - const onPressSignout = React.useCallback(() => { - track('Settings:SignOutButtonClicked') - store.session.logout() - }, [track, store]) - return ( <BottomSheetScrollView style={[styles.container, pal.view]} @@ -46,62 +113,20 @@ export function Component({}: {}) { <Text type="title-xl" style={[styles.title, pal.text]}> <Trans>Switch Account</Trans> </Text> - {isSwitching ? ( + + {isSwitchingAccounts || !currentAccount ? ( <View style={[pal.view, styles.linkCard]}> <ActivityIndicator /> </View> ) : ( - <Link href={makeProfileLink(store.me)} title="Your profile" noFeedback> - <View style={[pal.view, styles.linkCard]}> - <View style={styles.avi}> - <UserAvatar size={40} avatar={store.me.avatar} /> - </View> - <View style={[s.flex1]}> - <Text type="md-bold" style={pal.text} numberOfLines={1}> - {store.me.displayName || store.me.handle} - </Text> - <Text type="sm" style={pal.textLight} numberOfLines={1}> - {store.me.handle} - </Text> - </View> - <TouchableOpacity - testID="signOutBtn" - onPress={isSwitching ? undefined : onPressSignout} - accessibilityRole="button" - accessibilityLabel={_lingui(msg`Sign out`)} - accessibilityHint={`Signs ${store.me.displayName} out of Bluesky`}> - <Text type="lg" style={pal.link}> - <Trans>Sign out</Trans> - </Text> - </TouchableOpacity> - </View> - </Link> + <SwitchAccountCard account={currentAccount} /> )} - {store.session.switchableAccounts.map(account => ( - <TouchableOpacity - testID={`switchToAccountBtn-${account.handle}`} - key={account.did} - style={[pal.view, styles.linkCard, isSwitching && styles.dimmed]} - onPress={ - isSwitching ? undefined : () => onPressSwitchAccount(account) - } - accessibilityRole="button" - accessibilityLabel={`Switch to ${account.handle}`} - accessibilityHint="Switches the account you are logged in to"> - <View style={styles.avi}> - <UserAvatar size={40} avatar={account.aviUrl} /> - </View> - <View style={[s.flex1]}> - <Text type="md-bold" style={pal.text}> - {account.displayName || account.handle} - </Text> - <Text type="sm" style={pal.textLight}> - {account.handle} - </Text> - </View> - <AccountDropdownBtn handle={account.handle} /> - </TouchableOpacity> - ))} + + {accounts + .filter(a => a.did !== currentAccount?.did) + .map(account => ( + <SwitchAccountCard key={account.did} account={account} /> + ))} </BottomSheetScrollView> ) } diff --git a/src/view/com/modals/VerifyEmail.tsx b/src/view/com/modals/VerifyEmail.tsx index e48e0e4a2..106e05b87 100644 --- a/src/view/com/modals/VerifyEmail.tsx +++ b/src/view/com/modals/VerifyEmail.tsx @@ -14,7 +14,6 @@ import {Text} from '../util/text/Text' import {Button} from '../util/forms/Button' import {ErrorMessage} from '../util/error/ErrorMessage' import * as Toast from '../util/Toast' -import {useStores} from 'state/index' import {s, colors} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {isWeb} from 'platform/detection' @@ -23,6 +22,7 @@ import {cleanError} from 'lib/strings/errors' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useModalControls} from '#/state/modals' +import {useSession, useSessionApi} from '#/state/session' export const snapPoints = ['90%'] @@ -38,7 +38,8 @@ export const Component = observer(function Component({ showReminder?: boolean }) { const pal = usePalette('default') - const store = useStores() + const {agent, currentAccount} = useSession() + const {updateCurrentAccount} = useSessionApi() const {_} = useLingui() const [stage, setStage] = useState<Stages>( showReminder ? Stages.Reminder : Stages.Email, @@ -53,7 +54,7 @@ export const Component = observer(function Component({ setError('') setIsProcessing(true) try { - await store.agent.com.atproto.server.requestEmailConfirmation() + await agent.com.atproto.server.requestEmailConfirmation() setStage(Stages.ConfirmCode) } catch (e) { setError(cleanError(String(e))) @@ -66,11 +67,11 @@ export const Component = observer(function Component({ setError('') setIsProcessing(true) try { - await store.agent.com.atproto.server.confirmEmail({ - email: (store.session.currentSession?.email || '').trim(), + await agent.com.atproto.server.confirmEmail({ + email: (currentAccount?.email || '').trim(), token: confirmationCode.trim(), }) - store.session.updateLocalAccountData({emailConfirmed: true}) + updateCurrentAccount({emailConfirmed: true}) Toast.show('Email verified') closeModal() } catch (e) { @@ -112,9 +113,8 @@ export const Component = observer(function Component({ </Trans> ) : stage === Stages.ConfirmCode ? ( <Trans> - An email has been sent to{' '} - {store.session.currentSession?.email || ''}. It includes a - confirmation code which you can enter below. + An email has been sent to {currentAccount?.email || ''}. It + includes a confirmation code which you can enter below. </Trans> ) : ( '' @@ -130,7 +130,7 @@ export const Component = observer(function Component({ size={16} /> <Text type="xl-medium" style={[pal.text, s.flex1, {minWidth: 0}]}> - {store.session.currentSession?.email || ''} + {currentAccount?.email || ''} </Text> </View> <Pressable diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 062533c27..4fd2f2d53 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -57,7 +57,8 @@ import { useRequireAltTextEnabled, useSetRequireAltTextEnabled, } from '#/state/preferences' -import {useSession, useSessionApi} from '#/state/session' +import {useSession, useSessionApi, SessionAccount} from '#/state/session' +import {useGetProfile} from '#/data/useGetProfile' // TEMPORARY (APP-700) // remove after backend testing finishes @@ -67,6 +68,73 @@ import {STATUS_PAGE_URL} from 'lib/constants' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +function SettingsAccountCard({account}: {account: SessionAccount}) { + const pal = usePalette('default') + const {isSwitchingAccounts, currentAccount} = useSession() + const {logout} = useSessionApi() + const {isError, data} = useGetProfile({did: account.did}) + const isCurrentAccount = account.did === currentAccount?.did + const {onPressSwitchAccount} = useAccountSwitcher() + + // TODO + if (isError || !currentAccount) return null + + const contents = ( + <View style={[pal.view, styles.linkCard]}> + <View style={styles.avi}> + <UserAvatar size={40} avatar={data?.avatar} /> + </View> + <View style={[s.flex1]}> + <Text type="md-bold" style={pal.text}> + {data?.displayName || account.handle} + </Text> + <Text type="sm" style={pal.textLight}> + {account.handle} + </Text> + </View> + + {isCurrentAccount ? ( + <TouchableOpacity + testID="signOutBtn" + onPress={logout} + accessibilityRole="button" + accessibilityLabel="Sign out" + accessibilityHint={`Signs ${data?.displayName} out of Bluesky`}> + <Text type="lg" style={pal.link}> + Sign out + </Text> + </TouchableOpacity> + ) : ( + <AccountDropdownBtn handle={account.handle} /> + )} + </View> + ) + + return isCurrentAccount ? ( + <Link + href={makeProfileLink({ + did: currentAccount?.did, + handle: currentAccount?.handle, + })} + title="Your profile" + noFeedback> + {contents} + </Link> + ) : ( + <TouchableOpacity + testID={`switchToAccountBtn-${account.handle}`} + key={account.did} + onPress={ + isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account) + } + accessibilityRole="button" + accessibilityLabel={`Switch to ${account.handle}`} + accessibilityHint="Switches the account you are logged in to"> + {contents} + </TouchableOpacity> + ) +} + type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> export const SettingsScreen = withAuthRequired( observer(function Settings({}: Props) { @@ -82,14 +150,12 @@ export const SettingsScreen = withAuthRequired( const navigation = useNavigation<NavigationProp>() const {isMobile} = useWebMediaQueries() const {screen, track} = useAnalytics() - const [isSwitching, setIsSwitching, onPressSwitchAccount] = - useAccountSwitcher() const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting( store.agent, ) const {openModal} = useModalControls() - const {logout} = useSessionApi() - const {accounts} = useSession() + const {isSwitchingAccounts, accounts, currentAccount} = useSession() + const {clearCurrentAccount} = useSessionApi() const primaryBg = useCustomPalette<ViewStyle>({ light: {backgroundColor: colors.blue0}, @@ -120,30 +186,27 @@ export const SettingsScreen = withAuthRequired( track('Settings:AddAccountButtonClicked') navigation.navigate('HomeTab') navigation.dispatch(StackActions.popToTop()) - store.session.clear() - }, [track, navigation, store]) + clearCurrentAccount() + }, [track, navigation, clearCurrentAccount]) const onPressChangeHandle = React.useCallback(() => { track('Settings:ChangeHandleButtonClicked') openModal({ name: 'change-handle', onChanged() { - setIsSwitching(true) store.session.reloadFromServer().then( () => { - setIsSwitching(false) Toast.show('Your handle has been updated') }, err => { logger.error('Failed to reload from server after handle update', { error: err, }) - setIsSwitching(false) }, ) }, }) - }, [track, store, openModal, setIsSwitching]) + }, [track, store, openModal]) const onPressInviteCodes = React.useCallback(() => { track('Settings:InvitecodesButtonClicked') @@ -154,12 +217,6 @@ export const SettingsScreen = withAuthRequired( navigation.navigate('LanguageSettings') }, [navigation]) - const onPressSignout = React.useCallback(() => { - track('Settings:SignOutButtonClicked') - logout() - store.session.logout() - }, [track, store, logout]) - const onPressDeleteAccount = React.useCallback(() => { openModal({name: 'delete-account'}) }, [openModal]) @@ -217,7 +274,7 @@ export const SettingsScreen = withAuthRequired( contentContainerStyle={isMobile && pal.viewLight} scrollIndicatorInsets={{right: 1}}> <View style={styles.spacer20} /> - {store.session.currentSession !== undefined ? ( + {currentAccount ? ( <> <Text type="xl-bold" style={[pal.text, styles.heading]}> <Trans>Account</Trans> @@ -226,7 +283,7 @@ export const SettingsScreen = withAuthRequired( <Text type="lg-medium" style={pal.text}> Email:{' '} </Text> - {!store.session.emailNeedsConfirmation && ( + {currentAccount.emailConfirmed && ( <> <FontAwesomeIcon icon="check" @@ -236,7 +293,7 @@ export const SettingsScreen = withAuthRequired( </> )} <Text type="lg" style={pal.text}> - {store.session.currentSession?.email}{' '} + {currentAccount.email}{' '} </Text> <Link onPress={() => openModal({name: 'change-email'})}> <Text type="lg" style={pal.link}> @@ -255,7 +312,8 @@ export const SettingsScreen = withAuthRequired( </Link> </View> <View style={styles.spacer20} /> - <EmailConfirmationNotice /> + + {!currentAccount.emailConfirmed && <EmailConfirmationNotice />} </> ) : null} <View style={[s.flexRow, styles.heading]}> @@ -264,70 +322,29 @@ export const SettingsScreen = withAuthRequired( </Text> <View style={s.flex1} /> </View> - {isSwitching ? ( + + {isSwitchingAccounts ? ( <View style={[pal.view, styles.linkCard]}> <ActivityIndicator /> </View> ) : ( - <Link - href={makeProfileLink(store.me)} - title="Your profile" - noFeedback> - <View style={[pal.view, styles.linkCard]}> - <View style={styles.avi}> - <UserAvatar size={40} avatar={store.me.avatar} /> - </View> - <View style={[s.flex1]}> - <Text type="md-bold" style={pal.text} numberOfLines={1}> - {store.me.displayName || store.me.handle} - </Text> - <Text type="sm" style={pal.textLight} numberOfLines={1}> - {store.me.handle} - </Text> - </View> - <TouchableOpacity - testID="signOutBtn" - onPress={isSwitching ? undefined : onPressSignout} - accessibilityRole="button" - accessibilityLabel={_(msg`Sign out`)} - accessibilityHint={`Signs ${store.me.displayName} out of Bluesky`}> - <Text type="lg" style={pal.link}> - <Trans>Sign out</Trans> - </Text> - </TouchableOpacity> - </View> - </Link> + <SettingsAccountCard account={currentAccount!} /> )} - {accounts.map(account => ( - <TouchableOpacity - testID={`switchToAccountBtn-${account.handle}`} - key={account.did} - style={[pal.view, styles.linkCard, isSwitching && styles.dimmed]} - onPress={ - isSwitching ? undefined : () => onPressSwitchAccount(account) - } - accessibilityRole="button" - accessibilityLabel={`Switch to ${account.handle}`} - accessibilityHint="Switches the account you are logged in to"> - <View style={styles.avi}> - {/*<UserAvatar size={40} avatar={account.aviUrl} />*/} - </View> - <View style={[s.flex1]}> - <Text type="md-bold" style={pal.text}> - {/* @ts-ignore */} - {account.displayName || account.handle} - </Text> - <Text type="sm" style={pal.textLight}> - {account.handle} - </Text> - </View> - <AccountDropdownBtn handle={account.handle} /> - </TouchableOpacity> - ))} + + {accounts + .filter(a => a.did !== currentAccount?.did) + .map(account => ( + <SettingsAccountCard key={account.did} account={account} /> + ))} + <TouchableOpacity testID="switchToNewAccountBtn" - style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} - onPress={isSwitching ? undefined : onPressAddAccount} + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} + onPress={isSwitchingAccounts ? undefined : onPressAddAccount} accessibilityRole="button" accessibilityLabel={_(msg`Add account`)} accessibilityHint="Create a new Bluesky account"> @@ -349,8 +366,12 @@ export const SettingsScreen = withAuthRequired( </Text> <TouchableOpacity testID="inviteFriendBtn" - style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} - onPress={isSwitching ? undefined : onPressInviteCodes} + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} + onPress={isSwitchingAccounts ? undefined : onPressInviteCodes} accessibilityRole="button" accessibilityLabel={_(msg`Invite`)} accessibilityHint="Opens invite code list"> @@ -427,7 +448,11 @@ export const SettingsScreen = withAuthRequired( </Text> <TouchableOpacity testID="preferencesHomeFeedButton" - style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} onPress={openHomeFeedPreferences} accessibilityRole="button" accessibilityHint="" @@ -444,7 +469,11 @@ export const SettingsScreen = withAuthRequired( </TouchableOpacity> <TouchableOpacity testID="preferencesThreadsButton" - style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} onPress={openThreadsPreferences} accessibilityRole="button" accessibilityHint="" @@ -462,7 +491,11 @@ export const SettingsScreen = withAuthRequired( </TouchableOpacity> <TouchableOpacity testID="savedFeedsBtn" - style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} accessibilityHint="My Saved Feeds" accessibilityLabel={_(msg`Opens screen with all saved feeds`)} onPress={onPressSavedFeeds}> @@ -475,8 +508,12 @@ export const SettingsScreen = withAuthRequired( </TouchableOpacity> <TouchableOpacity testID="languageSettingsBtn" - style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} - onPress={isSwitching ? undefined : onPressLanguageSettings} + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} + onPress={isSwitchingAccounts ? undefined : onPressLanguageSettings} accessibilityRole="button" accessibilityHint="Language settings" accessibilityLabel={_(msg`Opens configurable language settings`)}> @@ -492,9 +529,15 @@ export const SettingsScreen = withAuthRequired( </TouchableOpacity> <TouchableOpacity testID="moderationBtn" - style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} onPress={ - isSwitching ? undefined : () => navigation.navigate('Moderation') + isSwitchingAccounts + ? undefined + : () => navigation.navigate('Moderation') } accessibilityRole="button" accessibilityHint="" @@ -513,7 +556,11 @@ export const SettingsScreen = withAuthRequired( </Text> <TouchableOpacity testID="appPasswordBtn" - style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} onPress={onPressAppPasswords} accessibilityRole="button" accessibilityHint="Open app password settings" @@ -530,8 +577,12 @@ export const SettingsScreen = withAuthRequired( </TouchableOpacity> <TouchableOpacity testID="changeHandleBtn" - style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} - onPress={isSwitching ? undefined : onPressChangeHandle} + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} + onPress={isSwitchingAccounts ? undefined : onPressChangeHandle} accessibilityRole="button" accessibilityLabel={_(msg`Change handle`)} accessibilityHint="Choose a new Bluesky username or create"> @@ -655,15 +706,10 @@ const EmailConfirmationNotice = observer( function EmailConfirmationNoticeImpl() { const pal = usePalette('default') const palInverted = usePalette('inverted') - const store = useStores() const {_} = useLingui() const {isMobile} = useWebMediaQueries() const {openModal} = useModalControls() - if (!store.session.emailNeedsConfirmation) { - return null - } - return ( <View style={{marginBottom: 20}}> <Text type="xl-bold" style={[pal.text, styles.heading]}> diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index b85823b6f..3a0c0c95d 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -41,18 +41,31 @@ import {router} from '../../../routes' import {makeProfileLink} from 'lib/routes/links' import {useLingui} from '@lingui/react' import {Trans, msg} from '@lingui/macro' +import {useGetProfile} from '#/data/useGetProfile' +import {useSession} from '#/state/session' const ProfileCard = observer(function ProfileCardImpl() { - const store = useStores() + const {currentAccount} = useSession() + const { + isLoading, + isError, + data: profile, + } = useGetProfile({did: currentAccount!.did}) const {isDesktop} = useWebMediaQueries() const size = 48 - return store.me.handle ? ( + + if (isError || !profile || !currentAccount) return null + + return !isLoading ? ( <Link - href={makeProfileLink(store.me)} + href={makeProfileLink({ + did: currentAccount.did, + handle: currentAccount.handle, + })} style={[styles.profileCard, !isDesktop && styles.profileCardTablet]} title="My Profile" asAnchor> - <UserAvatar avatar={store.me.avatar} size={size} /> + <UserAvatar avatar={profile.avatar} size={size} /> </Link> ) : ( <View style={[styles.profileCard, !isDesktop && styles.profileCardTablet]}> @@ -255,7 +268,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() { pal.view, pal.border, ]}> - {store.session.hasSession && <ProfileCard />} + <ProfileCard /> <BackBtn /> <NavItem href="/" @@ -360,26 +373,24 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() { } label="Moderation" /> - {store.session.hasSession && ( - <NavItem - href={makeProfileLink(store.me)} - icon={ - <UserIcon - strokeWidth={1.75} - size={isDesktop ? 28 : 30} - style={pal.text} - /> - } - iconFilled={ - <UserIconSolid - strokeWidth={1.75} - size={isDesktop ? 28 : 30} - style={pal.text} - /> - } - label="Profile" - /> - )} + <NavItem + href={makeProfileLink(store.me)} + icon={ + <UserIcon + strokeWidth={1.75} + size={isDesktop ? 28 : 30} + style={pal.text} + /> + } + iconFilled={ + <UserIconSolid + strokeWidth={1.75} + size={isDesktop ? 28 : 30} + style={pal.text} + /> + } + label="Profile" + /> <NavItem href="/settings" icon={ @@ -398,7 +409,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() { } label="Settings" /> - {store.session.hasSession && <ComposeBtn />} + <ComposeBtn /> </View> ) }) diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx index a4b3e5746..cb62b4be2 100644 --- a/src/view/shell/desktop/RightNav.tsx +++ b/src/view/shell/desktop/RightNav.tsx @@ -14,11 +14,13 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {pluralize} from 'lib/strings/helpers' import {formatCount} from 'view/com/util/numeric/format' import {useModalControls} from '#/state/modals' +import {useSession} from '#/state/session' export const DesktopRightNav = observer(function DesktopRightNavImpl() { const store = useStores() const pal = usePalette('default') const palError = usePalette('error') + const {hasSession, currentAccount} = useSession() const {isTablet} = useWebMediaQueries() if (isTablet) { @@ -27,8 +29,8 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() { return ( <View style={[styles.rightNav, pal.view]}> - {store.session.hasSession && <DesktopSearch />} - {store.session.hasSession && <DesktopFeeds />} + {hasSession && <DesktopSearch />} + {hasSession && <DesktopFeeds />} <View style={styles.message}> {store.session.isSandbox ? ( <View style={[palError.view, styles.messageLine, s.p10]}> @@ -42,8 +44,8 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() { type="md" style={pal.link} href={FEEDBACK_FORM_URL({ - email: store.session.currentSession?.email, - handle: store.session.currentSession?.handle, + email: currentAccount!.email, + handle: currentAccount!.handle, })} text="Send feedback" /> diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 498bc11bd..75ed07475 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -33,6 +33,7 @@ import { } from '#/state/shell' import {isAndroid} from 'platform/detection' import {useModalControls} from '#/state/modals' +import {useSession} from '#/state/session' const ShellInner = observer(function ShellInnerImpl() { const store = useStores() @@ -57,6 +58,8 @@ const ShellInner = observer(function ShellInnerImpl() { [setIsDrawerOpen], ) const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) + const {hasSession} = useSession() + React.useEffect(() => { let listener = {remove() {}} if (isAndroid) { @@ -81,9 +84,7 @@ const ShellInner = observer(function ShellInnerImpl() { onOpen={onOpenDrawer} onClose={onCloseDrawer} swipeEdgeWidth={winDim.width / 2} - swipeEnabled={ - !canGoBack && store.session.hasSession && !isDrawerSwipeDisabled - }> + swipeEnabled={!canGoBack && hasSession && !isDrawerSwipeDisabled}> <TabsNavigator /> </Drawer> </ErrorBoundary> diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 792499521..a74cd126f 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -24,6 +24,7 @@ import { useOnboardingState, } from '#/state/shell' import {useModalControls} from '#/state/modals' +import {useSession} from '#/state/session' const ShellInner = observer(function ShellInnerImpl() { const store = useStores() @@ -33,6 +34,8 @@ const ShellInner = observer(function ShellInnerImpl() { const onboardingState = useOnboardingState() const {isDesktop, isMobile} = useWebMediaQueries() const navigator = useNavigation<NavigationProp>() + const {hasSession} = useSession() + useAuxClick() useEffect(() => { @@ -44,8 +47,7 @@ const ShellInner = observer(function ShellInnerImpl() { }, [navigator, store.shell, setDrawerOpen, closeModal]) const showBottomBar = isMobile && !onboardingState.isActive - const showSideNavs = - !isMobile && store.session.hasSession && !onboardingState.isActive + const showSideNavs = !isMobile && hasSession && !onboardingState.isActive return ( <View style={[s.hContentRegion, {overflow: 'hidden'}]}> <View style={s.hContentRegion}> |