Invite friends: wire followers banner to find-friends settings, polish

- Add promo banner illustration (assets/images/invite_friends_promo_banner.webp)
  and replace the empty placeholder box in FollowersPromoBanner.
- Redirect the followers empty-state banner to the Find and Invite Friends
  settings screen instead of opening the share sheet, gated to mirror that
  screen's availability (native, geolocation allowlist, feature flag) so we
  don't promote contact import where the settings entry is hidden.
- Drop the now-dead invite dialog mount/control from ProfileFollowers and
  update the banner a11y copy to match the new destination.
- Scanner: collapse the unused error-message state to a boolean (the overlay
  renders its own localized copy).
This commit is contained in:
vineyardbovines
2026-06-01 14:21:06 -04:00
parent 7e6198edb2
commit ac83105139
4 changed files with 24 additions and 31 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

@@ -25,7 +25,7 @@ export function InviteScannerScreen() {
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const [permission, requestPermission] = useCameraPermissions() const [permission, requestPermission] = useCameraPermissions()
const [error, setError] = useState<string | null>(null) const [showError, setShowError] = useState(false)
const [scannerEnabled, setScannerEnabled] = useState(true) const [scannerEnabled, setScannerEnabled] = useState(true)
const onClose = useCallback(() => { const onClose = useCallback(() => {
@@ -44,13 +44,7 @@ export function InviteScannerScreen() {
) )
if (!profileMatch) { if (!profileMatch) {
setScannerEnabled(false) setScannerEnabled(false)
setError( setShowError(true)
l({
message: 'Profile not found',
comment:
'Error shown when a scanned QR code is not a valid Bluesky profile URL',
}),
)
return return
} }
@@ -58,11 +52,11 @@ export function InviteScannerScreen() {
const handle = profileMatch[1] const handle = profileMatch[1]
navigation.replace('Profile', {name: handle}) navigation.replace('Profile', {name: handle})
}, },
[scannerEnabled, navigation, l], [scannerEnabled, navigation],
) )
const onRetry = useCallback(() => { const onRetry = useCallback(() => {
setError(null) setShowError(false)
setScannerEnabled(true) setScannerEnabled(true)
}, []) }, [])
@@ -165,7 +159,7 @@ export function InviteScannerScreen() {
</View> </View>
{/* Error overlay shown when scanned QR isn't a valid profile URL */} {/* Error overlay shown when scanned QR isn't a valid profile URL */}
{error && ( {showError && (
<View <View
style={{ style={{
position: 'absolute', position: 'absolute',
@@ -1,4 +1,5 @@
import {Pressable, View} from 'react-native' import {Pressable, View} from 'react-native'
import {Image} from 'expo-image'
import {useLingui} from '@lingui/react/macro' import {useLingui} from '@lingui/react/macro'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
@@ -18,8 +19,8 @@ export function FollowersPromoBanner({
<View style={[a.px_lg, a.pt_md]}> <View style={[a.px_lg, a.pt_md]}>
<Pressable <Pressable
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={l`Invite friends`} accessibilityLabel={l`Find and invite friends`}
accessibilityHint={l`Opens the invite friends sheet to share your profile`} accessibilityHint={l`Opens the find and invite friends settings`}
onPress={onPress} onPress={onPress}
style={({pressed}) => [ style={({pressed}) => [
a.flex_row, a.flex_row,
@@ -34,14 +35,11 @@ export function FollowersPromoBanner({
opacity: pressed ? 0.85 : 1, opacity: pressed ? 0.85 : 1,
}, },
]}> ]}>
<View <Image
style={{ accessibilityIgnoresInvertColors
width: 126, source={require('../../../../assets/images/invite_friends_promo_banner.webp')}
height: 64, style={{width: 126, height: 64, marginRight: 16}}
marginRight: 16, contentFit="contain"
alignItems: 'center',
justifyContent: 'center',
}}
/> />
<Text <Text
style={[ style={[
+11 -10
View File
@@ -5,19 +5,19 @@ import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {type NavigationProp} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useProfileFollowersQuery} from '#/state/queries/profile-followers' import {useProfileFollowersQuery} from '#/state/queries/profile-followers'
import {useResolveDidQuery} from '#/state/queries/resolve-uri' import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useDialogControl} from '#/components/Dialog' import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from '#/components/contacts/country-allowlist'
import {PeopleRemove2_Stroke1_Corner0_Rounded as PeopleRemoveIcon} from '#/components/icons/PeopleRemove2' import {PeopleRemove2_Stroke1_Corner0_Rounded as PeopleRemoveIcon} from '#/components/icons/PeopleRemove2'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env' import {IS_NATIVE} from '#/env'
import { import {
FollowersPromoBanner, FollowersPromoBanner,
InviteFriendsDialog,
useFollowersPromoDismissed, useFollowersPromoDismissed,
} from '#/features/inviteFriends' } from '#/features/inviteFriends'
import {List} from '../util/List' import {List} from '../util/List'
@@ -50,7 +50,7 @@ function keyExtractor(item: ActorDefs.ProfileViewBasic) {
export function ProfileFollowers({name}: {name: string}) { export function ProfileFollowers({name}: {name: string}) {
const {_} = useLingui() const {_} = useLingui()
const ax = useAnalytics() const ax = useAnalytics()
const navigation = useNavigation() const navigation = useNavigation<NavigationProp>()
const initialNumToRender = useInitialNumToRender() const initialNumToRender = useInitialNumToRender()
const {currentAccount} = useSession() const {currentAccount} = useSession()
@@ -165,14 +165,18 @@ export function ProfileFollowers({name}: {name: string}) {
[ax, followers, resolvedDid], [ax, followers, resolvedDid],
) )
const inviteFriendsControl = useDialogControl()
const [followersPromoDismissed, setFollowersPromoDismissed] = const [followersPromoDismissed, setFollowersPromoDismissed] =
useFollowersPromoDismissed() useFollowersPromoDismissed()
// Native-only: the invite-friends sheet is a no-op on web, so the banner const findContactsEnabled = useIsFindContactsFeatureEnabledBasedOnGeolocation()
// would open nothing. Gate it to avoid a dead promo on web. // The banner deep-links into the Find and Invite Friends settings screen, so
// mirror that screen's availability gates: native-only, allowed in the user's
// region (geolocation allowlist), and not disabled by the feature flag. This
// avoids promoting contact import where the settings entry itself is hidden.
const showFollowersPromo = const showFollowersPromo =
IS_NATIVE && IS_NATIVE &&
isMe && isMe &&
findContactsEnabled &&
!ax.features.enabled(ax.features.ImportContactsSettingsDisable) &&
!followersPromoDismissed && !followersPromoDismissed &&
followers.length < 1 && followers.length < 1 &&
!isDidLoading && !isDidLoading &&
@@ -184,7 +188,7 @@ export function ProfileFollowers({name}: {name: string}) {
<> <>
{showFollowersPromo && ( {showFollowersPromo && (
<FollowersPromoBanner <FollowersPromoBanner
onPress={() => inviteFriendsControl.open()} onPress={() => navigation.navigate('FindContactsSettings')}
onDismiss={() => setFollowersPromoDismissed(true)} onDismiss={() => setFollowersPromoDismissed(true)}
/> />
)} )}
@@ -210,9 +214,6 @@ export function ProfileFollowers({name}: {name: string}) {
onPress: () => navigation.goBack(), onPress: () => navigation.goBack(),
}} }}
/> />
{showFollowersPromo && (
<InviteFriendsDialog control={inviteFriendsControl} />
)}
</> </>
) )
} }