import {Suspense, useRef, useState} from 'react' import {Pressable, View} from 'react-native' import type ViewShot from 'react-native-view-shot' import {setStringAsync} from 'expo-clipboard' import {requestPermissionsAsync, saveToLibraryAsync} from 'expo-media-library' import {useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {type NavigationProp} from '#/lib/routes/types' import {shareUrl as nativeShareUrl} from '#/lib/sharing' import {logger} from '#/logger' import {useProfileQuery} from '#/state/queries/profile' import {useSession} from '#/state/session' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {Divider} from '#/components/Divider' import {Error as ErrorMessage} from '#/components/Error' import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_NATIVE} from '#/env' import {ActionButtons} from './components/ActionButtons' import {ThemedQrCard} from './components/ThemedQrCard' import {ThemePicker} from './components/ThemePicker' import {getInviteTheme, type InviteThemeKey} from './themes' import {getInviteDisplayUrl, getInviteShareUrl} from './urls' export function InviteFriendsDialogInner({ control, }: { control: Dialog.DialogControlProps }) { const {t: l} = useLingui() const t = useTheme() const ax = useAnalytics() const navigation = useNavigation() const {currentAccount} = useSession() const profileQuery = useProfileQuery({did: currentAccount?.did}) const [themeKey, setThemeKey] = useState('day') const captureRef = useRef(null) const theme = getInviteTheme(themeKey) const variant = t.name === 'light' ? theme.light : theme.dark const handle = profileQuery.data?.handle const avatarUri = profileQuery.data?.avatar // 'handle.invalid' is the atproto sentinel for an unresolvable DID - treat // it like a load error so we don't display a broken share URL. const isHandleValid = !!handle && handle !== 'handle.invalid' const canonicalShareUrl = isHandleValid ? getInviteShareUrl(handle) : '' const displayUrl = isHandleValid ? getInviteDisplayUrl(handle) : '' const onShare = async () => { if (!canonicalShareUrl) { Toast.show(l`Could not share – please try again`, {type: 'error'}) return } ax.metric('invite:action:share', {}) try { await nativeShareUrl(canonicalShareUrl) } catch (err) { logger.error('InviteFriendsDialog: share failed', {safeMessage: err}) } } const onDownload = async () => { if (!IS_NATIVE) return if (!canonicalShareUrl) { Toast.show(l`Could not download – please try again`, {type: 'error'}) return } const uri = await captureRef.current?.capture?.() if (!uri) { Toast.show(l`Could not capture QR code`, {type: 'error'}) return } // Write-only permission - saving the QR image does not require read access // to the user's photo library. const permission = await requestPermissionsAsync(true) if (!permission.granted) { Toast.show( l`You must grant access to your photo library to save a QR code`, {type: 'error'}, ) return } try { // saveToLibraryAsync writes without reading the asset back, so it works // with the add-only permission. createAssetAsync fetches the created // asset, which triggers the full library read prompt on iOS (APP-2374). await saveToLibraryAsync(`file://${uri}`) ax.metric('invite:action:download', {}) Toast.show(l`QR code saved to your camera roll!`) } catch (err) { logger.error('InviteFriendsDialog: download failed', {safeMessage: err}) Toast.show(l`An error occurred while saving the QR code!`, { type: 'error', }) } } const onScan = () => { ax.metric('invite:action:scan', {}) // Close dialog first, then navigate (control.close callback per CLAUDE.md // Dialog footgun rule — prevents race with the navigation push). control.close(() => { navigation.navigate('InviteScanner') }) } const onCopy = async () => { if (!canonicalShareUrl) { Toast.show(l`Could not copy – please try again`, {type: 'error'}) return } try { await setStringAsync(canonicalShareUrl) ax.metric('invite:action:copy', {}) Toast.show(l`Invite link copied`) } catch (err) { logger.error('InviteFriendsDialog: copy failed', {safeMessage: err}) Toast.show(l`Failed to copy link`, {type: 'error'}) } } const onThemeChange = (next: InviteThemeKey) => { ax.metric('invite:theme:change', {themeKey: next}) setThemeKey(next) } return ( ( )}> {l`Invite Friends`} }> {profileQuery.isLoading ? ( ) : profileQuery.error || !isHandleValid ? ( profileQuery.refetch()} hideBackButton /> ) : ( }> )} void onShare()} onDownload={() => void onDownload()} onScan={onScan} /> {l({ message: 'Invite link', context: 'profile invite', comment: 'Link that invites someone to follow your profile, distinct from a group chat invite link', })} {displayUrl || ' '} void onCopy()} hitSlop={8} style={({pressed}) => [{opacity: pressed ? 0.7 : 1}]}> {l`Copy`} ) }