diff --git a/src/components/InternationalPhoneCodeSelect.tsx b/src/components/InternationalPhoneCodeSelect.tsx
index 0cfd4d6b5c..57d362ff65 100644
--- a/src/components/InternationalPhoneCodeSelect.tsx
+++ b/src/components/InternationalPhoneCodeSelect.tsx
@@ -1,4 +1,5 @@
import {Fragment, useMemo} from 'react'
+import {Text as RNText} from 'react-native'
import {Image} from 'expo-image'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -113,5 +114,5 @@ function Flag({unicodeFlag, svgFlag}: {unicodeFlag: string; svgFlag: any}) {
/>
)
}
- return unicodeFlag + ' '
+ return {unicodeFlag + ' '}
}
diff --git a/src/components/contacts/FindContactsBannerNUX.tsx b/src/components/contacts/FindContactsBannerNUX.tsx
index d2b2d1d3dd..e08cf2edd1 100644
--- a/src/components/contacts/FindContactsBannerNUX.tsx
+++ b/src/components/contacts/FindContactsBannerNUX.tsx
@@ -6,6 +6,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {HITSLOP_10} from '#/lib/constants'
+import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
@@ -20,9 +21,8 @@ export function FindContactsBannerNUX() {
const t = useTheme()
const {_} = useLingui()
const {visible, close} = useInternalState()
- const isFeatureEnabled = useIsFindContactsFeatureEnabledBasedOnGeolocation()
- if (!visible || !isFeatureEnabled) return null
+ if (!visible) return null
return (
@@ -88,13 +88,17 @@ function useInternalState() {
const {nux} = useNux(Nux.FindContactsDismissibleBanner)
const {mutate: save, variables} = useSaveNux()
const hidden = !!variables
+ const isFeatureEnabled = useIsFindContactsFeatureEnabledBasedOnGeolocation()
+ const gate = useGate()
const visible = useMemo(() => {
if (isWeb) return false
if (hidden) return false
if (nux && nux.completed) return false
+ if (!isFeatureEnabled) return false
+ if (gate('disable_settings_find_contacts')) return false
return true
- }, [hidden, nux])
+ }, [hidden, nux, isFeatureEnabled, gate])
const close = () => {
save({
diff --git a/src/components/contacts/country-allowlist.ts b/src/components/contacts/country-allowlist.ts
index 97d3d44587..35ba43c8fa 100644
--- a/src/components/contacts/country-allowlist.ts
+++ b/src/components/contacts/country-allowlist.ts
@@ -18,7 +18,16 @@ const FIND_CONTACTS_FEATURE_COUNTRY_ALLOWLIST = [
'IT',
] satisfies CountryCode[] as string[]
-export function isFindContactsFeatureEnabled(countryCode: string): boolean {
+export function isFindContactsFeatureEnabled(countryCode?: string): boolean {
+ if (IS_DEV) return true
+
+ /*
+ * This should never happen unless geolocation fails entirely. In that
+ * case, let the user try, since it should work as long as they have a
+ * phone number from one of the allow-listed countries.
+ */
+ if (!countryCode) return true
+
return FIND_CONTACTS_FEATURE_COUNTRY_ALLOWLIST.includes(
countryCode.toUpperCase(),
)
@@ -26,12 +35,5 @@ export function isFindContactsFeatureEnabled(countryCode: string): boolean {
export function useIsFindContactsFeatureEnabledBasedOnGeolocation() {
const location = useGeolocation()
-
- if (IS_DEV) return true
-
- // they can try, by they'll need a phone number
- // from one of the allowlisted countries
- if (!location.countryCode) return true
-
return isFindContactsFeatureEnabled(location.countryCode)
}
diff --git a/src/components/contacts/screens/ViewMatches.tsx b/src/components/contacts/screens/ViewMatches.tsx
index 9b4ff9382c..d92cb88b1e 100644
--- a/src/components/contacts/screens/ViewMatches.tsx
+++ b/src/components/contacts/screens/ViewMatches.tsx
@@ -104,8 +104,6 @@ export function ViewMatches({
match => !state.dismissedMatches.includes(match.profile.did),
)
- console.log(matches)
-
const followableDids = matches.map(match => match.profile.did)
const [didFollowAll, setDidFollowAll] = useState(followableDids.length === 0)
@@ -449,7 +447,7 @@ function MatchItem({
const contactName = useMemo(() => {
if (!contact) return null
- const name = contact.firstName ?? contact.lastName ?? contact.name
+ const name = contact.name ?? contact.firstName ?? contact.lastName
if (name) return _(msg`Your contact ${name}`)
const phone =
contact.phoneNumbers?.find(p => p.isPrimary) ?? contact.phoneNumbers?.[0]
@@ -520,7 +518,7 @@ function ContactItem({
const {_} = useLingui()
const {currentAccount} = useSession()
- const name = contact.firstName ?? contact.lastName ?? contact.name
+ const name = contact.name ?? contact.firstName ?? contact.lastName
const phone =
contact.phoneNumbers?.find(phone => phone.isPrimary) ??
contact.phoneNumbers?.[0]
diff --git a/src/components/dialogs/nuxs/FindContactsAnnouncement.tsx b/src/components/dialogs/nuxs/FindContactsAnnouncement.tsx
index dd3aed013c..b92ece98a7 100644
--- a/src/components/dialogs/nuxs/FindContactsAnnouncement.tsx
+++ b/src/components/dialogs/nuxs/FindContactsAnnouncement.tsx
@@ -6,26 +6,33 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
-import {isWeb} from '#/platform/detection'
+import {isNative, isWeb} from '#/platform/detection'
import {atoms as a, useTheme, web} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
-import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from '#/components/contacts/country-allowlist'
+import {isFindContactsFeatureEnabled} from '#/components/contacts/country-allowlist'
import * as Dialog from '#/components/Dialog'
import {useNuxDialogContext} from '#/components/dialogs/nuxs'
+import {
+ createIsEnabledCheck,
+ isExistingUserAsOf,
+} from '#/components/dialogs/nuxs/utils'
import {Text} from '#/components/Typography'
+import {IS_E2E} from '#/env'
import {navigate} from '#/Navigation'
+export const enabled = createIsEnabledCheck(props => {
+ return (
+ !IS_E2E &&
+ isNative &&
+ isExistingUserAsOf(
+ '2025-12-16T00:00:00.000Z',
+ props.currentProfile.createdAt,
+ ) &&
+ isFindContactsFeatureEnabled(props.geolocation.countryCode)
+ )
+})
+
export function FindContactsAnnouncement() {
- const isFeatureEnabled = useIsFindContactsFeatureEnabledBasedOnGeolocation()
-
- if (!isFeatureEnabled) {
- return null
- }
-
- return
-}
-
-function Inner() {
const t = useTheme()
const {_} = useLingui()
const nuxDialogs = useNuxDialogContext()
diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx
index c0b1410e21..2ab319fafd 100644
--- a/src/components/dialogs/nuxs/index.tsx
+++ b/src/components/dialogs/nuxs/index.tsx
@@ -10,7 +10,6 @@ import {type AppBskyActorDefs} from '@atproto/api'
import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
-import {isNative} from '#/platform/detection'
import {STALE} from '#/state/queries'
import {Nux, useNuxs, useResetNuxs, useSaveNux} from '#/state/queries/nuxs'
import {
@@ -20,13 +19,13 @@ import {
import {useProfileQuery} from '#/state/queries/profile'
import {type SessionAccount, useSession} from '#/state/session'
import {useOnboardingState} from '#/state/shell'
+import {
+ enabled as isFindContactsAnnouncementEnabled,
+ FindContactsAnnouncement,
+} from '#/components/dialogs/nuxs/FindContactsAnnouncement'
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
-import {ENV} from '#/env'
-/*
- * NUXs
- */
-import {FindContactsAnnouncement} from './FindContactsAnnouncement'
-import {isExistingUserAsOf} from './utils'
+import {type EnabledCheckProps} from '#/components/dialogs/nuxs/utils'
+import {useGeolocation} from '#/geolocation'
type Context = {
activeNux: Nux | undefined
@@ -35,22 +34,11 @@ type Context = {
const queuedNuxs: {
id: Nux
- enabled?: (props: {
- gate: ReturnType
- currentAccount: SessionAccount
- currentProfile: AppBskyActorDefs.ProfileViewDetailed
- preferences: UsePreferencesQueryResponse
- }) => boolean
+ enabled?: (props: EnabledCheckProps) => boolean
}[] = [
{
id: Nux.FindContactsAnnouncement,
- enabled: ({currentProfile}) => {
- return (
- isNative &&
- ENV !== 'e2e' &&
- isExistingUserAsOf('2025-12-16T00:00:00.000Z', currentProfile.createdAt)
- )
- },
+ enabled: isFindContactsAnnouncementEnabled,
},
]
@@ -101,6 +89,7 @@ function Inner({
preferences: UsePreferencesQueryResponse
}) {
const gate = useGate()
+ const geolocation = useGeolocation()
const {nuxs} = useNuxs()
const [snoozed, setSnoozed] = useState(() => {
return isSnoozed()
@@ -143,7 +132,13 @@ function Inner({
// then check gate (track exposure)
if (
enabled &&
- !enabled({gate, currentAccount, currentProfile, preferences})
+ !enabled({
+ gate,
+ currentAccount,
+ currentProfile,
+ preferences,
+ geolocation,
+ })
) {
continue
}
@@ -178,6 +173,7 @@ function Inner({
currentAccount,
currentProfile,
preferences,
+ geolocation,
])
const ctx = useMemo(() => {
diff --git a/src/components/dialogs/nuxs/utils.ts b/src/components/dialogs/nuxs/utils.ts
index ba8f0169d6..68ea38ebf2 100644
--- a/src/components/dialogs/nuxs/utils.ts
+++ b/src/components/dialogs/nuxs/utils.ts
@@ -1,3 +1,24 @@
+import {type AppBskyActorDefs} from '@atproto/api'
+
+import {type useGate} from '#/lib/statsig/statsig'
+import {type UsePreferencesQueryResponse} from '#/state/queries/preferences'
+import {type SessionAccount} from '#/state/session'
+import {type Geolocation} from '#/geolocation'
+
+export type EnabledCheckProps = {
+ gate: ReturnType
+ currentAccount: SessionAccount
+ currentProfile: AppBskyActorDefs.ProfileViewDetailed
+ preferences: UsePreferencesQueryResponse
+ geolocation: Geolocation
+}
+
+export function createIsEnabledCheck(
+ cb: (props: EnabledCheckProps) => boolean,
+) {
+ return cb
+}
+
const ONE_DAY = 1000 * 60 * 60 * 24
export function isDaysOld(days: number, createdAt?: string) {
diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts
index 8c757a0160..ea67ac01bb 100644
--- a/src/lib/statsig/gates.ts
+++ b/src/lib/statsig/gates.ts
@@ -4,6 +4,7 @@ export type Gate =
| 'debug_show_feedcontext'
| 'debug_subscriptions'
| 'disable_onboarding_find_contacts'
+ | 'disable_settings_find_contacts'
| 'explore_show_suggested_feeds'
| 'feed_reply_button_open_thread'
| 'old_postonboarding'
diff --git a/src/routes.ts b/src/routes.ts
index 614b248722..f325539c71 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -7,7 +7,7 @@ type AllNavigatableRoutes = Omit<
>
export const router = new Router({
- Home: '/',
+ Home: ['/', '/download'],
Search: '/search',
Feeds: '/feeds',
Notifications: '/notifications',
diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx
index 6b0e184c03..2fa5aa7de7 100644
--- a/src/screens/Settings/Settings.tsx
+++ b/src/screens/Settings/Settings.tsx
@@ -16,6 +16,7 @@ import {
type CommonNavigatorParams,
type NavigationProp,
} from '#/lib/routes/types'
+import {useGate} from '#/lib/statsig/statsig'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {isIOS, isNative} from '#/platform/detection'
@@ -93,6 +94,7 @@ export function SettingsScreen({}: Props) {
const [showDevOptions, setShowDevOptions] = useState(false)
const findContactsEnabled =
useIsFindContactsFeatureEnabledBasedOnGeolocation()
+ const gate = useGate()
return (
@@ -211,16 +213,18 @@ export function SettingsScreen({}: Props) {
Content and media
- {isNative && findContactsEnabled && (
-
-
-
- Find friends from contacts
-
-
- )}
+ {isNative &&
+ findContactsEnabled &&
+ !gate('disable_settings_find_contacts') && (
+
+
+
+ Find friends from contacts
+
+
+ )}
diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx
index c3dad8decb..ee859a8275 100644
--- a/src/view/screens/Storybook/Forms.tsx
+++ b/src/view/screens/Storybook/Forms.tsx
@@ -1,6 +1,7 @@
import React from 'react'
import {type TextInput, View} from 'react-native'
+import {APP_LANGUAGES} from '#/lib/../locale/languages'
import {atoms as a} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {DateField, LabelText} from '#/components/forms/DateField'
@@ -9,6 +10,8 @@ import * as TextField from '#/components/forms/TextField'
import * as Toggle from '#/components/forms/Toggle'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
+import {InternationalPhoneCodeSelect} from '#/components/InternationalPhoneCodeSelect'
+import * as Select from '#/components/Select'
import {H1, H3} from '#/components/Typography'
export function Forms() {
@@ -22,6 +25,9 @@ export function Forms() {
const [value, setValue] = React.useState('')
const [date, setDate] = React.useState('2001-01-01')
+ const [countryCode, setCountryCode] = React.useState('US')
+ const [phoneNumber, setPhoneNumber] = React.useState('')
+ const [lang, setLang] = React.useState('en')
const inputRef = React.useRef(null)
@@ -29,6 +35,44 @@ export function Forms() {
Forms
+
+
+
+
+
+ (
+
+
+ {label}
+
+ )}
+ items={APP_LANGUAGES.map(l => ({
+ label: l.name,
+ value: l.code2,
+ }))}
+ />
+
+
+
+
+ setCountryCode(value)}
+ />
+
+
+
+
+
+
+
InputText