+2
-1
src/components/InternationalPhoneCodeSelect.tsx
+2
-1
src/components/InternationalPhoneCodeSelect.tsx
···
1
1
import {Fragment, useMemo} from 'react'
2
+
import {Text as RNText} from 'react-native'
2
3
import {Image} from 'expo-image'
3
4
import {msg} from '@lingui/macro'
4
5
import {useLingui} from '@lingui/react'
···
113
114
/>
114
115
)
115
116
}
116
-
return unicodeFlag + ' '
117
+
return <RNText style={[{lineHeight: 21}]}>{unicodeFlag + ' '}</RNText>
117
118
}
+7
-3
src/components/contacts/FindContactsBannerNUX.tsx
+7
-3
src/components/contacts/FindContactsBannerNUX.tsx
···
6
6
import {useLingui} from '@lingui/react'
7
7
8
8
import {HITSLOP_10} from '#/lib/constants'
9
+
import {useGate} from '#/lib/statsig/statsig'
9
10
import {logger} from '#/logger'
10
11
import {isWeb} from '#/platform/detection'
11
12
import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
···
20
21
const t = useTheme()
21
22
const {_} = useLingui()
22
23
const {visible, close} = useInternalState()
23
-
const isFeatureEnabled = useIsFindContactsFeatureEnabledBasedOnGeolocation()
24
24
25
-
if (!visible || !isFeatureEnabled) return null
25
+
if (!visible) return null
26
26
27
27
return (
28
28
<View style={[a.w_full, a.p_lg, a.border_b, t.atoms.border_contrast_low]}>
···
88
88
const {nux} = useNux(Nux.FindContactsDismissibleBanner)
89
89
const {mutate: save, variables} = useSaveNux()
90
90
const hidden = !!variables
91
+
const isFeatureEnabled = useIsFindContactsFeatureEnabledBasedOnGeolocation()
92
+
const gate = useGate()
91
93
92
94
const visible = useMemo(() => {
93
95
if (isWeb) return false
94
96
if (hidden) return false
95
97
if (nux && nux.completed) return false
98
+
if (!isFeatureEnabled) return false
99
+
if (gate('disable_settings_find_contacts')) return false
96
100
return true
97
-
}, [hidden, nux])
101
+
}, [hidden, nux, isFeatureEnabled, gate])
98
102
99
103
const close = () => {
100
104
save({
+10
-8
src/components/contacts/country-allowlist.ts
+10
-8
src/components/contacts/country-allowlist.ts
···
18
18
'IT',
19
19
] satisfies CountryCode[] as string[]
20
20
21
-
export function isFindContactsFeatureEnabled(countryCode: string): boolean {
21
+
export function isFindContactsFeatureEnabled(countryCode?: string): boolean {
22
+
if (IS_DEV) return true
23
+
24
+
/*
25
+
* This should never happen unless geolocation fails entirely. In that
26
+
* case, let the user try, since it should work as long as they have a
27
+
* phone number from one of the allow-listed countries.
28
+
*/
29
+
if (!countryCode) return true
30
+
22
31
return FIND_CONTACTS_FEATURE_COUNTRY_ALLOWLIST.includes(
23
32
countryCode.toUpperCase(),
24
33
)
···
26
35
27
36
export function useIsFindContactsFeatureEnabledBasedOnGeolocation() {
28
37
const location = useGeolocation()
29
-
30
-
if (IS_DEV) return true
31
-
32
-
// they can try, by they'll need a phone number
33
-
// from one of the allowlisted countries
34
-
if (!location.countryCode) return true
35
-
36
38
return isFindContactsFeatureEnabled(location.countryCode)
37
39
}
+2
-4
src/components/contacts/screens/ViewMatches.tsx
+2
-4
src/components/contacts/screens/ViewMatches.tsx
···
104
104
match => !state.dismissedMatches.includes(match.profile.did),
105
105
)
106
106
107
-
console.log(matches)
108
-
109
107
const followableDids = matches.map(match => match.profile.did)
110
108
const [didFollowAll, setDidFollowAll] = useState(followableDids.length === 0)
111
109
···
449
447
const contactName = useMemo(() => {
450
448
if (!contact) return null
451
449
452
-
const name = contact.firstName ?? contact.lastName ?? contact.name
450
+
const name = contact.name ?? contact.firstName ?? contact.lastName
453
451
if (name) return _(msg`Your contact ${name}`)
454
452
const phone =
455
453
contact.phoneNumbers?.find(p => p.isPrimary) ?? contact.phoneNumbers?.[0]
···
520
518
const {_} = useLingui()
521
519
const {currentAccount} = useSession()
522
520
523
-
const name = contact.firstName ?? contact.lastName ?? contact.name
521
+
const name = contact.name ?? contact.firstName ?? contact.lastName
524
522
const phone =
525
523
contact.phoneNumbers?.find(phone => phone.isPrimary) ??
526
524
contact.phoneNumbers?.[0]
+19
-12
src/components/dialogs/nuxs/FindContactsAnnouncement.tsx
+19
-12
src/components/dialogs/nuxs/FindContactsAnnouncement.tsx
···
6
6
import {useLingui} from '@lingui/react'
7
7
8
8
import {logger} from '#/logger'
9
-
import {isWeb} from '#/platform/detection'
9
+
import {isNative, isWeb} from '#/platform/detection'
10
10
import {atoms as a, useTheme, web} from '#/alf'
11
11
import {Button, ButtonText} from '#/components/Button'
12
-
import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from '#/components/contacts/country-allowlist'
12
+
import {isFindContactsFeatureEnabled} from '#/components/contacts/country-allowlist'
13
13
import * as Dialog from '#/components/Dialog'
14
14
import {useNuxDialogContext} from '#/components/dialogs/nuxs'
15
+
import {
16
+
createIsEnabledCheck,
17
+
isExistingUserAsOf,
18
+
} from '#/components/dialogs/nuxs/utils'
15
19
import {Text} from '#/components/Typography'
20
+
import {IS_E2E} from '#/env'
16
21
import {navigate} from '#/Navigation'
17
22
18
-
export function FindContactsAnnouncement() {
19
-
const isFeatureEnabled = useIsFindContactsFeatureEnabledBasedOnGeolocation()
23
+
export const enabled = createIsEnabledCheck(props => {
24
+
return (
25
+
!IS_E2E &&
26
+
isNative &&
27
+
isExistingUserAsOf(
28
+
'2025-12-16T00:00:00.000Z',
29
+
props.currentProfile.createdAt,
30
+
) &&
31
+
isFindContactsFeatureEnabled(props.geolocation.countryCode)
32
+
)
33
+
})
20
34
21
-
if (!isFeatureEnabled) {
22
-
return null
23
-
}
24
-
25
-
return <Inner />
26
-
}
27
-
28
-
function Inner() {
35
+
export function FindContactsAnnouncement() {
29
36
const t = useTheme()
30
37
const {_} = useLingui()
31
38
const nuxDialogs = useNuxDialogContext()
+17
-21
src/components/dialogs/nuxs/index.tsx
+17
-21
src/components/dialogs/nuxs/index.tsx
···
10
10
11
11
import {useGate} from '#/lib/statsig/statsig'
12
12
import {logger} from '#/logger'
13
-
import {isNative} from '#/platform/detection'
14
13
import {STALE} from '#/state/queries'
15
14
import {Nux, useNuxs, useResetNuxs, useSaveNux} from '#/state/queries/nuxs'
16
15
import {
···
20
19
import {useProfileQuery} from '#/state/queries/profile'
21
20
import {type SessionAccount, useSession} from '#/state/session'
22
21
import {useOnboardingState} from '#/state/shell'
22
+
import {
23
+
enabled as isFindContactsAnnouncementEnabled,
24
+
FindContactsAnnouncement,
25
+
} from '#/components/dialogs/nuxs/FindContactsAnnouncement'
23
26
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
24
-
import {ENV} from '#/env'
25
-
/*
26
-
* NUXs
27
-
*/
28
-
import {FindContactsAnnouncement} from './FindContactsAnnouncement'
29
-
import {isExistingUserAsOf} from './utils'
27
+
import {type EnabledCheckProps} from '#/components/dialogs/nuxs/utils'
28
+
import {useGeolocation} from '#/geolocation'
30
29
31
30
type Context = {
32
31
activeNux: Nux | undefined
···
35
34
36
35
const queuedNuxs: {
37
36
id: Nux
38
-
enabled?: (props: {
39
-
gate: ReturnType<typeof useGate>
40
-
currentAccount: SessionAccount
41
-
currentProfile: AppBskyActorDefs.ProfileViewDetailed
42
-
preferences: UsePreferencesQueryResponse
43
-
}) => boolean
37
+
enabled?: (props: EnabledCheckProps) => boolean
44
38
}[] = [
45
39
{
46
40
id: Nux.FindContactsAnnouncement,
47
-
enabled: ({currentProfile}) => {
48
-
return (
49
-
isNative &&
50
-
ENV !== 'e2e' &&
51
-
isExistingUserAsOf('2025-12-16T00:00:00.000Z', currentProfile.createdAt)
52
-
)
53
-
},
41
+
enabled: isFindContactsAnnouncementEnabled,
54
42
},
55
43
]
56
44
···
101
89
preferences: UsePreferencesQueryResponse
102
90
}) {
103
91
const gate = useGate()
92
+
const geolocation = useGeolocation()
104
93
const {nuxs} = useNuxs()
105
94
const [snoozed, setSnoozed] = useState(() => {
106
95
return isSnoozed()
···
143
132
// then check gate (track exposure)
144
133
if (
145
134
enabled &&
146
-
!enabled({gate, currentAccount, currentProfile, preferences})
135
+
!enabled({
136
+
gate,
137
+
currentAccount,
138
+
currentProfile,
139
+
preferences,
140
+
geolocation,
141
+
})
147
142
) {
148
143
continue
149
144
}
···
178
173
currentAccount,
179
174
currentProfile,
180
175
preferences,
176
+
geolocation,
181
177
])
182
178
183
179
const ctx = useMemo(() => {
+21
src/components/dialogs/nuxs/utils.ts
+21
src/components/dialogs/nuxs/utils.ts
···
1
+
import {type AppBskyActorDefs} from '@atproto/api'
2
+
3
+
import {type useGate} from '#/lib/statsig/statsig'
4
+
import {type UsePreferencesQueryResponse} from '#/state/queries/preferences'
5
+
import {type SessionAccount} from '#/state/session'
6
+
import {type Geolocation} from '#/geolocation'
7
+
8
+
export type EnabledCheckProps = {
9
+
gate: ReturnType<typeof useGate>
10
+
currentAccount: SessionAccount
11
+
currentProfile: AppBskyActorDefs.ProfileViewDetailed
12
+
preferences: UsePreferencesQueryResponse
13
+
geolocation: Geolocation
14
+
}
15
+
16
+
export function createIsEnabledCheck(
17
+
cb: (props: EnabledCheckProps) => boolean,
18
+
) {
19
+
return cb
20
+
}
21
+
1
22
const ONE_DAY = 1000 * 60 * 60 * 24
2
23
3
24
export function isDaysOld(days: number, createdAt?: string) {
+1
src/lib/statsig/gates.ts
+1
src/lib/statsig/gates.ts
+1
-1
src/routes.ts
+1
-1
src/routes.ts
+14
-10
src/screens/Settings/Settings.tsx
+14
-10
src/screens/Settings/Settings.tsx
···
16
16
type CommonNavigatorParams,
17
17
type NavigationProp,
18
18
} from '#/lib/routes/types'
19
+
import {useGate} from '#/lib/statsig/statsig'
19
20
import {sanitizeDisplayName} from '#/lib/strings/display-names'
20
21
import {sanitizeHandle} from '#/lib/strings/handles'
21
22
import {isIOS, isNative} from '#/platform/detection'
···
93
94
const [showDevOptions, setShowDevOptions] = useState(false)
94
95
const findContactsEnabled =
95
96
useIsFindContactsFeatureEnabledBasedOnGeolocation()
97
+
const gate = useGate()
96
98
97
99
return (
98
100
<Layout.Screen>
···
211
213
<Trans>Content and media</Trans>
212
214
</SettingsList.ItemText>
213
215
</SettingsList.LinkItem>
214
-
{isNative && findContactsEnabled && (
215
-
<SettingsList.LinkItem
216
-
to="/settings/find-contacts"
217
-
label={_(msg`Find friends from contacts`)}>
218
-
<SettingsList.ItemIcon icon={ContactsIcon} />
219
-
<SettingsList.ItemText>
220
-
<Trans>Find friends from contacts</Trans>
221
-
</SettingsList.ItemText>
222
-
</SettingsList.LinkItem>
223
-
)}
216
+
{isNative &&
217
+
findContactsEnabled &&
218
+
!gate('disable_settings_find_contacts') && (
219
+
<SettingsList.LinkItem
220
+
to="/settings/find-contacts"
221
+
label={_(msg`Find friends from contacts`)}>
222
+
<SettingsList.ItemIcon icon={ContactsIcon} />
223
+
<SettingsList.ItemText>
224
+
<Trans>Find friends from contacts</Trans>
225
+
</SettingsList.ItemText>
226
+
</SettingsList.LinkItem>
227
+
)}
224
228
<SettingsList.LinkItem
225
229
to="/settings/appearance"
226
230
label={_(msg`Appearance`)}>
+44
src/view/screens/Storybook/Forms.tsx
+44
src/view/screens/Storybook/Forms.tsx
···
1
1
import React from 'react'
2
2
import {type TextInput, View} from 'react-native'
3
3
4
+
import {APP_LANGUAGES} from '#/lib/../locale/languages'
4
5
import {atoms as a} from '#/alf'
5
6
import {Button, ButtonText} from '#/components/Button'
6
7
import {DateField, LabelText} from '#/components/forms/DateField'
···
9
10
import * as Toggle from '#/components/forms/Toggle'
10
11
import * as ToggleButton from '#/components/forms/ToggleButton'
11
12
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
13
+
import {InternationalPhoneCodeSelect} from '#/components/InternationalPhoneCodeSelect'
14
+
import * as Select from '#/components/Select'
12
15
import {H1, H3} from '#/components/Typography'
13
16
14
17
export function Forms() {
···
22
25
23
26
const [value, setValue] = React.useState('')
24
27
const [date, setDate] = React.useState('2001-01-01')
28
+
const [countryCode, setCountryCode] = React.useState('US')
29
+
const [phoneNumber, setPhoneNumber] = React.useState('')
30
+
const [lang, setLang] = React.useState('en')
25
31
26
32
const inputRef = React.useRef<TextInput>(null)
27
33
28
34
return (
29
35
<View style={[a.gap_4xl, a.align_start]}>
30
36
<H1>Forms</H1>
37
+
38
+
<Select.Root value={lang} onValueChange={setLang}>
39
+
<Select.Trigger label="Select app language">
40
+
<Select.ValueText />
41
+
<Select.Icon />
42
+
</Select.Trigger>
43
+
<Select.Content
44
+
label="App language"
45
+
renderItem={({label, value}) => (
46
+
<Select.Item value={value} label={label}>
47
+
<Select.ItemIndicator />
48
+
<Select.ItemText>{label}</Select.ItemText>
49
+
</Select.Item>
50
+
)}
51
+
items={APP_LANGUAGES.map(l => ({
52
+
label: l.name,
53
+
value: l.code2,
54
+
}))}
55
+
/>
56
+
</Select.Root>
57
+
58
+
<View style={[a.flex_row, a.gap_sm, a.align_center]}>
59
+
<View>
60
+
<InternationalPhoneCodeSelect
61
+
// @ts-ignore
62
+
value={countryCode}
63
+
onChange={value => setCountryCode(value)}
64
+
/>
65
+
</View>
66
+
67
+
<View style={[a.flex_1]}>
68
+
<TextField.Input
69
+
label="Phone number"
70
+
value={phoneNumber}
71
+
onChangeText={setPhoneNumber}
72
+
/>
73
+
</View>
74
+
</View>
31
75
32
76
<View style={[a.gap_md, a.align_start, a.w_full]}>
33
77
<H3>InputText</H3>