{"id":"l-rn-4-3","courseId":"react-native-apps","lang":"ar","kind":"lesson","summary":"اطلب الوصول إلى الموقع والكاميرا في اللحظة التي يكون فيها الطلب منطقيًا، وتعامل مع كل حالات الإذن بما فيها الرفض الدائم، واكتب أوصاف الاستخدام التي يشترطها iOS وتشترطها مراجعة المتاجر.","html":"<p>أفضل ميزة في Trailhead هي التتبّع الحيّ: ابدأ رحلة، فيسجّل التطبيق مسارك. يحتاج ذلك إلى موقع الهاتف، والهاتف لن يعطيه دون أن يسأل المستخدم. وهذا الطلب أكثر لحظة هشاشة في التطبيق. اسأل بطريقة سيئة فتخسر الميزة لدى ذلك المستخدم، وأحيانًا إلى الأبد.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">الحالات التي يمكن أن يكون فيها الإذن</h2>\n<p>كل وحدة في Expo تحتاج إلى إذن (الموقع، الكاميرا، مكتبة الوسائط، الإشعارات) تُرجع شكل الاستجابة نفسه:</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=\"نسخ الكود\"><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>نسخ</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>: لم تسأل قط. والطلب سيعرض نافذة النظام.</li>\n<li><strong>granted</strong>: تابع. وفي إصدارات iOS وAndroid الحديثة، قد يمنح المستخدمون الوصول مرة واحدة فقط (لهذه الجلسة)، أو الموقع التقريبي فقط، فكن مستعدًا لأن تسأل مرة أخرى في يوم آخر، وأن تتلقّى مواقع أقل دقّة.</li>\n<li><strong>denied مع <code dir=\"ltr\">canAskAgain: true</code></strong>: Android بعد رفض واحد. يمكنك أن تسأل مرة أخرى، ويُفضَّل بعد شرح أفضل.</li>\n<li><strong>denied مع <code dir=\"ltr\">canAskAgain: false</code></strong>: لن يعرض النظام النافذة مرة أخرى. على iOS هذه هي الحالة مباشرة بعد أول رفض؛ وعلى Android بعد أن يرفض المستخدم مرتين. ولا يستطيع تغييرها الآن إلا تطبيق الإعدادات.</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\">من حالة undetermined، يشرح التطبيق ثم يطلب. يمنح المستخدم الإذن أو يرفضه. المنح يقود إلى الميزة. والرفض مع canAskAgain بقيمة true يسمح بالطلب مجددًا؛ أما الرفض مع canAskAgain بقيمة false فلا يُغيَّر إلا بفتح الإعدادات.</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\">اشرح، ثم</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: الميزة تعمل</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\">نعم</text>\n  <text class=\"d-label\" x=\"567\" y=\"135\" text-anchor=\"middle\">لا: افتح الإعدادات</text>\n  <path class=\"d-line\" d=\"M567 165 L567 145\"/>\n</svg><figcaption>إلى أين تقود كل حالة إذن</figcaption></figure>\n<h2 id=\"sec-2\" tabindex=\"-1\">اسأل في السياق، بعد أن تشرح</h2>\n<p>أسوأ نمط هو الذي تطلقه تطبيقات كثيرة: سلسلة من نوافذ النظام عند أول تشغيل، قبل أن يعرف المستخدم ما يفعله التطبيق. الناس يرفضون ما لا يفهمونه، وعلى iOS يكون ذلك الـ “Don’t Allow” الأول دائمًا ما لم ينقّبوا في الإعدادات.</p>\n<p>يطلب Trailhead الموقع حين ينقر المستخدم على <strong>Start tracking</strong>، وهي أول لحظة يرتبط فيها الطلب بوضوح بشيء يريده. وقبل نافذة النظام، يعرض شرحه القصير الخاص: “Trailhead records your route while you hike. Your location stays on this phone unless you sync.” ثم يُطلق زرّ <strong>Continue</strong> الطلب الحقيقي. شاشتك أنت يمكن إغلاقها دون عواقب؛ أما نافذة النظام فلا.</p>\n<p>والقاعدة نفسها تنطبق على كل إذن في التطبيق. اطلب الكاميرا حين ينقر المستخدم على <strong>Add photo</strong>، والإشعارات حين يفعّل التذكيرات، لا قبل ذلك. عندها يصل كل طلب وسببه ظاهر على الشاشة أصلًا، وترتفع نسب القبول بشكل ملحوظ.</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=\"نسخ الكود\"><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>نسخ</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>ثم تقرأ شاشة التتبّع <code dir=\"ltr\">permission</code> وتختار ما ترسمه: مؤشّر تحميل ما دامت <code dir=\"ltr\">null</code>، والشرح مع زرّ Continue ما دام الطلب ممكنًا، ورسالة “Location is off for Trailhead” مع زرّ <strong>Open Settings</strong> بمجرد أن تصبح <code dir=\"ltr\">canAskAgain</code> قيمتها false، والخريطة الحيّة بمجرد منح الإذن. وتحويل الاستجابة إلى واحد من هذه الأفعال الأربعة دالة نقية صغيرة، وهي تمرين هذا الدرس.</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\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>الطلب من جديد بعد رفض دائم</span></p>\n<p>استدعاء <code dir=\"ltr\">requestForegroundPermissionsAsync()</code> حين تكون <code dir=\"ltr\">canAskAgain</code> قيمتها false لا يعرض شيئًا، ويُحَلّ فورًا بالرفض نفسه. بالنسبة إلى المستخدم، الزر ببساطة لا يفعل شيئًا. تحقّق من <code dir=\"ltr\">canAskAgain</code> أولًا وانتقل إلى مسار الإعدادات.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">أوصاف الاستخدام والـ config plugins</h2>\n<p>يعرض iOS جملة من تطبيقك في كل نافذة إذن، ويرفض الـ App Store التطبيقات التي تكون أوصافها مفقودة أو مبهمة (“This app needs your location”). قل ما الذي تفعله الميزة بالبيانات. تضبط هذه النصوص عبر الـ <strong>config plugin</strong> الخاص بكل وحدة في <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=\"نسخ الكود\"><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>نسخ</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 الإعدادات الأصلية (ملف <code dir=\"ltr\">Info.plist</code> في iOS، والـ manifest في Android) حين يُبنى التطبيق. وهذا يجعلها <strong>تغييرات أصلية</strong>: لن يطبّقها Fast Refresh، فأعِد بناء الـ development build بعد تعديلها. ويعرض Expo Go نصوصه العامة الخاصة، وهذا سبب إضافي لانتقال Trailhead الآن إلى development build.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">المقدّمة أولًا، والخلفية فقط عند الضرورة</h2>\n<p>تتبّع رحلة والشاشة مطفأة يحتاج إلى موقع في <strong>الخلفية</strong> (background)، وهو إذن منفصل وأكثر حساسية، له متطلّبات إضافية من المنصّتين ومراجعة أدقّ في المتجرين. ابدأ بالوصول في المقدّمة (“while using the app”)، الذي يغطّي التتبّع ما دام Trailhead مفتوحًا. وأضف الوصول في الخلفية فقط حين يطلبه المستخدمون، مع شرح واضح للسبب، وتوقّع أن تطرح عليك المراجعة السؤال نفسه.</p>\n<p>في الدرس التالي ستستخدم هذه الأذونات فعلًا: موقع المتنزّه، وصور الدروب، وإشعار تذكير.</p>\n","toc":[{"id":"sec-1","text":"الحالات التي يمكن أن يكون فيها الإذن","level":2},{"id":"sec-2","text":"اسأل في السياق، بعد أن تشرح","level":2},{"id":"sec-3","text":"أوصاف الاستخدام والـ config plugins","level":2},{"id":"sec-4","text":"المقدّمة أولًا، والخلفية فقط عند الضرورة","level":2}],"takeaways":["استجابات الأذونات لها <code dir=\"ltr\">status</code> قيمتها undetermined أو granted أو denied، إضافةً إلى <code dir=\"ltr\">canAskAgain</code>، التي تخبرك هل ما زال يمكن أن تظهر نافذة النظام.","اطلب الإذن حين يبدأ المستخدم الميزة التي تحتاج إليه، بعد شرح قصير في واجهتك أنت، ولا تطلق سيلًا من النوافذ عند التشغيل أبدًا.","بمجرد أن تصبح <code dir=\"ltr\">canAskAgain</code> قيمتها false، لا يستطيع تغيير الإجابة إلا تطبيق الإعدادات في النظام، فقدّم زرًّا يستدعي <code dir=\"ltr\">Linking.openSettings()</code>.","يشترط iOS وصفًا للاستخدام لكل إذن؛ اضبطها عبر خيارات الـ config plugins في app.json وأعد البناء.","أعِد التحقّق من الأذونات حين تستعيد الشاشة التركيز، لأن المستخدمين يغيّرونها في الإعدادات بينما تطبيقك في الخلفية."],"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 الموقع في أول تشغيل للتطبيق، قبل أن يرى المستخدم أي شيء. على iOS، ينقر كثير من المستخدمين على Don’t Allow. لماذا يهمّ هذا إلى هذا الحد؟</p>","options":[{"text":"يُرفض التطبيق في الـ App Store لأنه يطلب الإذن عند التشغيل.","why":"الطلب عند التشغيل لا يعني الرفض تلقائيًا. الكلفة الحقيقية تقع على مستخدميك وميزاتك."},{"text":"بعد الرفض، لا يعرض iOS نافذة النظام مرة أخرى؛ ولا يغيّرها إلا تطبيق الإعدادات، فتصبح ميزة التتبّع عمليًا مفقودة لهؤلاء المستخدمين.","why":"صحيح. تحصل على نافذة نظام واحدة لكل إذن على iOS، فاستخدمها حين يفهم المستخدم السبب."},{"text":"الرفض عند التشغيل يرفض أيضًا أذونات الكاميرا والإشعارات.","why":"كل إذن يُجاب عنه على حدة. الضرر يقع على الموقع وحده، لكنه دائم."}],"answer":1},{"q":"<p>استجابة الإذن هي <code dir=\"ltr\">{ status: 'denied', granted: false, canAskAgain: false }</code>. ماذا يجب أن يفعل زرّ Start tracking؟</p>","options":[{"text":"يستدعي <code dir=\"ltr\">requestForegroundPermissionsAsync()</code> مرة أخرى؛ فقد يكون المستخدم غيّر رأيه.","why":"مع <code dir=\"ltr\">canAskAgain</code> بقيمة false، يُحَلّ الطلب فورًا بالرفض نفسه ولا تظهر أي نافذة."},{"text":"يُخفي الزر نهائيًا.","why":"قد يريد المستخدم الميزة لاحقًا. وإخفاء الزر لا يترك له طريقًا للعودة."},{"text":"يشرح أن الموقع مُطفأ لـ Trailhead ويقدّم زرًّا يفتح صفحة التطبيق في الإعدادات.","why":"صحيح. يأخذه <code dir=\"ltr\">Linking.openSettings()</code> مباشرة إلى المفتاح الذي يحتاج إليه."},{"text":"يبدأ التتبّع على أي حال ويعرض خطأ إن فشل.","why":"ستُرفض استدعاءات الموقع؛ وبدء ميزة تعرف أنها لا تعمل تجربة أسوأ من الشرح."}],"answer":2},{"q":"<p>غيّرت وصف استخدام الموقع في app.json، وأعدت التحميل بـ Fast Refresh، وما زال النص القديم يظهر في النافذة. لماذا؟</p>","options":[{"text":"الوصف جزء من الإعدادات الأصلية للتطبيق، فلا يتغيّر إلا في build جديد.","why":"صحيح. نصوص الاستخدام تُترجَم إلى Info.plist؛ أعِد بناء الـ development build لتراها."},{"text":"يخزّن iOS نص النافذة مؤقتًا لمدة 24 ساعة.","why":"لا يوجد تخزين مؤقت كهذا؛ النص يأتي من الملف التنفيذي المثبّت."},{"text":"يجب ضبط الوصف في JavaScript باستخدام <code dir=\"ltr\">Location.setDescription()</code>.","why":"لا توجد دالة كهذه. أوصاف الاستخدام إعدادات أصلية، لا استدعاءات وقت التشغيل."}],"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":"حوّل استجابة الإذن إلى شاشة","prompt":"<p>تستدعي شاشة التتبّع في Trailhead الدالة <code dir=\"ltr\">permissionAction(response)</code> لتقرّر ما ترسمه. <code dir=\"ltr\">response</code> هو الكائن القادم من <code dir=\"ltr\">getForegroundPermissionsAsync()</code>، أو <code dir=\"ltr\">null</code> ما دام التحميل جاريًا. أرجع:</p>\n<ul>\n<li><code dir=\"ltr\">'wait'</code> ما دامت الاستجابة <code dir=\"ltr\">null</code> أو <code dir=\"ltr\">undefined</code> (اعرض مؤشّر تحميل)،</li>\n<li><code dir=\"ltr\">'proceed'</code> حين يُمنح الوصول (اعرض الخريطة)،</li>\n<li><code dir=\"ltr\">'ask'</code> حين يمكن أن تظهر نافذة النظام: لم يُسأل قط، أو رُفض مع <code dir=\"ltr\">canAskAgain: true</code> (اعرض الشرح وزرّ Continue)،</li>\n<li><code dir=\"ltr\">'open-settings'</code> حين يُرفض وتكون <code dir=\"ltr\">canAskAgain</code> قيمتها <code dir=\"ltr\">false</code> (اعرض زرّ Open Settings).</li>\n</ul>\n","hints":["تعامل مع حالة التحميل أولًا، قبل أن تقرأ أي خاصية من <code dir=\"ltr\">response</code>.","<code dir=\"ltr\">granted</code> قيمة منطقية أصلًا، فلا حاجة إلى مقارنة <code dir=\"ltr\">status</code> في تلك الحالة.","في حالة الرفض، الشيء الوحيد المهم هو <code dir=\"ltr\">canAskAgain</code>."],"explanation":"<p>انهار الكود المبدئي مع <code dir=\"ltr\">null</code> (قراءة <code dir=\"ltr\">.status</code> من لا شيء)، وأرسل المستخدمين الذين رفضوا رفضًا دائمًا إلى طلب لا يفعل شيئًا بصمت. تغطّي الدالة المُصلحة كل حالة بترتيب ثابت: التحميل، ثم المنح، ثم النوعين من «لم يُمنح بعد». ولأنها نقية، يمكنك اختبار الفروع الأربعة في أجزاء من الثانية، وتصبح الشاشة مجرّد switch على أربعة نصوص.</p>\n","checkLabels":{"loading":"الاستجابة المفقودة تعني الانتظار","granted":"الإذن الممنوح يتابع","undetermined":"الإذن الذي لم يُطلب قط يقود إلى الطلب","denied-can-ask":"الرفض الذي يمكن الطلب بعده مجددًا يقود إلى الطلب","denied-final":"الرفض الدائم يقود إلى الإعدادات"}},"words":576,"readMinutes":3}