{"id":"l-rn-3-5","courseId":"react-native-apps","lang":"ar","kind":"lesson","summary":"اعرض نموذج إضافة رحلة في Trailhead بوصفه modal، وأبقِ المستخدمين غير المسجّلين على شاشة تسجيل الدخول باستخدام Stack.Protected، وافتح رحلات محدّدة من الـ deep links، مع فهم كيف تُطابَق الـ URLs مع ملفات المسارات.","html":"<p>ثلاثة مواقف لا تناسب نمط «أضف شاشة» الذي استخدمته حتى الآن. تسجيل رحلة مهمّة قصيرة يبدؤها المستخدم وينهيها، فيجب أن تبدو منفصلة عن التصفّح. وبعض الشاشات يجب ألا توجد إلا للمستخدمين المسجّلين. ويجب أن ينفتح Trailhead على الرحلة الصحيحة حين ينقر أحدهم على رابط في تذكير أو رسالة. يتعامل Expo Router مع الثلاثة في ملف الـ layout.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">نموذج إضافة رحلة بوصفه modal</h2>\n<p>الـ modal شاشة تُعرض فوق السياق الحالي، وتنزلق عادةً من الأسفل. على iOS تظهر بطاقةً تبقى الشاشة السابقة مرئية خلفها، ويستطيع المستخدمون سحبها إلى الأسفل للإلغاء. اجعل النموذج modal بضبط طريقة عرضه في الـ Stack الجذري:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/app/_layout.tsx</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\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Screen</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"(tabs)\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> options</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ headerShown: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</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\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Screen</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"hikes/[id]\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> options</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ title: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Hike'</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\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Screen</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"new-hike\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> options</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ presentation: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'modal'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, title: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Log a hike'</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\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>يبقى فتحه بـ <code dir=\"ltr\">router.push('/new-hike')</code> (أو <code dir=\"ltr\">Link</code>)؛ وإغلاقه بعد الحفظ بـ <code dir=\"ltr\">router.back()</code>، الذي يُزيل الـ modal كأي شاشة. لا يملك Android ورقة على شكل بطاقة افتراضيًا، فيظهر الـ modal صفحةً بملء الشاشة بحركة مختلفة؛ لذا أعطِ النموذج زرّ Cancel ظاهرًا في <code dir=\"ltr\">headerLeft</code> ليكون للمنصّتين مخرج واضح.</p>\n<p>حين يحوي النموذج مدخلات غير محفوظة، فإن سحبه بعيدًا يُضيّع ما كتبه المستخدم. في النماذج الطويلة، اطلب التأكيد قبل التجاهل باستخدام <code dir=\"ltr\">Alert</code> عند Cancel؛ أما في نموذج قصير مثل نموذج Trailhead، فمسوّدة تُعاد كتابتها بسرعة مقايضة مقبولة.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">المسارات المحميّة</h2>\n<p>يزامن Trailhead الرحلات مع حساب، لذا تتطلّب معظم الشاشات تسجيل الدخول. يغلّف <code dir=\"ltr\">Stack.Protected</code> المسارات بشرط:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">src/app/_layout.tsx</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:#1F2328;--shiki-dark:#E6EDF3\"> { Stack } </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\"> { useSession } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '../state/session'</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\"> default</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> RootLayout</span><span style=\"color:#953800;--shiki-dark:#FFA657\">() </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\">isSignedIn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">isLoading</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useSession</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (isLoading) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// the splash screen is still showing</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\"> (</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Protected</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> guard</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">isSignedIn</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\">        &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Screen</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"sign-in\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> options</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ headerShown: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</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\">      &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Protected</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Protected</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> guard</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">isSignedIn</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\">        &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Screen</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"(tabs)\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> options</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ headerShown: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</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\">        &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Screen</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"hikes/[id]\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> options</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ title: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Hike'</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\">        &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Screen</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"new-hike\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> options</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ presentation: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'modal'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, title: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Log a hike'</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\">      &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack.Protected</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Stack</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></code></pre></div>\n<p>حين يكون <code dir=\"ltr\">guard</code> قيمته false، تصبح تلك المسارات غير متاحة: التنقّل إليها، بما في ذلك من deep link، يرسل المستخدم إلى أول شاشة متاحة بدلًا منها، وتُحذف أي مدخلات لها في السجلّ. تسجيل الدخول يقلب قيمة <code dir=\"ltr\">isSignedIn</code>، فيُعاد الـ render للـ layout، وتظهر الشاشات المحميّة دون أن تستدعي <code dir=\"ltr\">router</code> على الإطلاق. وتسجيل الخروج يفعل العكس، فيمسح أيضًا الـ back stack، فلا تستطيع إيماءة الرجوع كشف شاشة خاصة بالمستخدم المسجّل.</p>\n<p><code dir=\"ltr\">useSession</code> هو context مثل الذي بنيته قبل درسين. تغطّي <code dir=\"ltr\">isLoading</code> اللحظة عند بدء التشغيل التي يكون فيها Trailhead ما زال يقرأ الـ token المحفوظ من التخزين الآمن (القسم 4)؛ ورسم لا شيء بينما شاشة البداية ظاهرة يتجنّب وميض صفحة تسجيل الدخول للمستخدمين المسجّلين أصلًا.</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\">if (!user) return &lt;Redirect href=&quot;/sign-in&quot; /&gt;</code> في أعلى كل شاشة ينجح حتى يضيف أحدهم شاشة وينسى التحقّق. أما الـ guard في الـ layout فيحمي كل مسار مدرج داخله، بما في ذلك المسارات التي ستُضاف العام القادم. احتفظ بإعادة التوجيه داخل الشاشة للحالات الخاصة.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">الـ deep links</h2>\n<p>الـ <strong>deep link</strong> (الرابط العميق) يفتح التطبيق على شاشة محدّدة. أعطِ Trailhead مخطّط URL (scheme) في <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\">    \"name\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Trailhead\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    \"scheme\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"trailhead\"</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>الآن يفتح <code dir=\"ltr\">trailhead://hikes/42</code> التطبيق ويرسم <code dir=\"ltr\">/hikes/42</code> بالـ Stack نفسه الذي بنيته، بما في ذلك زرّ رجوع إلى السجلّ. تحصل على هذا مجانًا من التوجيه المبني على الملفات: كل مسار له URL أصلًا. المخطّطات جزء من الإعدادات الأصلية للتطبيق، لذا اختبرها في development build؛ فلـ Expo Go مخطّطه الخاص. ومن الطرفية:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">Terminal</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:#953800;--shiki-dark:#FFA657\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> uri-scheme</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> open</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> trailhead://hikes/42</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --ios</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> uri-scheme</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> open</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> trailhead://hikes/42</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --android</span></span></code></pre></div>\n<p>للمخطّطات المخصّصة نقطة ضعف: أي تطبيق يستطيع ادّعاء <code dir=\"ltr\">trailhead://</code>. وللروابط التي تشاركها على الويب، استخدم الـ <strong>universal links</strong> (على iOS) والـ <strong>App Links</strong> (على Android)، التي تستخدم نطاق <code dir=\"ltr\">https://</code> الخاص بك وتتطلّب استضافة ملف تحقّق صغير عليه. يشرح دليل الروابط في Expo الاثنين خطوة بخطوة.</p>\n<aside class=\"callout callout-tip\" 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=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>الـ deep links مدخلات غير موثوقة</span></p>\n<p>يمكن أن يأتي الـ deep link من أي مكان، بأي معرّف. والدالة <code dir=\"ltr\">parseHikeParams</code> من وقت سابق في هذا القسم هي بالضبط ما تحتاج إليه هذه الشاشات: تحقّق، ثم اعرض «الرحلة غير موجودة» لأي شيء غير موجود.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">كيف يختار الـ URL مساره</h2>\n<p>حين يصل رابط، يقارن Expo Router مساره بكل ملف مسار. ومعرفة القواعد تُزيل التخمين:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"جدول قابل للتمرير\" tabindex=\"0\"><table><thead>\n<tr>\n<th>القاعدة</th>\n<th>مثال</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>المجموعات مثل <code dir=\"ltr\">(tabs)</code> لا تضيف جزءًا</td>\n<td><code dir=\"ltr\">(tabs)/stats.tsx</code> يطابق <code dir=\"ltr\">/stats</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">index</code> لا يضيف جزءًا</td>\n<td><code dir=\"ltr\">settings/index.tsx</code> يطابق <code dir=\"ltr\">/settings</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">[name]</code> يطابق جزءًا واحدًا بالضبط</td>\n<td><code dir=\"ltr\">hikes/[id].tsx</code> يطابق <code dir=\"ltr\">/hikes/42</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">[...name]</code> يطابق جزءًا متبقّيًا أو أكثر، بوصفها مصفوفة</td>\n<td><code dir=\"ltr\">trails/[...path].tsx</code> يطابق <code dir=\"ltr\">/trails/alps/mont-blanc</code></td>\n</tr>\n<tr>\n<td>الثابت يتغلّب على الديناميكي، والديناميكي على الشامل</td>\n<td><code dir=\"ltr\">/hikes/new</code> يختار <code dir=\"ltr\">hikes/new.tsx</code> بدلًا من <code dir=\"ltr\">hikes/[id].tsx</code></td>\n</tr>\n<tr>\n<td>الـ query string تصبح params إضافية</td>\n<td><code dir=\"ltr\">/hikes/42?units=mi</code> يضيف <code dir=\"ltr\">units</code></td>\n</tr>\n</tbody>\n</table></div><p>هذا الترتيب هو سبب قدرتك على إضافة <code dir=\"ltr\">hikes/new.tsx</code> بجوار <code dir=\"ltr\">hikes/[id].tsx</code> بأمان. وفي التمرين ستكتب أداة المطابقة هذه بنفسك، وهي أسرع طريقة لكي لا تفاجئك مرة أخرى أبدًا.</p>\n<p>بهذا يكتمل التنقّل. أما القسم 4 فيصل Trailhead بالعالم الخارجي: خادم، وتخزين الجهاز، ومستشعراته.</p>\n","toc":[{"id":"sec-1","text":"نموذج إضافة رحلة بوصفه modal","level":2},{"id":"sec-2","text":"المسارات المحميّة","level":2},{"id":"sec-3","text":"الـ deep links","level":2},{"id":"sec-4","text":"كيف يختار الـ URL مساره","level":2}],"takeaways":["شاشة Stack مع <code dir=\"ltr\">presentation: 'modal'</code> تنزلق إلى الأعلى كورقة على iOS؛ وأغلقها باستخدام <code dir=\"ltr\">router.back()</code> بعد الحفظ.","يُخفي <code dir=\"ltr\">Stack.Protected</code> مع <code dir=\"ltr\">guard</code> المسارات حين يكون الشرط false، فلا يستطيع المستخدمون غير المسجّلين الوصول إليها، حتى من deep link.","الـ <code dir=\"ltr\">scheme</code> في app.json يجعل <code dir=\"ltr\">trailhead://hikes/42</code> يفتح المسار <code dir=\"ltr\">/hikes/[id]</code>؛ والمخطّطات المخصّصة تحتاج إلى development build، لا إلى Expo Go.","عند مطابقة URL، لا تضيف المجموعات ولا <code dir=\"ltr\">index</code> أي جزء، والأجزاء الثابتة تتغلّب على الديناميكية، و<code dir=\"ltr\">[...rest]</code> يلتقط كل ما تبقّى."],"further":[{"title":"Modals","url":"https://docs.expo.dev/router/advanced/modals/"},{"title":"Protected routes","url":"https://docs.expo.dev/router/advanced/protected/"},{"title":"Linking into your app","url":"https://docs.expo.dev/linking/into-your-app/"}],"quiz":[{"q":"<p>في Trailhead الملفان <code dir=\"ltr\">src/app/hikes/new.tsx</code> و<code dir=\"ltr\">src/app/hikes/[id].tsx</code> معًا. أيّهما يُرسم للعنوان <code dir=\"ltr\">/hikes/new</code>؟</p>","options":[{"text":"<code dir=\"ltr\">[id].tsx</code>، مع <code dir=\"ltr\">id</code> بقيمة “new”، لأن المسارات الديناميكية تطابق أي شيء.","why":"المسارات الديناميكية تطابق فعلًا أي جزء، لكن الجزء الثابت الذي يطابق تمامًا هو المفضّل."},{"text":"الملف الذي أُنشئ أولًا.","why":"المطابقة لا تعتمد على عمر الملف. إنها ترتّب المسارات بحسب درجة تحديدها."},{"text":"لا هذا ولا ذاك؛ يُبلغ Expo Router عن تعارض عند بدء التشغيل.","why":"الإخوة الثابتون والديناميكيون إعداد طبيعي ومدعوم."},{"text":"<code dir=\"ltr\">new.tsx</code>، لأن الجزء الثابت يتغلّب على الجزء الديناميكي.","why":"صحيح. يطابق <code dir=\"ltr\">/hikes/new</code> المسار الثابت؛ أما <code dir=\"ltr\">/hikes/42</code> فيذهب إلى <code dir=\"ltr\">[id].tsx</code>."}],"answer":3},{"q":"<p>ينقر مستخدمون غير مسجّلين على رابط في بريد قديم إلى <code dir=\"ltr\">trailhead://hikes/42</code>. يقع مسار الرحلة داخل <code dir=\"ltr\">&lt;Stack.Protected guard={isSignedIn}&gt;</code>. ماذا يحدث؟</p>","options":[{"text":"تنفتح شاشة الرحلة، ثم تعرض خطأ فورًا.","why":"المسارات المحميّة لا تُرسم أصلًا ما دام الـ guard قيمته false، فلا تُركَّب شاشة الرحلة أبدًا."},{"text":"يصل المستخدم إلى أول شاشة متاحة (شاشة تسجيل الدخول في Trailhead) بدلًا من المسار المحمي.","why":"صحيح. ما دام الـ guard قيمته false يكون المسار غير متاح، ويُعاد توجيه التنقّل إليه."},{"text":"ينهار التطبيق لأن المسار غير موجود.","why":"المسارات المحميّة مخفيّة، لا محذوفة؛ ويتعامل Expo Router مع المحاولة بلطف."}],"answer":1},{"q":"<p>أضفت <code dir=\"ltr\">scheme</code> بقيمة trailhead إلى app.json وجرّبت <code dir=\"ltr\">trailhead://hikes/42</code> في Expo Go. لا شيء ينفتح. لماذا؟</p>","options":[{"text":"المخطّطات المخصّصة تُسجَّل في الإعدادات الأصلية للتطبيق، فتعمل في development build أو build إنتاجي من Trailhead، لا داخل Expo Go.","why":"صحيح. Expo Go تطبيق مختلف له مخطّطه الخاص؛ ابنِ Trailhead نفسه لتختبر روابطه."},{"text":"يجب أن تبدأ المخطّطات بـ <code dir=\"ltr\">https</code>.","why":"الـ universal links والـ App Links تستخدم https، لكن المخطّطات المخصّصة مثل <code dir=\"ltr\">trailhead://</code> صالحة أيضًا."},{"text":"الـ deep links لا تعمل إلا حين يكون التطبيق مفتوحًا أصلًا.","why":"يستطيع الـ deep link تشغيل التطبيق من الصفر وفتح المسار المطابق مباشرة."}],"answer":0}],"exercise":{"kind":"web","mode":"js","starter":{"js":"const ROUTES = [\n  '(tabs)/index',\n  '(tabs)/stats',\n  'settings/index',\n  'settings/units',\n  'hikes/new',\n  'hikes/[id]',\n  'hikes/[id]/photos',\n  'trails/[...path]',\n];\n\n// Return { route, params } for the best matching route file, or null.\nfunction matchRoute(url, routes) {\n  const path = url.replace(/^\\//, '');\n  const route = routes.find((r) => r === path);\n  return route ? { route, params: {} } : null;\n}\n\nconsole.log(JSON.stringify(matchRoute('/hikes/42?units=mi', ROUTES)));\n"},"solution":{"js":"const ROUTES = [\n  '(tabs)/index',\n  '(tabs)/stats',\n  'settings/index',\n  'settings/units',\n  'hikes/new',\n  'hikes/[id]',\n  'hikes/[id]/photos',\n  'trails/[...path]',\n];\n\nfunction segmentsOf(route) {\n  const segs = route.split('/').filter((s) => !/^\\(.+\\)$/.test(s));\n  if (segs[segs.length - 1] === 'index') segs.pop();\n  return segs;\n}\n\nfunction tryMatch(route, parts) {\n  const pattern = segmentsOf(route);\n  const params = {};\n  let rank = '';\n  for (let i = 0; i < pattern.length; i++) {\n    const seg = pattern[i];\n    const catchAll = seg.match(/^\\[\\.\\.\\.(\\w+)\\]$/);\n    const dynamic = seg.match(/^\\[(\\w+)\\]$/);\n    if (catchAll) {\n      const rest = parts.slice(i);\n      if (rest.length === 0) return null;\n      params[catchAll[1]] = rest;\n      return { route, params, rank: rank + '1' };\n    }\n    if (i >= parts.length) return null;\n    if (dynamic) {\n      params[dynamic[1]] = parts[i];\n      rank += '2';\n    } else if (seg === parts[i]) {\n      rank += '3';\n    } else {\n      return null;\n    }\n  }\n  return pattern.length === parts.length ? { route, params, rank } : null;\n}\n\nfunction matchRoute(url, routes) {\n  const [path, query = ''] = url.split('?');\n  const parts = path.split('/').filter(Boolean);\n  let best = null;\n  for (const route of routes) {\n    const m = tryMatch(route, parts);\n    if (m && (!best || m.rank > best.rank)) best = m;\n  }\n  if (!best) return null;\n  const params = { ...best.params };\n  for (const [key, value] of new URLSearchParams(query)) params[key] = value;\n  return { route: best.route, params };\n}\n\nconsole.log(JSON.stringify(matchRoute('/hikes/42?units=mi', ROUTES)));\n"},"checks":[{"id":"groups-index","test":"const R = ['(tabs)/index', '(tabs)/stats', 'settings/index', 'settings/units', 'hikes/new', 'hikes/[id]', 'hikes/[id]/photos', 'trails/[...path]']; const m = (u) => matchRoute(u, R); return typeof matchRoute === 'function' && m('/')?.route === '(tabs)/index' && m('/stats')?.route === '(tabs)/stats' && m('/settings')?.route === 'settings/index' && m('/settings/units')?.route === 'settings/units';"},{"id":"dynamic","test":"const R = ['(tabs)/index', '(tabs)/stats', 'settings/index', 'settings/units', 'hikes/new', 'hikes/[id]', 'hikes/[id]/photos', 'trails/[...path]']; const m = (u) => matchRoute(u, R); const a = m('/hikes/42'); const b = m('/hikes/42/photos'); return a?.route === 'hikes/[id]' && a.params.id === '42' && b?.route === 'hikes/[id]/photos' && b.params.id === '42';"},{"id":"static-wins","test":"const R = ['(tabs)/index', '(tabs)/stats', 'settings/index', 'settings/units', 'hikes/new', 'hikes/[id]', 'hikes/[id]/photos', 'trails/[...path]']; const m = (u) => matchRoute(u, R); return m('/hikes/new')?.route === 'hikes/new';"},{"id":"catch-all","test":"const R = ['(tabs)/index', '(tabs)/stats', 'settings/index', 'settings/units', 'hikes/new', 'hikes/[id]', 'hikes/[id]/photos', 'trails/[...path]']; const m = (u) => matchRoute(u, R); const a = m('/trails/alps/mont-blanc'); return a?.route === 'trails/[...path]' && Array.isArray(a.params.path) && a.params.path.join('/') === 'alps/mont-blanc' && m('/trails') === null;"},{"id":"query-slash","test":"const R = ['(tabs)/index', '(tabs)/stats', 'settings/index', 'settings/units', 'hikes/new', 'hikes/[id]', 'hikes/[id]/photos', 'trails/[...path]']; const m = (u) => matchRoute(u, R); const a = m('/hikes/7?units=mi'); return a?.route === 'hikes/[id]' && a.params.id === '7' && a.params.units === 'mi' && m('/stats/')?.route === '(tabs)/stats';"},{"id":"no-match","test":"const R = ['(tabs)/index', '(tabs)/stats', 'settings/index', 'settings/units', 'hikes/new', 'hikes/[id]', 'hikes/[id]/photos', 'trails/[...path]']; const m = (u) => matchRoute(u, R); return m('/nope') === null && m('/settings/units/extra') === null;"}],"title":"طابق deep link مع ملف مسار","prompt":"<p>اكتب <code dir=\"ltr\">matchRoute(url, routes)</code> بالطريقة التي يختار بها Expo Router شاشة لـ deep link. الـ <code dir=\"ltr\">routes</code> مسارات ملفات دون <code dir=\"ltr\">.tsx</code> (انظر <code dir=\"ltr\">ROUTES</code>). أرجع <code dir=\"ltr\">{ route, params }</code> لأفضل مطابقة، أو <code dir=\"ltr\">null</code>.</p>\n<ul>\n<li>المجموعات مثل <code dir=\"ltr\">(tabs)</code> و<code dir=\"ltr\">index</code> الأخير لا تضيف جزءًا إلى الـ URL.</li>\n<li><code dir=\"ltr\">[id]</code> يطابق جزءًا واحدًا بالضبط ويضيفه إلى <code dir=\"ltr\">params</code> نصًّا.</li>\n<li><code dir=\"ltr\">[...path]</code> يطابق جزءًا متبقّيًا <strong>أو أكثر</strong> ويضيفها مصفوفةً.</li>\n<li>حين تطابق عدّة مسارات، فضّل الأجزاء الثابتة على أجزاء <code dir=\"ltr\">[dynamic]</code>، و<code dir=\"ltr\">[dynamic]</code> على <code dir=\"ltr\">[...catchAll]</code>، مقارنًا من الجزء الأول.</li>\n<li>تجاهل الشرطة المائلة في النهاية؛ وأضف قيم الـ query string (<code dir=\"ltr\">?units=mi</code>) إلى <code dir=\"ltr\">params</code>.</li>\n</ul>\n","hints":["حوّل كل مسار إلى أجزاء الـ URL الخاصة به أولًا: أسقط أجزاء <code dir=\"ltr\">(group)</code> و<code dir=\"ltr\">index</code> الأخير. وقسّم مسار الـ URL باستخدام <code dir=\"ltr\">.split('/').filter(Boolean)</code>.","امشِ على أجزاء المسار بالتوازي مع أجزاء الـ URL. الجزء الحرفي يجب أن يساوي جزء الـ URL؛ و<code dir=\"ltr\">[x]</code> يلتقطه؛ و<code dir=\"ltr\">[...x]</code> يلتقط <code dir=\"ltr\">parts.slice(i)</code> وينهي المطابقة. وبعد ذلك يجب أن تتطابق الأطوال.","للترتيب، ابنِ نصًّا لكل مطابقة برقم واحد لكل جزء (3 للثابت، 2 للديناميكي، 1 للشامل) واحتفظ بالمطابقة ذات النص الأكبر. و<code dir=\"ltr\">new URLSearchParams(query)</code> يمرّ على أزواج الـ query."],"explanation":"<p>يوحّد الحل كل ملف مسار إلى أجزاء URL، ويحاول مطابقته جزءًا جزءًا بينما يجمع الـ params، ويعطي كل جزء درجة بحيث تفوز المسارات الأكثر تحديدًا: مقارنة نصوص الدرجات ترتّب الثابت قبل الديناميكي قبل الشامل من اليسار إلى اليمين. ولهذا يصل <code dir=\"ltr\">/hikes/new</code> إلى <code dir=\"ltr\">hikes/new.tsx</code> بينما يذهب <code dir=\"ltr\">/hikes/42</code> إلى <code dir=\"ltr\">hikes/[id].tsx</code>، ولهذا أيضًا يجب أن تظل الشاشة تتحقّق من <code dir=\"ltr\">id</code>: فأداة المطابقة تقبل أي جزء على الإطلاق.</p>\n","checkLabels":{"groups-index":"المجموعات وملفات index لا تضيف جزءًا إلى الـ URL","dynamic":"الأجزاء الديناميكية تُطابَق وتصبح params نصية","static-wins":"المسار الثابت يتغلّب على الديناميكي","catch-all":"المسارات الشاملة تجمع الأجزاء المتبقّية في مصفوفة","query-slash":"الـ query strings تصبح params والشرطات المائلة الأخيرة تُتجاهَل","no-match":"المسارات غير المعروفة أو الطويلة جدًا تُرجع null"}},"words":646,"readMinutes":4}