Permissions screen
ProReact Native · ExpoA permission priming screen: each permission is a list row with a switch that asks the system only when turned on, a reminder preview drops in on a spring when notifications are allowed, a denial shows a banner with a way to Settings, and Not now is one tap away.
Installation
$ npx shadcn@latest add @goodui-native-pro/permissions-screenThen run npx expo install --fix so react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/permissions-screen.
Pro screens install from the license-gated @goodui-native-pro registry. Setup: Pro access.
Usage
// app/(onboarding)/permissions.tsx
import { Linking } from "react-native";
import { router } from "expo-router";
import * as Notifications from "expo-notifications";
import * as Location from "expo-location";
import { PermissionsScreen } from "@/components/goodui-native/permissions-screen/permissions-screen";
export default function PermissionsRoute() {
return (
<PermissionsScreen
appName="Stride"
onRequest={async (key) => {
const result =
key === "notifications"
? await Notifications.requestPermissionsAsync()
: await Location.requestForegroundPermissionsAsync();
return result.granted;
}}
onOpenSettings={() => Linking.openSettings()}
onContinue={() => router.replace("/paywall")}
onSkip={() => router.replace("/paywall")}
/>
);
}When to use
- Right after sign-up, before the system dialogs, to explain why you ask for notifications, location, camera or contacts.
- When a denied permission needs a clear way back through Settings.
- Asking at the moment a feature needs it (attach a photo, share location once): ask in place with alert-dialog or directly.
- Settings the app controls by itself: use list-row switches in a settings screen.
Examples
Already granted on open
<PermissionsScreen defaultStatus={{ notifications: "granted" }} onRequest={request} onContinue={next} onSkip={next} />Your own permissions
<PermissionsScreen
title="Share your moments"
preview={null}
items={[
{ key: "camera", title: "Camera", description: "Take photos for your posts." },
{ key: "contacts", title: "Find friends", description: "We match numbers on your phone. Nothing is stored." },
]}
onRequest={request}
onContinue={next}
onSkip={next}
/>API reference
| Prop | Type | Default |
|---|---|---|
| appName Product name for the default copy and the preview notification. Without it the copy says "the app". | string | — |
| title Headline. Same as `labels.title`. | string | — |
| description One or two sentences under the headline. Same as `labels.description`. | string | — |
| items The permissions to prime, in order. Default notifications and location. | PermissionItem[] | — |
| defaultStatus Statuses you already know (from the system) when the screen opens. Granted rows start switched on. | Record<string, PermissionStatus> | {} |
| onRequest Called when a switch is turned on. Ask the system here (expo-notifications, expo-location) and return a promise of the result: `"granted"` or `true` keeps the switch on; anything else flips it back with a shake and shows the item's denied banner. While pending the switch shows a spinner. Without it the switch simply toggles. | (key: string) => unknown | — |
| onOpenSettings "Open settings" action on the denied banner, for example `Linking.openSettings()`. Hidden when omitted. | () => unknown | — |
| onContinue Continue. Receives which items ended up on. Return a promise to show pending and a check on the button. | (enabled: Record<string, boolean>) => unknown | — |
| onSkip "Not now". Leave the screen without asking anything. | () => void | — |
| preview Art above the headline. Defaults to a reminder preview that drops in when notifications turn on. `null` hides it. | ReactNode | null | — |
| previewCopy Words in the default preview, so it can speak for your app without replacing it. Neutral by default. | PermissionsPreviewCopy | — |
| labels Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. | Partial<PermissionsScreenLabels> | — |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
| testID Identifier for end-to-end tests. | string | — |
Motion and haptics
- Allowing notifications drops the reminder preview into place on the bouncy spring (from 14 pt above, 95 % scale, 40 % opacity) and the location chip rises in when location is on.
- While the system dialog is open the switch shows a spinner in its thumb; a denial flips it back with a shake and the banner slides down above the list.
- Reduced motion: the preview and chip only fade; switches and the banner follow their own reduced-motion branches.
- Haptic: toggle-on or toggle-off when a switch changes, error when a request is denied (from switch). The banner stays silent so a denial buzzes once.
- Haptic: tap and success from the Continue button.
- Two shared values drive the preview on the UI thread; switches re-render only their row.
Accessibility
- The headline is a header. Each row's switch carries the row title as its label (from list-row).
- A denied request flips the switch back and the warning banner is announced with its title and description.
- The reminder preview is decoration and hidden from screen readers.
- Continue and Not now are buttons in the bottom bar, within thumb reach.
Notes for AI
- onRequest(key) must ask the system and return a promise of true or 'granted' to keep the switch on; return anything else for a denial. Do not show your own spinner or alert.
- Turning a switch off never calls onRequest; it only records that the person does not want it. onContinue receives { key: boolean } for every item.
- Pass defaultStatus from the system's current permissions so granted rows start on and are not asked again.
- The default preview reacts to the keys 'notifications' and 'location'; pass preview={null} or your own node for other items.
- Default copy (items, headline, preview) is neutral so it fits any app. Pass items and title for your app, and previewCopy={{ title, body, location }} to make the default preview speak for it (for example a run reminder in a fitness app).
- Render it as the whole route with the header hidden; it handles safe areas itself.
Related
Settings and navigation cells with a token-colored icon tile, title, subtitle and a trailing value, switch, badge, check or chevron, grouped in inset sections with headers and footers. On touch down the highlight floods in, the tile sinks and the chevron leans forward; on release the highlight lingers and fades.
An iOS-style switch with squash and stretch: the thumb stretches toward where it is going on touch down, then travels on a spring while the track color follows. Settings-row layout, tones, sizes and optimistic async changes.
An inline status banner that slides down from under the edge above and pushes the content down, flings away sideways with the finger, and can collapse on its own behind a draining hairline; includes an offline preset.
A complete first-run welcome: an onboarding pager with parallax pages and a worm page dot, where each page's art plays as it settles (a weekly ring fills, a distance chart draws, a group of runners fans out), Get started and a Sign in link.