11import { useMemo } from 'react'
2- import { View } from 'react-native'
2+ import { Image , View } from 'react-native'
33import { msg } from '@lingui/core/macro'
44import { useLingui } from '@lingui/react'
55import { Trans } from '@lingui/react/macro'
66import { useQuery } from '@tanstack/react-query'
77
88import { DEFAULT_BRAND_CONFIG } from '#/lib/community/BrandContext'
99import { fetchBrandList } from '#/lib/community/resolveBrand'
10+ import { Logo } from '#/view/icons/Logo'
1011import { 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'
1214import * 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 '
1416import { Text } from '#/components/Typography'
1517import { useAnalytics } from '#/analytics'
1618import { 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 */
2839export 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 ( / ^ h t t p s ? : \/ \/ / , '' ) }
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