Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 294 lines 9.1 kB view raw
1import {useCallback, useEffect, useMemo, useRef, useState} from 'react' 2import {KeyboardAvoidingView} from 'react-native' 3import Animated, {FadeIn, LayoutAnimationConfig} from 'react-native-reanimated' 4import {type Did} from '@atproto/api' 5import {msg} from '@lingui/core/macro' 6import {useLingui} from '@lingui/react' 7import debounce from 'lodash.debounce' 8 9import {DEFAULT_SERVICE} from '#/lib/constants' 10import {logger} from '#/logger' 11import {resolvePdsServiceUrl} from '#/state/queries/resolve-identity' 12import {useServiceQuery} from '#/state/queries/service' 13import {type SessionAccount, useAgent, useSession} from '#/state/session' 14import {useLoggedOutView} from '#/state/shell/logged-out' 15import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' 16import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm' 17import {LoginForm} from '#/screens/Login/LoginForm' 18import {PasswordUpdatedForm} from '#/screens/Login/PasswordUpdatedForm' 19import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm' 20import {atoms as a, native} from '#/alf' 21import {ScreenTransition} from '#/components/ScreenTransition' 22import {useAnalytics} from '#/analytics' 23import {IS_WEB} from '#/env' 24import {ChooseAccountForm} from './ChooseAccountForm' 25import * as AuthLayout from './components/AuthLayout' 26import {AuthLayoutNavigationContext} from './components/AuthLayout/context' 27 28enum Forms { 29 Login, 30 ChooseAccount, 31 ForgotPassword, 32 SetNewPassword, 33 PasswordUpdated, 34} 35 36const OrderedForms = [ 37 Forms.ChooseAccount, 38 Forms.Login, 39 Forms.ForgotPassword, 40 Forms.SetNewPassword, 41 Forms.PasswordUpdated, 42] as const 43 44export const Login = ({onPressBack}: {onPressBack: () => void}) => { 45 const {_} = useLingui() 46 const failedAttemptCountRef = useRef(0) 47 const startTimeRef = useRef(Date.now()) 48 49 const agent = useAgent() 50 const {accounts} = useSession() 51 const {requestedAccountSwitchTo} = useLoggedOutView() 52 const requestedAccount = accounts.find( 53 acc => acc.did === requestedAccountSwitchTo, 54 ) 55 56 const [isResolvingService, setIsResolvingService] = useState(false) 57 const [error, setError] = useState<string>('') 58 const [serviceUrl, setServiceUrl] = useState<string | undefined>( 59 requestedAccount?.service, 60 ) 61 const [initialHandle, setInitialHandle] = useState( 62 requestedAccount?.handle || '', 63 ) 64 const [currentForm, setCurrentForm] = useState<Forms>( 65 requestedAccount 66 ? Forms.Login 67 : accounts.length 68 ? Forms.ChooseAccount 69 : Forms.Login, 70 ) 71 const [screenTransitionDirection, setScreenTransitionDirection] = useState< 72 'Forward' | 'Backward' 73 >('Forward') 74 75 const ax = useAnalytics() 76 const { 77 data: serviceDescription, 78 error: serviceError, 79 refetch: refetchService, 80 } = useServiceQuery(serviceUrl ?? '') 81 82 const onSelectAccount = (account?: SessionAccount) => { 83 if (account?.service) { 84 setServiceUrl(account.service) 85 } 86 setInitialHandle(account?.handle || '') 87 gotoForm(Forms.Login) 88 } 89 90 const gotoForm = (form: Forms) => { 91 setError('') 92 const index = OrderedForms.indexOf(currentForm) 93 const nextIndex = OrderedForms.indexOf(form) 94 setScreenTransitionDirection(index < nextIndex ? 'Forward' : 'Backward') 95 setCurrentForm(form) 96 } 97 98 useEffect(() => { 99 if (serviceError) { 100 setError( 101 _( 102 msg`Unable to contact your service. Please check your Internet connection.`, 103 ), 104 ) 105 logger.warn(`Failed to fetch service description for ${serviceUrl}`, { 106 error: String(serviceError), 107 }) 108 ax.metric('signin:hostingProviderFailedResolution', {}) 109 } else { 110 setError('') 111 } 112 }, [serviceError, serviceUrl, _]) 113 114 const resolveIdentity = useCallback( 115 async (identifier: string) => { 116 setIsResolvingService(true) 117 118 try { 119 const getDid = async () => { 120 if (identifier.startsWith('did:')) return identifier 121 else 122 return ( 123 await agent.resolveHandle({ 124 handle: identifier, 125 }) 126 ).data.did 127 } 128 129 const did = (await getDid()) as Did 130 const pdsUrl = await resolvePdsServiceUrl(did) 131 132 if (!pdsUrl) { 133 throw new Error(`No PDS service found in DID document for ${did}`) 134 } 135 136 if (pdsUrl.endsWith('.bsky.network')) { 137 setServiceUrl('https://bsky.social') 138 } else { 139 setServiceUrl(pdsUrl) 140 } 141 } catch (err) { 142 logger.error( 143 `Service auto-resolution failed: ${err instanceof Error ? err.message : String(err)}`, 144 ) 145 } finally { 146 setIsResolvingService(false) 147 } 148 }, 149 [agent], 150 ) 151 152 const debouncedResolveService = useMemo( 153 () => debounce(resolveIdentity, 400), 154 [resolveIdentity], 155 ) 156 157 const onPressForgotPassword = () => { 158 gotoForm(Forms.ForgotPassword) 159 ax.metric('signin:forgotPasswordPressed', {}) 160 } 161 162 const handlePressBack = () => { 163 onPressBack() 164 setScreenTransitionDirection('Backward') 165 ax.metric('signin:backPressed', { 166 failedAttemptsCount: failedAttemptCountRef.current, 167 }) 168 } 169 170 const onAttemptSuccess = () => { 171 ax.metric('signin:success', { 172 isUsingCustomProvider: serviceUrl !== DEFAULT_SERVICE, 173 timeTakenSeconds: Math.round((Date.now() - startTimeRef.current) / 1000), 174 failedAttemptsCount: failedAttemptCountRef.current, 175 }) 176 } 177 178 const onAttemptFailed = () => { 179 failedAttemptCountRef.current += 1 180 } 181 182 let content = null 183 let title = '' 184 let description = '' 185 let goBack = null 186 187 switch (currentForm) { 188 case Forms.Login: 189 title = _(msg`Sign in`) 190 description = IS_WEB 191 ? _(msg`Enter your handle to sign in`) 192 : _(msg`Enter your username and password`) 193 goBack = () => 194 accounts.length ? gotoForm(Forms.ChooseAccount) : handlePressBack() 195 content = ( 196 <LoginForm 197 error={error} 198 serviceUrl={serviceUrl} 199 serviceDescription={serviceDescription} 200 initialHandle={initialHandle} 201 setError={setError} 202 onAttemptFailed={onAttemptFailed} 203 onAttemptSuccess={onAttemptSuccess} 204 setServiceUrl={setServiceUrl} 205 onPressBack={goBack} 206 onPressForgotPassword={onPressForgotPassword} 207 onPressRetryConnect={refetchService} 208 debouncedResolveService={debouncedResolveService} 209 isResolvingService={isResolvingService} 210 /> 211 ) 212 break 213 case Forms.ChooseAccount: 214 title = _(msg`Sign in`) 215 description = _(msg`Select from an existing account`) 216 goBack = handlePressBack 217 content = ( 218 <ChooseAccountForm 219 onSelectAccount={onSelectAccount} 220 onPressBack={goBack} 221 /> 222 ) 223 break 224 case Forms.ForgotPassword: 225 title = _(msg`Forgot Password`) 226 description = _(msg`Let's get your password reset!`) 227 goBack = () => gotoForm(Forms.Login) 228 content = ( 229 <ForgotPasswordForm 230 error={error} 231 serviceUrl={serviceUrl ?? DEFAULT_SERVICE} 232 serviceDescription={serviceDescription} 233 setError={setError} 234 setServiceUrl={setServiceUrl} 235 onPressBack={goBack} 236 onEmailSent={() => gotoForm(Forms.SetNewPassword)} 237 /> 238 ) 239 break 240 case Forms.SetNewPassword: 241 title = _(msg`Forgot Password`) 242 description = _(msg`Let's get your password reset!`) 243 goBack = () => gotoForm(Forms.ForgotPassword) 244 content = ( 245 <SetNewPasswordForm 246 error={error} 247 serviceUrl={serviceUrl ?? DEFAULT_SERVICE} 248 setError={setError} 249 onPressBack={goBack} 250 onPasswordSet={() => gotoForm(Forms.PasswordUpdated)} 251 /> 252 ) 253 break 254 case Forms.PasswordUpdated: 255 title = _(msg`Password updated`) 256 description = _(msg`You can now sign in with your new password.`) 257 content = ( 258 <PasswordUpdatedForm onPressNext={() => gotoForm(Forms.Login)} /> 259 ) 260 break 261 } 262 263 const navigation = goBack ? {goBack} : null 264 265 return ( 266 <AuthLayoutNavigationContext value={navigation}> 267 <Animated.View style={a.flex_1} entering={native(FadeIn.duration(90))}> 268 <KeyboardAvoidingView 269 testID="signIn" 270 behavior="padding" 271 style={a.flex_1}> 272 <AuthLayout.Header.Outer> 273 <AuthLayout.Header.BackButton /> 274 <AuthLayout.Header.Content /> 275 <AuthLayout.Header.Slot /> 276 </AuthLayout.Header.Outer> 277 <LoggedOutLayout 278 leadin="" 279 title={title} 280 description={description} 281 scrollable> 282 <LayoutAnimationConfig skipEntering> 283 <ScreenTransition 284 key={currentForm} 285 direction={screenTransitionDirection}> 286 {content} 287 </ScreenTransition> 288 </LayoutAnimationConfig> 289 </LoggedOutLayout> 290 </KeyboardAvoidingView> 291 </Animated.View> 292 </AuthLayoutNavigationContext> 293 ) 294}