Skip to content

Commit 5b9b5ed

Browse files
refactor(signup): community picker as account-switcher-style list with icons
Replace the plain Select dropdown with an AccountList-style list: a rounded, bordered container with one row per community (logo + display name + PDS host) and a check on the selected one. Blacksky leads (bundled Logo); other communities show their brand logo, falling back to a theme-colored initial tile. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent eede9d1 commit 5b9b5ed

1 file changed

Lines changed: 148 additions & 32 deletions

File tree

Lines changed: 148 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,43 @@
11
import {useMemo} from 'react'
2-
import {View} from 'react-native'
2+
import {Image, View} from 'react-native'
33
import {msg} from '@lingui/core/macro'
44
import {useLingui} from '@lingui/react'
55
import {Trans} from '@lingui/react/macro'
66
import {useQuery} from '@tanstack/react-query'
77

88
import {DEFAULT_BRAND_CONFIG} from '#/lib/community/BrandContext'
99
import {fetchBrandList} from '#/lib/community/resolveBrand'
10+
import {Logo} from '#/view/icons/Logo'
1011
import {useSignupContext} from '#/screens/Signup/state'
11-
import {atoms as a} from '#/alf'
12+
import {atoms as a, useTheme} from '#/alf'
13+
import {Button} from '#/components/Button'
1214
import * as TextField from '#/components/forms/TextField'
13-
import * as Select from '#/components/Select'
15+
import {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
1416
import {Text} from '#/components/Typography'
1517
import {useAnalytics} from '#/analytics'
1618
import {BackNextButtons} from '../BackNextButtons'
1719

18-
type CommunityOption = {slug: string; displayName: string; pds: string}
20+
type CommunityOption = {
21+
slug: string
22+
displayName: string
23+
pds: string
24+
logo: string
25+
themeColor: string
26+
isDefault: boolean
27+
}
28+
29+
const ICON_SIZE = 48
1930

2031
/**
21-
* First signup step: choose the community to create the account in. Blacksky is
22-
* the default and always the first option (its config is bundled into the app,
23-
* not served by the brand service). Other published communities are listed below
24-
* it in the dropdown, sourced from the brand service. Selecting one points signup
25-
* at that community's PDS and stamps its slug so the client can resolve the brand
26-
* deterministically later.
32+
* First signup step: choose the community to create the account in, shown as an
33+
* account-switcher-style list (icon + name per row). Blacksky is the default and
34+
* always the first option (its config is bundled into the app, not served by the
35+
* brand service); other published communities follow, sourced from the brand
36+
* service. Selecting one points signup at that community's PDS and stamps its
37+
* slug so the client can resolve the brand deterministically later.
2738
*/
2839
export function StepCommunity({onPressBack}: {onPressBack: () => void}) {
40+
const t = useTheme()
2941
const {_} = useLingui()
3042
const ax = useAnalytics()
3143
const {state, dispatch} = useSignupContext()
@@ -36,32 +48,31 @@ export function StepCommunity({onPressBack}: {onPressBack: () => void}) {
3648
staleTime: 5 * 60 * 1000,
3749
})
3850

39-
// Blacksky (the default) always leads; dedupe it out of the served list.
4051
const options = useMemo<CommunityOption[]>(() => {
4152
const blacksky: CommunityOption = {
4253
slug: DEFAULT_BRAND_CONFIG.metadata.slug,
4354
displayName: DEFAULT_BRAND_CONFIG.metadata.displayName,
4455
pds: DEFAULT_BRAND_CONFIG.services.pds.url,
56+
logo: DEFAULT_BRAND_CONFIG.assets.logo,
57+
themeColor: DEFAULT_BRAND_CONFIG.web.themeColor,
58+
isDefault: true,
4559
}
4660
const others = (brands ?? [])
4761
.filter(b => b.slug !== blacksky.slug)
4862
.map(b => ({
4963
slug: b.slug,
5064
displayName: b.displayName || b.name,
5165
pds: b.pds,
66+
logo: b.logo,
67+
themeColor: b.themeColor,
68+
isDefault: false,
5269
}))
5370
return [blacksky, ...others]
5471
}, [brands])
5572

5673
const selectedSlug =
5774
state.selectedBrandSlug ?? DEFAULT_BRAND_CONFIG.metadata.slug
5875

59-
const onValueChange = (slug: string) => {
60-
const option = options.find(o => o.slug === slug)
61-
if (!option) return
62-
dispatch({type: 'setCommunity', slug: option.slug, serviceUrl: option.pds})
63-
}
64-
6576
const onNextPress = () => {
6677
dispatch({type: 'next'})
6778
ax.metric('signup:nextPressed', {activeStep: state.activeStep})
@@ -80,21 +91,33 @@ export function StepCommunity({onPressBack}: {onPressBack: () => void}) {
8091
<TextField.LabelText>
8192
<Trans>Community</Trans>
8293
</TextField.LabelText>
83-
<Select.Root value={selectedSlug} onValueChange={onValueChange}>
84-
<Select.Trigger label={_(msg`Select your community`)}>
85-
<Select.ValueText placeholder={_(msg`Select your community`)} />
86-
<Select.Icon />
87-
</Select.Trigger>
88-
<Select.Content
89-
items={options.map(o => ({label: o.displayName, value: o.slug}))}
90-
renderItem={({label, value}) => (
91-
<Select.Item value={value} label={label}>
92-
<Select.ItemIndicator />
93-
<Select.ItemText>{label}</Select.ItemText>
94-
</Select.Item>
95-
)}
96-
/>
97-
</Select.Root>
94+
<View
95+
style={[
96+
a.rounded_lg,
97+
a.overflow_hidden,
98+
a.border,
99+
t.atoms.border_contrast_low,
100+
]}>
101+
{options.map((option, i) => (
102+
<View key={option.slug}>
103+
{i > 0 && (
104+
<View style={[a.border_b, t.atoms.border_contrast_low]} />
105+
)}
106+
<CommunityItem
107+
option={option}
108+
selected={option.slug === selectedSlug}
109+
onSelect={() =>
110+
dispatch({
111+
type: 'setCommunity',
112+
slug: option.slug,
113+
serviceUrl: option.pds,
114+
})
115+
}
116+
label={_(msg`Create your account in ${option.displayName}`)}
117+
/>
118+
</View>
119+
))}
120+
</View>
98121
</View>
99122
</View>
100123
<BackNextButtons
@@ -105,3 +128,96 @@ export function StepCommunity({onPressBack}: {onPressBack: () => void}) {
105128
</>
106129
)
107130
}
131+
132+
function CommunityItem({
133+
option,
134+
selected,
135+
onSelect,
136+
label,
137+
}: {
138+
option: CommunityOption
139+
selected: boolean
140+
onSelect: () => void
141+
label: string
142+
}) {
143+
const t = useTheme()
144+
return (
145+
<Button label={label} onPress={onSelect} style={[a.w_full]}>
146+
{({hovered, pressed}) => (
147+
<View
148+
style={[
149+
a.flex_1,
150+
a.flex_row,
151+
a.align_center,
152+
a.p_lg,
153+
a.gap_sm,
154+
(hovered || pressed) && t.atoms.bg_contrast_25,
155+
]}>
156+
<View
157+
style={[
158+
{width: ICON_SIZE, height: ICON_SIZE},
159+
a.rounded_sm,
160+
a.overflow_hidden,
161+
a.justify_center,
162+
a.align_center,
163+
t.atoms.bg_contrast_25,
164+
]}>
165+
{option.isDefault ? (
166+
<Logo width={ICON_SIZE * 0.7} />
167+
) : option.logo ? (
168+
<Image
169+
accessibilityIgnoresInvertColors
170+
source={{uri: option.logo}}
171+
style={{width: ICON_SIZE, height: ICON_SIZE}}
172+
resizeMode="cover"
173+
/>
174+
) : (
175+
<View
176+
style={[
177+
a.flex_1,
178+
a.w_full,
179+
a.justify_center,
180+
a.align_center,
181+
{backgroundColor: option.themeColor},
182+
]}>
183+
<Text style={[a.text_lg, a.font_bold, {color: 'white'}]}>
184+
{option.displayName.slice(0, 1).toUpperCase()}
185+
</Text>
186+
</View>
187+
)}
188+
</View>
189+
190+
<View style={[a.flex_1, a.gap_2xs, a.pr_2xl]}>
191+
<Text
192+
emoji
193+
style={[a.font_medium, a.leading_tight, a.text_md]}
194+
numberOfLines={1}>
195+
{option.displayName}
196+
</Text>
197+
<Text
198+
style={[a.leading_tight, t.atoms.text_contrast_medium, a.text_sm]}
199+
numberOfLines={1}>
200+
{option.pds.replace(/^https?:\/\//, '')}
201+
</Text>
202+
</View>
203+
204+
{selected && (
205+
<View
206+
style={[
207+
{
208+
width: 20,
209+
height: 20,
210+
backgroundColor: t.palette.positive_500,
211+
},
212+
a.rounded_full,
213+
a.justify_center,
214+
a.align_center,
215+
]}>
216+
<CheckIcon size="xs" style={[{color: t.palette.white}]} />
217+
</View>
218+
)}
219+
</View>
220+
)}
221+
</Button>
222+
)
223+
}

0 commit comments

Comments
 (0)