[APP-1684] Some contact import tweaks (#9555)

* Handle download link

* Improve NUX geo gating from #9549

* Fix alignment of phone code select

* Show full name

* Add gate to nux banner

* Add gate to settings screen

* Invert gate check in settings, whoops
This commit is contained in:
Eric Bailey
2025-12-15 13:35:28 -06:00
committed by GitHub
parent 348e7fa379
commit da87515d0a
11 changed files with 138 additions and 60 deletions
@@ -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 <RNText style={[{lineHeight: 21}]}>{unicodeFlag + ' '}</RNText>
}
@@ -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 (
<View style={[a.w_full, a.p_lg, a.border_b, t.atoms.border_contrast_low]}>
@@ -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({
+10 -8
View File
@@ -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)
}
@@ -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]
@@ -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 <Inner />
}
function Inner() {
const t = useTheme()
const {_} = useLingui()
const nuxDialogs = useNuxDialogContext()
+17 -21
View File
@@ -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<typeof useGate>
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(() => {
+21
View File
@@ -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<typeof useGate>
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) {
+1
View File
@@ -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'
+1 -1
View File
@@ -7,7 +7,7 @@ type AllNavigatableRoutes = Omit<
>
export const router = new Router<AllNavigatableRoutes>({
Home: '/',
Home: ['/', '/download'],
Search: '/search',
Feeds: '/feeds',
Notifications: '/notifications',
+14 -10
View File
@@ -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 (
<Layout.Screen>
@@ -211,16 +213,18 @@ export function SettingsScreen({}: Props) {
<Trans>Content and media</Trans>
</SettingsList.ItemText>
</SettingsList.LinkItem>
{isNative && findContactsEnabled && (
<SettingsList.LinkItem
to="/settings/find-contacts"
label={_(msg`Find friends from contacts`)}>
<SettingsList.ItemIcon icon={ContactsIcon} />
<SettingsList.ItemText>
<Trans>Find friends from contacts</Trans>
</SettingsList.ItemText>
</SettingsList.LinkItem>
)}
{isNative &&
findContactsEnabled &&
!gate('disable_settings_find_contacts') && (
<SettingsList.LinkItem
to="/settings/find-contacts"
label={_(msg`Find friends from contacts`)}>
<SettingsList.ItemIcon icon={ContactsIcon} />
<SettingsList.ItemText>
<Trans>Find friends from contacts</Trans>
</SettingsList.ItemText>
</SettingsList.LinkItem>
)}
<SettingsList.LinkItem
to="/settings/appearance"
label={_(msg`Appearance`)}>
+44
View File
@@ -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<TextInput>(null)
@@ -29,6 +35,44 @@ export function Forms() {
<View style={[a.gap_4xl, a.align_start]}>
<H1>Forms</H1>
<Select.Root value={lang} onValueChange={setLang}>
<Select.Trigger label="Select app language">
<Select.ValueText />
<Select.Icon />
</Select.Trigger>
<Select.Content
label="App language"
renderItem={({label, value}) => (
<Select.Item value={value} label={label}>
<Select.ItemIndicator />
<Select.ItemText>{label}</Select.ItemText>
</Select.Item>
)}
items={APP_LANGUAGES.map(l => ({
label: l.name,
value: l.code2,
}))}
/>
</Select.Root>
<View style={[a.flex_row, a.gap_sm, a.align_center]}>
<View>
<InternationalPhoneCodeSelect
// @ts-ignore
value={countryCode}
onChange={value => setCountryCode(value)}
/>
</View>
<View style={[a.flex_1]}>
<TextField.Input
label="Phone number"
value={phoneNumber}
onChangeText={setPhoneNumber}
/>
</View>
</View>
<View style={[a.gap_md, a.align_start, a.w_full]}>
<H3>InputText</H3>