{"id":"l-rn-4-3","courseId":"react-native-apps","lang":"en","kind":"lesson","summary":"Ask for location and camera access at the moment it makes sense, handle every permission state including permanent denial, and write the usage descriptions iOS and app review require.","html":"<p>Trailhead’s best feature is live tracking: start a hike, and the app records your route. It needs the phone’s location, and the phone won’t give it without asking the user. That request is the most fragile moment in the app. Ask badly and you lose the feature for that user, sometimes for good.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The states a permission can be in</h2>\n<p>Every Expo module that needs a permission (location, camera, media library, notifications) returns the same response shape:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  status</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'undetermined'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'granted'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'denied'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  granted</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: boolean,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  canAskAgain</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: boolean,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  expires</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'never'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> number,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<ul>\n<li><strong>undetermined</strong>: you’ve never asked. A request will show the system prompt.</li>\n<li><strong>granted</strong>: go ahead. On recent iOS and Android versions, users may grant access only once (for this session) or only approximate location, so be ready to ask again another day and to receive less precise positions.</li>\n<li><strong>denied with <code dir=\"ltr\">canAskAgain: true</code></strong>: Android after a single denial. You can ask once more, ideally after explaining better.</li>\n<li><strong>denied with <code dir=\"ltr\">canAskAgain: false</code></strong>: the system won’t show the prompt again. On iOS that’s the state right after the first denial; on Android, after the user denies twice. Only the Settings app can change it now.</li>\n</ul>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 250\" role=\"img\" aria-labelledby=\"l-rn-4-3-t1\"><defs><marker id=\"arrow-l-rn-4-3-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-rn-4-3-t1\">From undetermined, the app explains and then requests. The user grants or denies. Granted proceeds to the feature. Denied with canAskAgain true can be requested again; denied with canAskAgain false can only be changed by opening Settings.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"95\" width=\"150\" height=\"56\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"95\" y=\"128\" text-anchor=\"middle\">undetermined</text>\n  <path class=\"d-arrow\" d=\"M170 123 L235 123\" marker-end=\"url(#arrow-l-rn-4-3-1)\"/>\n  <rect class=\"d-box-primary\" x=\"240\" y=\"95\" width=\"150\" height=\"56\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"315\" y=\"120\" text-anchor=\"middle\">explain, then</text>\n  <text class=\"d-code\" x=\"315\" y=\"140\" text-anchor=\"middle\">request…()</text>\n  <path class=\"d-arrow\" d=\"M390 110 L470 55\" marker-end=\"url(#arrow-l-rn-4-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M390 140 L470 195\" marker-end=\"url(#arrow-l-rn-4-3-1)\"/>\n  <rect class=\"d-box-success\" x=\"475\" y=\"25\" width=\"185\" height=\"56\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"567\" y=\"58\" text-anchor=\"middle\">granted → feature</text>\n  <rect class=\"d-box-warn\" x=\"475\" y=\"165\" width=\"185\" height=\"70\" rx=\"12\"/>\n  <text class=\"d-label-strong\" x=\"567\" y=\"192\" text-anchor=\"middle\">denied</text>\n  <text class=\"d-label-muted\" x=\"567\" y=\"216\" text-anchor=\"middle\">canAskAgain?</text>\n  <path class=\"d-arrow d-dashed\" d=\"M475 185 C420 175 400 160 380 152\" marker-end=\"url(#arrow-l-rn-4-3-1)\"/>\n  <text class=\"d-label-muted\" x=\"420\" y=\"200\" text-anchor=\"middle\">yes</text>\n  <text class=\"d-label\" x=\"567\" y=\"135\" text-anchor=\"middle\">no → open Settings</text>\n  <path class=\"d-line\" d=\"M567 165 L567 145\"/>\n</svg><figcaption>Where each permission state leads</figcaption></figure>\n<h2 id=\"sec-2\" tabindex=\"-1\">Ask in context, once you’ve explained</h2>\n<p>The worst pattern is the one many apps ship: a cascade of system prompts at first launch, before the user knows what the app does. People deny what they don’t understand, and on iOS that first “Don’t Allow” is permanent unless they dig into Settings.</p>\n<p>Trailhead asks for location when the user taps <strong>Start tracking</strong>, the first moment the request is obviously connected to something they want. Before the system prompt, it shows its own short explanation: “Trailhead records your route while you hike. Your location stays on this phone unless you sync.” Then a <strong>Continue</strong> button triggers the real request. Your own screen can be dismissed without consequence; the system prompt can’t.</p>\n<p>The same rule applies to every permission in the app. Ask for the camera when the user taps <strong>Add photo</strong>, and for notifications when they turn on reminders, not before. Each request then arrives with its reason already visible on screen, and acceptance rates go up noticeably.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">src/hooks/useLocationPermission.ts</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> *</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Location </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'expo-location'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { useFocusEffect } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'expo-router'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { useCallback, useState } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { Linking } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react-native'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useLocationPermission</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">permission</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setPermission</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Location</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#953800;--shiki-dark:#FFA657\">LocationPermissionResponse</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // Re-check whenever the screen regains focus: the user may have</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // changed the setting in the Settings app while Trailhead was in the background.</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  useFocusEffect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">    useCallback</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      Location.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getForegroundPermissionsAsync</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">then</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(setPermission);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }, []),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  );</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> request</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useCallback</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> result</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Location.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">requestForegroundPermissionsAsync</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">    setPermission</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(result);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> result.granted;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }, []);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { permission, request, </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">openSettings</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Linking.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">openSettings</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() };</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>The tracking screen then reads <code dir=\"ltr\">permission</code> and chooses what to render: a spinner while it’s <code dir=\"ltr\">null</code>, the explanation and Continue button while asking is still possible, a “Location is off for Trailhead” message with an <strong>Open Settings</strong> button once <code dir=\"ltr\">canAskAgain</code> is false, and the live map once it’s granted. Turning the response into one of those four actions is a small pure function, and it’s this lesson’s exercise.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Requesting again after a permanent denial</span></p>\n<p>Calling <code dir=\"ltr\">requestForegroundPermissionsAsync()</code> when <code dir=\"ltr\">canAskAgain</code> is false shows nothing and resolves straight away with the same denial. To the user, the button simply does nothing. Check <code dir=\"ltr\">canAskAgain</code> first and switch to the Settings path.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Usage descriptions and config plugins</h2>\n<p>iOS shows a sentence from your app in every permission prompt, and the App Store rejects apps whose descriptions are missing or vague (“This app needs your location”). Say what the feature does with the data. You set these strings through each module’s <strong>config plugin</strong> in <code dir=\"ltr\">app.json</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">app.json</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"expo\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    \"plugins\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      [</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">        \"expo-location\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"locationWhenInUsePermission\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Trailhead records your route on the map while you track a hike.\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      ],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      [</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">        \"expo-image-picker\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          \"photosPermission\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Trailhead lets you attach photos from your library to a hike.\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          \"cameraPermission\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Trailhead lets you take trail photos to attach to a hike.\"</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      ]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    ]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Config plugins write native configuration (the iOS <code dir=\"ltr\">Info.plist</code>, the Android manifest) when the app is built. That makes them <strong>native changes</strong>: Fast Refresh won’t apply them, so rebuild your development build after editing them. Expo Go shows its own generic strings, which is one more reason Trailhead moves to a development build now.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Foreground first, background only if you must</h2>\n<p>Tracking a hike with the screen off needs <strong>background</strong> location, a separate, more sensitive permission with extra platform requirements and closer review on both stores. Start with foreground (“while using the app”) access, which covers tracking while Trailhead is open. Add background access only when users ask for it, with a clear explanation of why, and expect the review to ask you the same question.</p>\n<p>Next you’ll use these permissions for real: the hiker’s position, trail photos, and a reminder notification.</p>\n","toc":[{"id":"sec-1","text":"The states a permission can be in","level":2},{"id":"sec-2","text":"Ask in context, once you’ve explained","level":2},{"id":"sec-3","text":"Usage descriptions and config plugins","level":2},{"id":"sec-4","text":"Foreground first, background only if you must","level":2}],"takeaways":["Permission responses have a <code dir=\"ltr\">status</code> of undetermined, granted or denied, plus <code dir=\"ltr\">canAskAgain</code>, which tells you whether a system prompt can still appear.","Ask when the user starts the feature that needs it, after a short explanation in your own UI, never as a wall of prompts at launch.","Once <code dir=\"ltr\">canAskAgain</code> is false, only the system Settings app can change the answer, so offer a button that calls <code dir=\"ltr\">Linking.openSettings()</code>.","iOS requires a usage description for each permission; set them through config plugin options in app.json and rebuild.","Re-check permissions when a screen regains focus, because users change them in Settings while your app is in the background."],"further":[{"title":"Permissions","url":"https://docs.expo.dev/guides/permissions/"},{"title":"Location","url":"https://docs.expo.dev/versions/latest/sdk/location/"},{"title":"Linking","url":"https://reactnative.dev/docs/linking"}],"quiz":[{"q":"<p>Trailhead asks for location the first time the app launches, before the user has seen anything. On iOS, many users tap Don’t Allow. Why does this matter so much?</p>","options":[{"text":"The app is rejected from the App Store for asking at launch.","why":"Asking at launch isn’t automatically a rejection. The real cost is to your users and features."},{"text":"After a denial, iOS won’t show the system prompt again; only the Settings app can change it, so the tracking feature is effectively gone for those users.","why":"Correct. You get one system prompt per permission on iOS, so spend it when the user understands why."},{"text":"Denying at launch also denies camera and notification permissions.","why":"Each permission is answered separately. The damage is to location only, but it’s lasting."}],"answer":1},{"q":"<p>A permission response is <code dir=\"ltr\">{ status: 'denied', granted: false, canAskAgain: false }</code>. What should the Start tracking button do?</p>","options":[{"text":"Call <code dir=\"ltr\">requestForegroundPermissionsAsync()</code> again; the user may have changed their mind.","why":"With <code dir=\"ltr\">canAskAgain</code> false, the request resolves immediately with the same denial and no prompt appears."},{"text":"Hide the button permanently.","why":"The user may want the feature later. Hiding it gives them no path back."},{"text":"Explain that location is off for Trailhead and offer a button that opens the app’s page in Settings.","why":"Correct. <code dir=\"ltr\">Linking.openSettings()</code> takes them straight to the switch they need."},{"text":"Start tracking anyway and show an error if it fails.","why":"The location calls will reject; starting a feature you know can’t work is a worse experience than explaining."}],"answer":2},{"q":"<p>You change the location usage description in app.json, reload with Fast Refresh, and the old text still appears in the prompt. Why?</p>","options":[{"text":"The description is part of the native app configuration, so it only changes in a new build.","why":"Correct. Usage strings are compiled into Info.plist; rebuild the development build to see them."},{"text":"iOS caches prompt text for 24 hours.","why":"There’s no such cache; the text comes from the installed binary."},{"text":"The description must be set in JavaScript with <code dir=\"ltr\">Location.setDescription()</code>.","why":"There’s no such function. Usage descriptions are native configuration, not runtime calls."}],"answer":0}],"exercise":{"kind":"web","mode":"js","starter":{"js":"// Decide what the tracking screen shows for a permission response.\n// Returns one of: 'wait' | 'proceed' | 'ask' | 'open-settings'\nfunction permissionAction(response) {\n  if (response.status === 'granted') return 'proceed';\n  return 'ask';\n}\n\nconsole.log(permissionAction({ status: 'denied', granted: false, canAskAgain: false, expires: 'never' }));\n"},"solution":{"js":"function permissionAction(response) {\n  if (!response) return 'wait';\n  if (response.granted) return 'proceed';\n  if (response.status === 'undetermined') return 'ask';\n  return response.canAskAgain ? 'ask' : 'open-settings';\n}\n\nconsole.log(permissionAction({ status: 'denied', granted: false, canAskAgain: false, expires: 'never' }));\n"},"checks":[{"id":"loading","test":"return typeof permissionAction === 'function' && permissionAction(null) === 'wait' && permissionAction(undefined) === 'wait';"},{"id":"granted","test":"return permissionAction({ status: 'granted', granted: true, canAskAgain: true, expires: 'never' }) === 'proceed';"},{"id":"undetermined","test":"return permissionAction({ status: 'undetermined', granted: false, canAskAgain: true, expires: 'never' }) === 'ask';"},{"id":"denied-can-ask","test":"return permissionAction({ status: 'denied', granted: false, canAskAgain: true, expires: 'never' }) === 'ask';"},{"id":"denied-final","test":"return permissionAction({ status: 'denied', granted: false, canAskAgain: false, expires: 'never' }) === 'open-settings';"}],"title":"Turn a permission response into a screen","prompt":"<p>Trailhead’s tracking screen calls <code dir=\"ltr\">permissionAction(response)</code> to decide what to render. <code dir=\"ltr\">response</code> is the object from <code dir=\"ltr\">getForegroundPermissionsAsync()</code>, or <code dir=\"ltr\">null</code> while it’s still loading. Return:</p>\n<ul>\n<li><code dir=\"ltr\">'wait'</code> while the response is <code dir=\"ltr\">null</code> or <code dir=\"ltr\">undefined</code> (show a spinner),</li>\n<li><code dir=\"ltr\">'proceed'</code> when access is granted (show the map),</li>\n<li><code dir=\"ltr\">'ask'</code> when a system prompt can still appear: never asked, or denied with <code dir=\"ltr\">canAskAgain: true</code> (show the explanation and a Continue button),</li>\n<li><code dir=\"ltr\">'open-settings'</code> when it’s denied and <code dir=\"ltr\">canAskAgain</code> is <code dir=\"ltr\">false</code> (show an Open Settings button).</li>\n</ul>\n","hints":["Handle the loading case first, before you read any property of <code dir=\"ltr\">response</code>.","<code dir=\"ltr\">granted</code> is already a boolean, so you don’t need to compare <code dir=\"ltr\">status</code> for that case.","For a denial, the only thing that matters is <code dir=\"ltr\">canAskAgain</code>."],"explanation":"<p>The starter crashed on <code dir=\"ltr\">null</code> (reading <code dir=\"ltr\">.status</code> of nothing) and sent permanently denied users to a request that silently does nothing. The fixed function covers every state in a fixed order: loading, granted, then the two kinds of not-yet-granted. Because it’s pure, you can test all four branches in milliseconds, and the screen becomes a simple switch over four strings.</p>\n","checkLabels":{"loading":"A missing response means wait","granted":"A granted permission proceeds","undetermined":"A permission never asked for leads to asking","denied-can-ask":"A denial that can be asked again leads to asking","denied-final":"A permanent denial leads to Settings"}},"words":657,"readMinutes":3}