From da87515d0ae5e22827243e18c3e2adc2d9ccb9fc Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 15 Dec 2025 13:35:28 -0600 Subject: [PATCH] [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 --- .../InternationalPhoneCodeSelect.tsx | 3 +- .../contacts/FindContactsBannerNUX.tsx | 10 +++-- src/components/contacts/country-allowlist.ts | 18 ++++---- .../contacts/screens/ViewMatches.tsx | 6 +-- .../dialogs/nuxs/FindContactsAnnouncement.tsx | 31 ++++++++----- src/components/dialogs/nuxs/index.tsx | 38 +++++++--------- src/components/dialogs/nuxs/utils.ts | 21 +++++++++ src/lib/statsig/gates.ts | 1 + src/routes.ts | 2 +- src/screens/Settings/Settings.tsx | 24 +++++----- src/view/screens/Storybook/Forms.tsx | 44 +++++++++++++++++++ 11 files changed, 138 insertions(+), 60 deletions(-) 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