From b667cafca174d00d02fe7cffbdb3e0e1f029f123 Mon Sep 17 00:00:00 2001 From: Spence Pope Date: Wed, 10 Jun 2026 16:39:40 -0400 Subject: [PATCH] Use add-only photo permission when saving QR codes (#10854) --- src/components/StarterPack/QrCodeDialog.tsx | 11 +++++++---- .../inviteFriends/InviteFriendsDialogInner.tsx | 7 +++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/components/StarterPack/QrCodeDialog.tsx b/src/components/StarterPack/QrCodeDialog.tsx index 4452c036a5..e239eee9a4 100644 --- a/src/components/StarterPack/QrCodeDialog.tsx +++ b/src/components/StarterPack/QrCodeDialog.tsx @@ -1,8 +1,7 @@ import {Suspense, useRef, useState} from 'react' import {View} from 'react-native' import type ViewShot from 'react-native-view-shot' -import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker' -import {createAssetAsync} from 'expo-media-library' +import {requestPermissionsAsync, saveToLibraryAsync} from 'expo-media-library' import * as Sharing from 'expo-sharing' import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' import {msg} from '@lingui/core/macro' @@ -60,7 +59,9 @@ export function QrCodeDialog({ const onSavePress = async () => { ref.current?.capture?.().then(async (uri: string) => { if (IS_NATIVE) { - const res = await requestMediaLibraryPermissionsAsync() + // Write-only permission - saving the QR image does not require read + // access to the user's photo library. + const res = await requestPermissionsAsync(true) if (!res.granted) { Toast.show( @@ -73,7 +74,9 @@ export function QrCodeDialog({ // Incase of a FS failure, don't crash the app try { - await createAssetAsync(`file://${uri}`) + // saveToLibraryAsync writes without reading the asset back, so it + // works with the add-only permission on iOS (APP-2374) + await saveToLibraryAsync(`file://${uri}`) } catch (e: unknown) { Toast.show(_(msg`An error occurred while saving the QR code!`), { type: 'error', diff --git a/src/features/inviteFriends/InviteFriendsDialogInner.tsx b/src/features/inviteFriends/InviteFriendsDialogInner.tsx index 7d2641a775..16377c49f3 100644 --- a/src/features/inviteFriends/InviteFriendsDialogInner.tsx +++ b/src/features/inviteFriends/InviteFriendsDialogInner.tsx @@ -2,7 +2,7 @@ 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 {createAssetAsync, requestPermissionsAsync} from 'expo-media-library' +import {requestPermissionsAsync, saveToLibraryAsync} from 'expo-media-library' import {useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' @@ -92,7 +92,10 @@ export function InviteFriendsDialogInner({ } try { - await createAssetAsync(`file://${uri}`) + // 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) {