Bluesky app fork with some witchin' additions 💫

rework signup flow (ollies fault)

commit 1bb99ef5cb0e2fe3f2f6cd289469a2605b21d3b9
Author: Aviva Ruben <aviva@rubenfamily.com>
Date: Fri Apr 18 00:31:36 2025 -0500

clean up a bunch

commit 3ea92d5f2c07c07fd40b8b266bbbfcd805bc6a72
Author: Aviva Ruben <aviva@rubenfamily.com>
Date: Thu Apr 17 23:37:45 2025 -0500

clean up copy and add a final hint

Aviva Ruben b6493bb2 e6fa8d08

+75 -135
-108
src/screens/Signup/StepAtmosphere.tsx
··· 1 - import React from 'react' 2 - import {View} from 'react-native' 3 - import {msg, Trans} from '@lingui/macro' 4 - import {useLingui} from '@lingui/react' 5 - 6 - import {logger} from '#/logger' 7 - import {isWeb} from '#/platform/detection' 8 - import {ScreenTransition} from '#/screens/Login/ScreenTransition' 9 - import {useSignupContext} from '#/screens/Signup/state' 10 - import {atoms as a, useTheme} from '#/alf' 11 - import {Button, ButtonText} from '#/components/Button' 12 - import {InlineLinkText} from '#/components/Link' 13 - import {Text} from '#/components/Typography' 14 - import {BackNextButtons} from './BackNextButtons' 15 - 16 - export function StepAtmosphere({ 17 - onPressBack, 18 - onPressSignIn, 19 - }: { 20 - onPressBack: () => void 21 - onPressSignIn: () => void 22 - }) { 23 - const {_} = useLingui() 24 - const t = useTheme() 25 - const {state, dispatch} = useSignupContext() 26 - 27 - const onNextPress = React.useCallback(async () => { 28 - logger.metric( 29 - 'signup:nextPressed', 30 - { 31 - activeStep: state.activeStep, 32 - }, 33 - {statsig: true}, 34 - ) 35 - dispatch({type: 'next'}) 36 - }, [dispatch, state.activeStep]) 37 - 38 - const onBackPress = React.useCallback(() => { 39 - logger.metric( 40 - 'signup:backPressed', 41 - {activeStep: state.activeStep}, 42 - {statsig: true}, 43 - ) 44 - onPressBack() 45 - }, [state.activeStep, onPressBack]) 46 - 47 - return ( 48 - <ScreenTransition> 49 - <View style={[a.gap_xl]}> 50 - <Text style={[a.gap_md, a.leading_snug]}> 51 - <Trans> 52 - deer.social is part of the{' '} 53 - { 54 - <InlineLinkText 55 - label={_(msg`ATmosphere`)} 56 - to="https://atproto.com/"> 57 - <Trans>ATmosphere</Trans> 58 - </InlineLinkText> 59 - } 60 - —the network of apps, services, and accounts built on the AT 61 - Protocol. For example, if you already have a Bluesky account you are 62 - already part of this ecosystem. That means you can sign in right now 63 - with your existing account. 64 - </Trans> 65 - </Text> 66 - <View style={isWeb && [a.flex_row, a.justify_start]}> 67 - <Button 68 - testID="signInButton" 69 - onPress={onPressSignIn} 70 - label={_(msg`Sign in with ATmosphere`)} 71 - accessibilityHint={_( 72 - msg`Opens flow to sign in to your existing ATmosphere account`, 73 - )} 74 - size="large" 75 - variant="solid" 76 - color="primary"> 77 - <ButtonText> 78 - <Trans>Sign in with ATmosphere</Trans> 79 - </ButtonText> 80 - </Button> 81 - </View> 82 - <Text style={[a.gap_md, t.atoms.text_contrast_medium, a.leading_snug]}> 83 - <Trans> 84 - Don’t have an account in the ATmosphere yet? You can create one on 85 - the next page. Just note that you'll need to choose a Personal Data 86 - Server ( 87 - { 88 - <InlineLinkText 89 - label={_(msg`PDS`)} 90 - to="https://atproto.com/guides/self-hosting"> 91 - <Trans>PDS</Trans> 92 - </InlineLinkText> 93 - } 94 - ) that isn’t hosted by Bluesky. If you want to use a Bluesky-hosted 95 - PDS, you’ll need to sign up through bsky.app first, then return here 96 - to continue. 97 - </Trans> 98 - </Text> 99 - </View> 100 - <BackNextButtons 101 - isLoading={false} 102 - isNextDisabled={false} 103 - onBackPress={onBackPress} 104 - onNextPress={onNextPress} 105 - /> 106 - </ScreenTransition> 107 - ) 108 - }
+64 -9
src/screens/Signup/StepInfo/index.tsx
··· 8 8 import {DEFAULT_SERVICE} from '#/lib/constants' 9 9 import {isEmailMaybeInvalid} from '#/lib/strings/email' 10 10 import {logger} from '#/logger' 11 + import {isWeb} from '#/platform/detection' 11 12 import {ScreenTransition} from '#/screens/Login/ScreenTransition' 12 13 import {is13, is18, useSignupContext} from '#/screens/Signup/state' 13 14 import {Policies} from '#/screens/Signup/StepInfo/Policies' 14 15 import {atoms as a, native} from '#/alf' 16 + import {Button, ButtonText} from '#/components/Button' 17 + import {Divider} from '#/components/Divider' 15 18 import * as DateField from '#/components/forms/DateField' 16 19 import {type DateFieldRef} from '#/components/forms/DateField/types' 17 20 import {FormError} from '#/components/forms/FormError' ··· 20 23 import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope' 21 24 import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' 22 25 import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket' 26 + import {InlineLinkText} from '#/components/Link' 23 27 import {Loader} from '#/components/Loader' 28 + import {Text} from '#/components/Typography' 24 29 import {BackNextButtons} from '../BackNextButtons' 25 30 26 31 function sanitizeDate(date: Date): Date { ··· 35 40 36 41 export function StepInfo({ 37 42 onPressBack, 43 + onPressSignIn, 38 44 isServerError, 39 45 refetchServer, 40 46 isLoadingStarterPack, 41 47 }: { 42 48 onPressBack: () => void 49 + onPressSignIn: () => void 43 50 isServerError: boolean 44 51 refetchServer: () => void 45 52 isLoadingStarterPack: boolean ··· 79 86 return 80 87 } 81 88 82 - if (state.serviceUrl === 'https://example.com') { 83 - return dispatch({ 84 - type: 'setError', 85 - value: _(msg`Please choose a service host.`), 86 - }) 87 - } 88 - 89 89 if (state.serviceUrl === DEFAULT_SERVICE) { 90 90 return dispatch({ 91 91 type: 'setError', ··· 162 162 return ( 163 163 <ScreenTransition> 164 164 <View style={[a.gap_md]}> 165 + {state.serviceUrl === DEFAULT_SERVICE && ( 166 + <View style={[a.gap_xl]}> 167 + <Text style={[a.gap_md, a.leading_normal]}> 168 + <Trans> 169 + deer.social is part of the{' '} 170 + { 171 + <InlineLinkText 172 + label={_(msg`ATmosphere`)} 173 + to="https://atproto.com/"> 174 + <Trans>ATmosphere</Trans> 175 + </InlineLinkText> 176 + } 177 + —the network of apps, services, and accounts built on the AT 178 + Protocol. 179 + </Trans> 180 + </Text> 181 + <Text style={[a.gap_md, a.leading_normal]}> 182 + <Trans> 183 + If you have one, sign in with an existing Bluesky account. 184 + </Trans> 185 + </Text> 186 + <View style={isWeb && [a.flex_row, a.justify_center]}> 187 + <Button 188 + testID="signInButton" 189 + onPress={onPressSignIn} 190 + label={_(msg`Sign in with ATmosphere`)} 191 + accessibilityHint={_( 192 + msg`Opens flow to sign in to your existing ATmosphere account`, 193 + )} 194 + size="large" 195 + variant="solid" 196 + color="primary"> 197 + <ButtonText> 198 + <Trans>Sign in with ATmosphere</Trans> 199 + </ButtonText> 200 + </Button> 201 + </View> 202 + <Divider style={[a.mb_xl]} /> 203 + </View> 204 + )} 165 205 <FormError error={state.error} /> 166 206 <HostingProvider 167 207 serviceUrl={state.serviceUrl} 168 208 onSelectServiceUrl={v => dispatch({type: 'setServiceUrl', value: v})} 169 209 /> 210 + {state.serviceUrl === DEFAULT_SERVICE && ( 211 + <Text style={[a.gap_md, a.leading_normal, a.mt_md]}> 212 + <Trans> 213 + Don't have an account provider or an existing Bluesky account? To 214 + create a new account on a Bluesky-hosted PDS, sign up through{' '} 215 + { 216 + <InlineLinkText label={_(msg`bsky.app`)} to="https://bsky.app"> 217 + <Trans>bsky.app</Trans> 218 + </InlineLinkText> 219 + }{' '} 220 + first, then return to deer.social and log in with the account you 221 + created. 222 + </Trans> 223 + </Text> 224 + )} 170 225 {state.isLoading || isLoadingStarterPack ? ( 171 226 <View style={[a.align_center]}> 172 227 <Loader size="xl" /> 173 228 </View> 174 - ) : state.serviceDescription ? ( 229 + ) : state.serviceDescription && state.serviceUrl !== DEFAULT_SERVICE ? ( 175 230 <> 176 231 {state.serviceDescription.inviteCodeRequired && ( 177 232 <View> ··· 297 352 </View> 298 353 <BackNextButtons 299 354 hideNext={ 300 - !is13(state.dateOfBirth) || state.serviceUrl === 'https://example.com' 355 + !is13(state.dateOfBirth) || state.serviceUrl === DEFAULT_SERVICE 301 356 } 302 357 showRetry={isServerError} 303 358 isLoading={state.isLoading}
+6 -13
src/screens/Signup/index.tsx
··· 27 27 import {InlineLinkText} from '#/components/Link' 28 28 import {Text} from '#/components/Typography' 29 29 import * as bsky from '#/types/bsky' 30 - import {StepAtmosphere} from './StepAtmosphere' 31 30 32 31 export function Signup({ 33 32 onPressBack, ··· 113 112 <LoggedOutLayout 114 113 leadin="" 115 114 title={_(msg`Create Account`)} 116 - description={_(msg`We're so excited to have you join us!`)} 115 + description={_(msg`Welcome to the ATmosphere!`)} 117 116 scrollable> 118 117 <View testID="createAccount" style={a.flex_1}> 119 118 {showStarterPackCard && ··· 156 155 Step {state.activeStep + 1} of{' '} 157 156 {state.serviceDescription && 158 157 !state.serviceDescription.phoneVerificationRequired 159 - ? '3' 160 - : '4'} 158 + ? '2' 159 + : '3'} 161 160 </Trans> 162 161 </Text> 163 162 <Text style={[a.text_3xl, a.font_bold]}> 164 - {state.activeStep == SignupStep.ATMOSPHERE ? ( 163 + {state.activeStep === SignupStep.INFO ? ( 165 164 <Trans>The ATmosphere ✨</Trans> 166 - ) : state.activeStep === SignupStep.INFO ? ( 167 - <Trans>Your account</Trans> 168 165 ) : state.activeStep === SignupStep.HANDLE ? ( 169 166 <Trans>Choose your username</Trans> 170 167 ) : ( ··· 174 171 </View> 175 172 176 173 <LayoutAnimationConfig skipEntering skipExiting> 177 - {state.activeStep === SignupStep.ATMOSPHERE ? ( 178 - <StepAtmosphere 179 - onPressBack={onPressBack} 180 - onPressSignIn={onPressSignIn} 181 - /> 182 - ) : state.activeStep === SignupStep.INFO ? ( 174 + {state.activeStep === SignupStep.INFO ? ( 183 175 <StepInfo 184 176 onPressBack={onPressBack} 177 + onPressSignIn={onPressSignIn} 185 178 isLoadingStarterPack={ 186 179 isFetchingStarterPack && !isErrorStarterPack 187 180 }
+5 -5
src/screens/Signup/state.ts
··· 8 8 import {useLingui} from '@lingui/react' 9 9 import * as EmailValidator from 'email-validator' 10 10 11 + import {DEFAULT_SERVICE} from '#/lib/constants' 11 12 import {cleanError} from '#/lib/strings/errors' 12 13 import {createFullHandle} from '#/lib/strings/handles' 13 14 import {getAge} from '#/lib/strings/time' ··· 20 21 const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago 21 22 22 23 export enum SignupStep { 23 - ATMOSPHERE, 24 24 INFO, 25 25 HANDLE, 26 26 CAPTCHA, ··· 83 83 84 84 export const initialState: SignupState = { 85 85 hasPrev: false, 86 - activeStep: SignupStep.ATMOSPHERE, 86 + activeStep: SignupStep.INFO, 87 87 88 - serviceUrl: 'https://example.com', 88 + serviceUrl: DEFAULT_SERVICE, 89 89 serviceDescription: undefined, 90 90 userDomain: '', 91 91 dateOfBirth: DEFAULT_DATE, ··· 125 125 126 126 switch (a.type) { 127 127 case 'prev': { 128 - if (s.activeStep !== SignupStep.ATMOSPHERE) { 128 + if (s.activeStep !== SignupStep.INFO) { 129 129 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 130 130 next.activeStep-- 131 131 next.error = '' ··· 230 230 } 231 231 } 232 232 233 - next.hasPrev = next.activeStep !== SignupStep.ATMOSPHERE 233 + next.hasPrev = next.activeStep !== SignupStep.INFO 234 234 235 235 logger.debug('signup', next) 236 236