Use add-only photo permission when saving QR codes (APP-2374)

createAssetAsync fetches the created asset back after writing, which is
a photo library read - on a fresh iOS install this triggers the full
library permission prompt and rejects, showing a failure toast at the
same time. saveToLibraryAsync writes without reading, matching the
lightbox image download flow.

Also applies to the starter pack QR dialog, which additionally requested
the read-scoped media library permission from expo-image-picker.
This commit is contained in:
vineyardbovines
2026-06-10 15:55:13 -04:00
parent 3feea02548
commit 6d90b9b7a9
2 changed files with 12 additions and 6 deletions
+7 -4
View File
@@ -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',
@@ -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) {