Recipe: limited photo access + upgrade
Problem. On iOS 14+, users can grant "Selected Photos" (partial) access to the photo library. The feature works, but you want to let users upgrade to full access without forcing them through Settings.
Solution. Detect the limited state, render a branded upgrade prompt, and call requestFullAccess() on the hook result. The engine opens the native Limited Photo Picker (on iOS 15+) or re-presents the upgrade dialog.
What you'll use
usePermissionHandler—isLimitedandrequestFullAccessPermissionGate—renderLimitedpropLimitedUpgradePrompt— default upgrade modal
Code
import React from "react";
import { Text, View } from "react-native";
import {
LimitedUpgradePrompt,
PermissionGate,
usePermissionHandler,
} from "react-native-permission-handler";
import { Permissions } from "react-native-permission-handler/rnp";
export function ProfilePhotoPicker() {
return (
<PermissionGate
permission={Permissions.PHOTO_LIBRARY}
prePrompt={{
title: "Photo access",
message: "We need access to your photos so you can pick a profile picture.",
}}
blockedPrompt={{
title: "Photos blocked",
message: "Enable photo access for this app in Settings.",
}}
renderLimited={(handler) => (
<View>
<PhotoPicker />
<LimitedUpgradePrompt
visible
title="Allow full access?"
message="You gave access to a few photos. Allow full access to choose from your whole library."
upgradeLabel="Allow Full Access"
dismissLabel="Keep current selection"
onUpgrade={async () => {
await handler.requestFullAccess();
}}
onDismiss={handler.dismissBlocked}
/>
</View>
)}
>
<PhotoPicker />
</PermissionGate>
);
}
function PhotoPicker() {
return <Text>Picker goes here</Text>;
}Using the hook directly
If you need more control than PermissionGate provides:
function PhotoUploadScreen() {
const photos = usePermissionHandler({
permission: Permissions.PHOTO_LIBRARY,
prePrompt: { title: "Photos", message: "Needed to upload." },
blockedPrompt: { title: "Blocked", message: "Enable in Settings." },
});
if (photos.isLimited) {
return (
<View>
<PhotoPicker />
<Button
title="Allow full access"
onPress={async () => {
const next = await photos.requestFullAccess();
if (next === "granted") {
analytics.track("photo_upgrade_granted");
}
}}
/>
</View>
);
}
if (photos.isGranted) return <PhotoPicker />;
return null;
}Why isGranted is still true in limited
For backward compatibility, isGranted returns true for both granted and limited — existing code that only branches on isGranted keeps working. Use isLimited to detect the narrower case, or branch directly on state if you want exclusive behavior.
Engine requirements
requestFullAccess() delegates to engine.requestFullAccess(), which is optional on the PermissionEngine interface.
- Expo engine (
createExpoEngine) — supported end-to-end viaMediaLibrary.presentPermissionsPickerAsync. - RNP engine (
createRNPEngine) — not yet implemented.react-native-permissionsdoes not currently expose a JS binding for iOSPHPhotoLibrary.presentLimitedLibraryPicker(from:), and this package ships no native code of its own. Callinghandler.requestFullAccess()on the RNP engine throws a clear error at runtime. Thelimitedstate detection andrenderLimitedbranch still work on RNP (those only depend oncheck()) — only the upgrade button is gated. Tracked as future work: ship a tiny optional native module or contribute the binding upstream. - Custom engine — implement
requestFullAccesson your adapter (e.g. wrap your own native shim around the iOS 15+ limited picker API).
Practical takeaway: if you need the limited → full upgrade flow and you're on bare React Native, use createExpoEngine (it works outside Expo Go too, via the Expo modules) or provide a custom engine. Otherwise surface the renderLimited UI and point users to Settings via openSettings() as a fallback.
See the engines reference for details.
Android note
On Android, there is no concept of "limited" photo access at the permission level. The READ_MEDIA_VISUAL_USER_SELECTED permission behaves like a separate permission rather than a partial grant, so the hook will usually stay in granted on Android for this flow. The renderLimited branch only fires on iOS.