// Standalone web pages — browse, detail, become-provider, contact, sign-in // All consume the WebContext (set by WebApp). const WebContext = React.createContext(null); const useWeb = () => React.useContext(WebContext); // ─── Page header (used on subpages) ──────────────────────────────────── function WebPageHeader({ theme, lang, eyebrow, title, sub, tone = "camel" }) { const T = theme; const rtl = isRTL(lang); const tones = { camel: `linear-gradient(135deg, ${T.camel} 0%, ${T.camelDeep} 100%)`, ink: `linear-gradient(135deg, ${T.ink} 0%, ${T.inkSoft} 100%)`, brown: `linear-gradient(135deg, ${T.pastelBrown} 0%, ${T.camelDeep} 100%)`, }; return (
{eyebrow && (
{eyebrow}
)}

{title}

{sub && (

{sub}

)}
); } // ─── Browse / search results page ────────────────────────────────────── function WebBrowsePage({ theme, lang }) { const T = theme; const l = L[lang]; const rtl = isRTL(lang); const { setPage, setPkg, umrahSearch } = useWeb(); const [filtersOpen, setFiltersOpen] = React.useState(false); const basePackages = [ { id: "fairmont", hotel: rtl ? "فيرمونت ساعة مكة" : "Fairmont Makkah Clock Tower", dist: rtl ? "50م من الحرم" : "50m from Haram", stars: 5, nights: 7, price: 4899, was: 5499, seats: 4, board: rtl ? "إفطار+عشاء" : "B&D", tone: "dark", provider: rtl ? "الإيلاف الفاخرة" : "Elaf Premium", dates: rtl ? "16–25 رمضان" : "16–25 Ramadan", }, { id: "swiss", hotel: rtl ? "سويس أوتيل برج الزمزم" : "Swissôtel Al Maqam", dist: rtl ? "120م من الحرم" : "120m from Haram", stars: 5, nights: 7, price: 3899, seats: 9, board: rtl ? "إفطار" : "Breakfast", tone: "camel", provider: rtl ? "الرحمة للحج" : "Al-Rahma", dates: rtl ? "1–8 شوال" : "1–8 Shawwal", }, { id: "hilton", hotel: rtl ? "هيلتون مكة المكرمة" : "Hilton Suites Makkah", dist: rtl ? "200م من الحرم" : "200m from Haram", stars: 4, nights: 7, price: 2799, seats: 12, board: rtl ? "بدون" : "Room only", tone: "brown", provider: rtl ? "إيلاف ضيوف" : "Elaf Guests", dates: rtl ? "20 رمضان" : "20 Ramadan", }, { id: "safwah", hotel: rtl ? "الصفوة رويال أوركيد" : "Al Safwah Royal Orchid", dist: rtl ? "320م" : "320m", stars: 4, nights: 6, price: 1199, seats: 9, board: rtl ? "إفطار" : "Breakfast", tone: "cream", provider: rtl ? "حجاج الجزيرة" : "Hujjaj Aljazeera", dates: rtl ? "8 ذو القعدة" : "8 Dhul Qadah", }, { id: "fairmont2", hotel: rtl ? "فيرمونت — شوال" : "Fairmont — Shawwal", dist: "50m", stars: 5, nights: 10, price: 5499, seats: 2, board: rtl ? "إفطار+عشاء" : "B&D", tone: "dark", provider: rtl ? "الإيلاف الفاخرة" : "Elaf Premium", dates: rtl ? "12 شوال" : "12 Shawwal", }, { id: "movenpick", hotel: rtl ? "موڤنبيك حجاج" : "Mövenpick Hajar", dist: "180m", stars: 5, nights: 10, price: 5290, seats: 7, board: rtl ? "إفطار" : "Breakfast", tone: "brown", provider: rtl ? "بوابة الكعبة" : "Kaaba Gate", dates: rtl ? "20 شوال" : "20 Shawwal", }, ]; const [approved, setApproved] = React.useState([]); React.useEffect(() => { fetch(`${window.DUYUF_API_URL}/v1/public/packages`) .then((r) => r.json()) .then((body) => { if (!body.ok) return; setApproved( (body.packages || []).map((p, i) => ({ id: p.id, hotel: p.hotel_name, dist: rtl ? "من مزوّد موثوق" : "Verified provider", stars: 5, nights: p.duration_days, price: Number(p.price_per_person), seats: p.seats, board: rtl ? "حسب الباقة" : "As listed", tone: ["camel", "brown", "cream", "dark"][i % 4], provider: p.business_name, providerLogoUrl: p.provider_logo_key ? `${window.DUYUF_API_URL}/v1/public/package-images/${encodeURIComponent(p.provider_logo_key)}` : "", dates: `${p.available_from} — ${p.available_to}`, title: p.title, imageUrl: p.cover_key ? `${window.DUYUF_API_URL}/v1/public/package-images/${encodeURIComponent(p.cover_key)}` : JSON.parse(p.hotel_images_json || "[]")[0] || "", inclusions: JSON.parse(p.inclusions || "[]"), exclusions: JSON.parse(p.exclusions || "[]"), originCity: p.origin_city, destinationCity: p.destination_city, notes: p.notes || "", occupancyPrices: JSON.parse(p.occupancy_prices_json || "{}"), roomOptions: JSON.parse(p.room_options_json || "[]"), cancellationPolicy: p.cancellation_policy || "", supportsTabby: Boolean(p.supports_tabby), supportsTamara: Boolean(p.supports_tamara), paymentConfirmationMode: p.payment_confirmation_mode || "pay_on_arrival", availableSeats: Math.max(0,Number(p.seats||0)-Number(p.booked_travelers||0)), departureAt:p.departure_at, returnAt:p.return_at, travelMode:p.travel_mode, airline:p.airline, flightNumber:p.flight_number, flightArrivalCity:p.flight_arrival_city, returnAirline:p.return_airline, returnFlightNumber:p.return_flight_number, returnOriginCity:p.return_origin_city, returnDestinationCity:p.return_destination_city, roomType:p.room_type, mealPlan:p.meal_plan, isDynamic: true, })), ); }) .catch(() => {}); }, [lang]); const normalizeCity = (value) => String(value || "") .replace("مكة المكرمة", "مكة") .replace("المدينة المنورة", "المدينة") .trim(); const matchesSearch = (p) => { if (!p.isDynamic) return ( normalizeCity(umrahSearch?.origin) === "جدة" && normalizeCity(umrahSearch?.destination) === "مكة" ); const originMatches = !umrahSearch?.origin || normalizeCity(p.originCity) === normalizeCity(umrahSearch.origin); const destinationMatches = !umrahSearch?.destination || normalizeCity(p.destinationCity) === normalizeCity(umrahSearch.destination); const seatsMatch = Number(p.seats || 0) >= Number(umrahSearch?.travelers || 1); return originMatches && destinationMatches && seatsMatch; }; const packages = approved.filter(matchesSearch); const routeLabel = `${rtl ? umrahSearch?.origin : { جدة: "Jeddah", الرياض: "Riyadh", "المدينة المنورة": "Madinah", الدمام: "Dammam", الطائف: "Taif" }[umrahSearch?.origin] || umrahSearch?.origin} → ${rtl ? umrahSearch?.destination : { "مكة المكرمة": "Makkah", "المدينة المنورة": "Madinah" }[umrahSearch?.destination] || umrahSearch?.destination}`; return ( <> {/* Compact search re-bar */}
{routeLabel}
{umrahSearch?.date || (rtl ? "أي موعد" : "Any date")}
{umrahSearch?.travelers || 1} {rtl ? "مسافرين" : "travelers"}
{filtersOpen && (
{/* Price range */}
{rtl ? "السعر" : "Price (SAR)"}
1,200 5,500
{/* Stars */}
{rtl ? "تقييم الفندق" : "Hotel rating"}
{[5, 4, 3].map((n, i) => ( ))}
{/* Distance from Haram */}
{rtl ? "المسافة من الحرم" : "Distance from Haram"}
{[ rtl ? "أقل من 100م" : "Within 100m", rtl ? "100–300م" : "100–300m", rtl ? "300–500م" : "300–500m", rtl ? "أكثر من 500م" : "Over 500m", ].map((d, i) => ( ))}
{/* Board */}
{rtl ? "الإقامة" : "Board"}
{[ rtl ? "بدون" : "Room only", rtl ? "إفطار" : "Breakfast", rtl ? "نصف إقامة" : "Half board", rtl ? "كاملة" : "Full board", ].map((b, i) => (
); } // ─── Package detail page ─────────────────────────────────────────────── function WebDetailPage({ theme, lang }) { const T = theme; const l = L[lang]; const rtl = isRTL(lang); const { pkg, setPage } = useWeb(); const p = pkg || { hotel: rtl ? "فيرمونت ساعة مكة" : "Fairmont Makkah Clock Royal", provider: rtl ? "الإيلاف الفاخرة" : "Elaf Premium", price: 4899, was: 5499, dates: rtl ? "16–25 رمضان" : "16–25 Ramadan", nights: 7, dist: rtl ? "50م من الحرم" : "50m from Haram", stars: 5, tone: "dark", board: rtl ? "إفطار+عشاء" : "B&D", seats: 4, }; return (
{/* Breadcrumb */}
{/* Gallery */}
{p.imageUrl ? ( {p.hotel} ) : ( )}
{rtl ? "+8 صور" : "+8 photos"}
{/* Main column */}

{p.hotel}

4.8 · 312 {rtl ? "تقييم" : "reviews"} · {p.dist}
{/* Stats strip */}
{[ { i: "calendar", t: p.dates, l: rtl ? "الموعد" : "Window" }, { i: "bed", t: `${p.nights} ${rtl ? "ليالٍ" : "nights"}`, l: rtl ? "الإقامة" : "Stay", }, { i: p.isDynamic ? "user" : "plane", t: p.isDynamic ? `${p.seats} ${rtl ? "شخص" : "people"}` : rtl ? "مع طيران" : "Flight included", l: p.isDynamic ? rtl ? "السعة" : "Capacity" : rtl ? "السفر" : "Travel", }, { i: "coffee", t: p.isDynamic ? `${p.inclusions?.length || 0}` : p.board, l: p.isDynamic ? rtl ? "مشتملات محددة" : "Selected items" : rtl ? "الوجبات" : "Meals", }, ].map((m, i) => (
{m.t} {m.l}
))}
{/* Description */}

{rtl ? "عن الباقة" : "About this package"}

{p.isDynamic ? p.notes || (rtl ? "باقة مقدمة من مزود معتمد. راجع المشتملات والتواريخ قبل الحجز." : "A package from a verified provider. Review inclusions and dates before booking.") : rtl ? "باقة عمرة كاملة تشمل الطيران والإقامة في فيرمونت ساعة مكة على بُعد 50 متراً فقط من الحرم. الإقامة شاملة الإفطار والعشاء يومياً، ونقل من المطار، ومرشد ميداني طوال فترة الإقامة. الباقة مجدولة مسبقاً من المزوّد ولا تتطلب اختيار مقعد." : "A complete Umrah package including flight and stay at Fairmont Makkah Clock Royal — just 50m from the Haram. Includes daily breakfast & dinner, airport transfers, and an on-site guide throughout your stay. Pre-arranged by the provider; no seat selection required."}

{/* What's included */}

{rtl ? "ما يشمله" : "What's included"}

{(p.isDynamic ? (p.inclusions || []).map((t) => ({ i: "check", t })) : [ { i: "plane", t: rtl ? "تذكرة طيران ذهاب وعودة" : "Round-trip flight ticket", }, { i: "bed", t: rtl ? "6 ليالٍ في فيرمونت" : "6 nights at Fairmont", }, { i: "coffee", t: rtl ? "إفطار وعشاء يومياً" : "Daily breakfast & dinner", }, { i: "car", t: rtl ? "نقل من المطار" : "Airport transfers", }, { i: "compass", t: rtl ? "مرشد ميداني" : "On-site guide" }, { i: "shield", t: rtl ? "تأمين سفر" : "Travel insurance" }, ] ).map((it, i) => (
{it.t}
))} {p.isDynamic && !p.inclusions?.length && (
{rtl ? "لم يحدد المزود مشتملات الباقة بعد." : "The provider has not selected inclusions yet."}
)}
{p.exclusions?.length > 0 && ( <>

{rtl ? "ما لا تشمله الباقة" : "Not included"}

{p.exclusions.map((text, i) => (
× {text}
))}
)} {/* Provider card */}

{rtl ? "عن المزوّد" : "About the provider"}

{" "} {rtl ? "مزوّد موثّق · منذ 2019" : "Verified provider · since 2019"}
142 {rtl ? "باقة نشطة · 4.7★" : "active packages · 4.7★"}
{/* Sticky booking panel */}
); } function WebCustomerBookingsPage({theme,lang}){ const T=theme,rtl=isRTL(lang),{setPage}=useWeb(); const [user,setUser]=React.useState(()=>firebase.auth().currentUser),[phone,setPhone]=React.useState(""),[otp,setOtp]=React.useState(""),[step,setStep]=React.useState(firebase.auth().currentUser?"bookings":"phone"),[bookings,setBookings]=React.useState([]),[tab,setTab]=React.useState("upcoming"),[busy,setBusy]=React.useState(false),[message,setMessage]=React.useState(""); const verifying=React.useRef(false); const load=async(current=user||firebase.auth().currentUser)=>{if(!current)return;setBusy(true);try{const token=await current.getIdToken(true),r=await fetch(`${window.DUYUF_API_URL}/v1/customer/bookings`,{headers:{authorization:`Bearer ${token}`}}),b=await r.json();if(!r.ok)throw new Error(b.error);setBookings(b.bookings||[]);localStorage.setItem("duyuf_dashboard_mode","customer");localStorage.setItem("duyuf_customer",JSON.stringify({phone:b.phone}));setStep("bookings");}catch(_){setMessage(rtl?"تعذر تحميل الحجوزات.":"Could not load bookings.");}finally{setBusy(false);}}; React.useEffect(()=>{if(user)load(user);},[]); const send=async()=>{const digits=phone.replace(/\D/g,"").replace(/^966/,"").replace(/^0/,"");if(!/^5\d{8}$/.test(digits)){setMessage(rtl?"أدخل رقم جوال صحيحًا.":"Enter a valid mobile number.");return;}setBusy(true);setMessage("");try{if(window.duyufCustomerRecaptcha)window.duyufCustomerRecaptcha.clear();window.duyufCustomerRecaptcha=new firebase.auth.RecaptchaVerifier("duyuf-customer-recaptcha",{size:"invisible"});window.duyufCustomerConfirmation=await firebase.auth().signInWithPhoneNumber(`+966${digits}`,window.duyufCustomerRecaptcha);setPhone(digits);setStep("otp");}catch(_){setMessage(rtl?"تعذر إرسال رمز التحقق.":"Could not send verification code.");}finally{setBusy(false);}}; const verify=async(value=otp)=>{if(verifying.current||!/^\d{6}$/.test(value))return;verifying.current=true;setBusy(true);try{const credential=await window.duyufCustomerConfirmation.confirm(value);setUser(credential.user);await load(credential.user);}catch(_){setMessage(rtl?"الرمز غير صحيح أو انتهت صلاحيته.":"Invalid or expired code.");}finally{verifying.current=false;setBusy(false);}}; const groups={upcoming:bookings.filter(b=>["confirmed","checked_in"].includes(b.status)),past:bookings.filter(b=>["checked_out","completed"].includes(b.status)),cancelled:bookings.filter(b=>["cancelled","no_show"].includes(b.status))},shown=groups[tab]||[]; const statusLabel=s=>s==="confirmed"?(rtl?"مؤكد":"Confirmed"):s==="checked_in"?(rtl?"أثناء الرحلة":"In progress"):s==="checked_out"?(rtl?"مكتمل":"Completed"):s==="cancelled"?(rtl?"ملغي من المزود":"Cancelled by provider"):s==="no_show"?(rtl?"لم يحضر":"No-show"):s; const download=async b=>{const r=await fetch(`${window.DUYUF_API_URL}/v1/public/bookings/${encodeURIComponent(b.booking_reference)}/pdf?token=${encodeURIComponent(b.public_token)}`),blob=await r.blob(),u=URL.createObjectURL(blob),a=document.createElement("a");a.href=u;a.download=`Duyuf-${b.booking_reference}.pdf`;a.click();setTimeout(()=>URL.revokeObjectURL(u),30000);}; if(step!=="bookings")return

{rtl?"الدخول إلى حجوزاتي":"Sign in to my bookings"}

{rtl?"استخدم رقم الجوال المرتبط بالحجز.":"Use the mobile number linked to your booking."}

{step==="phone"?
+966setPhone(e.target.value.replace(/\D/g,"").slice(0,10))} placeholder="5xxxxxxxx" style={{flex:1,border:0,padding:14,fontSize:16}}/>
:
{rtl?<>أرسلنا رمزًا إلى +966 {phone}:<>Code sent to +966 {phone}}
{const n=e.target.value.replace(/\D/g,"").slice(0,6);setOtp(n);if(n.length===6)setTimeout(()=>verify(n),0);}} style={{minHeight:54,textAlign:"center",fontSize:24,letterSpacing:8,direction:"ltr"}}/>
}{message&&

{message}

}
; return

{rtl?"إدارة حجوزاتي":"Manage my bookings"}

{rtl?"تابع رحلاتك، السداد ومستندات التأكيد من مكان واحد.":"Track trips, payments and confirmation documents."}

{[["upcoming",rtl?"القادمة":"Upcoming"],["past",rtl?"السابقة":"Past"],["cancelled",rtl?"الملغاة":"Cancelled"]].map(([id,label])=>)}
{busy?

{rtl?"جارٍ تحميل الحجوزات…":"Loading bookings…"}

:shown.length===0?

{rtl?"لا توجد حجوزات في هذا القسم":"No bookings here"}

{rtl?"أي حجز مرتبط بهذا الرقم سيظهر هنا تلقائيًا.":"Bookings linked to this phone appear automatically."}

:
{shown.map(b=>{const p=b.package||{};return
{statusLabel(b.status)}

{p.title||b.title}

{p.hotel||b.hotel_name} · {p.provider||b.business_name}

{rtl?"رقم الحجز":"Booking reference"}{b.booking_reference}
{rtl?"التاريخ":"Dates"}{String(p.departureAt||b.available_from||"—").slice(0,10)} - {String(p.returnAt||b.available_to||"—").slice(0,10)}
{rtl?"المسافرون":"Travelers"}{b.travelers}
{rtl?"الغرف":"Rooms"}{(b.rooms||[]).reduce((s,r)=>s+Number(r.count||0),0)}
{rtl?"الإجمالي / المتبقي":"Total / due"}{Number(b.total_amount).toLocaleString()} {b.currency}
{rtl?"السداد":"Payment"}{rtl?"عند الوصول":"Pay on arrival"}
})}
}
; } function WebCheckoutPage({ theme, lang }) { const T = theme, rtl = isRTL(lang), { pkg, setPage, setBookingConfirmation } = useWeb(); const [step, setStep] = React.useState("details"); const [form, setForm] = React.useState({ fullName: "", email: "", phone: "", adults: 1, children: 0, infants: 0, rooms:{ single:0,double:1,triple:0,quad:0 }, roomPreference:"" }); const [firebaseIdToken, setFirebaseIdToken] = React.useState(""); const [providerSessionToken,setProviderSessionToken]=React.useState(""); const [adminSessionToken,setAdminSessionToken]=React.useState(""); const [otp, setOtp] = React.useState(""); const otpVerifyingRef = React.useRef(false); const [busy, setBusy] = React.useState(false), [message, setMessage] = React.useState(""); const totalTravelers = form.adults + form.children + form.infants; const roomMeta = { single:{capacity:1,ar:"مفردة",en:"Single"}, double:{capacity:2,ar:"مزدوجة",en:"Double"}, triple:{capacity:3,ar:"ثلاثية",en:"Triple"}, quad:{capacity:4,ar:"رباعية",en:"Quad"} }; const availableRooms = Object.entries(roomMeta).map(([key,meta])=>({key,...meta,price:Number(pkg?.occupancyPrices?.[key]||0)})).filter(x=>x.price>0); const selectedRooms = availableRooms.map(x=>({...x,count:Number(form.rooms?.[x.key]||0)})).filter(x=>x.count>0); const roomCapacity = selectedRooms.reduce((sum,x)=>sum+x.capacity*x.count,0); const roomCount = selectedRooms.reduce((sum,x)=>sum+x.count,0); const estimatedTotal = selectedRooms.reduce((sum,x)=>sum+x.price*x.count,0); const fieldStyle = { width: "100%", minHeight: 52, boxSizing: "border-box", border: `1px solid ${T.hairlineStrong}`, borderRadius: 13, padding: "0 14px", background: T.surfaceAlt, color: T.text, font: "500 14px Alexandria", outline: "none" }; const requestCode = async () => { if (form.fullName.trim().length < 3 || (form.email.trim() && !/^\S+@\S+\.\S+$/.test(form.email)) || !/^5\d{8}$/.test(form.phone)) { setMessage(rtl ? "أدخل الاسم الكامل ورقم جوال صحيحًا. البريد الإلكتروني اختياري." : "Enter your full name and a valid mobile number. Email is optional."); return; } if (!roomCount || roomCapacity < totalTravelers) { setMessage(rtl?`اختر غرفًا تستوعب ${totalTravelers} أشخاص على الأقل.`:`Select rooms for at least ${totalTravelers} guests.`); return; } if (Number(pkg?.availableSeats ?? pkg?.seats ?? 0) < totalTravelers) { setMessage(rtl?`لا توجد سعة كافية. المتاح حاليًا ${Number(pkg?.availableSeats||0)} فقط. اختر عددًا أقل أو باقة أخرى.`:`Not enough capacity. Only ${Number(pkg?.availableSeats||0)} places remain.`); return; } setBusy(true); setMessage(""); try { const currentUser=firebase.auth().currentUser,currentPhone=String(currentUser?.phoneNumber||"").replace(/\D/g,"").replace(/^966/,""); if(currentUser&¤tPhone===form.phone){ const idToken=await currentUser.getIdToken(true); localStorage.setItem("duyuf_customer",JSON.stringify({phone:currentUser.phoneNumber})); setFirebaseIdToken(idToken);setStep("review");return; } if(currentUser&¤tPhone!==form.phone){ throw new Error("different_signed_in_phone"); } const storedSession=localStorage.getItem("duyuf_provider_session")||""; if(!currentUser&&storedSession){ const sessionResponse=await fetch(`${window.DUYUF_API_URL}/v1/provider/packages`,{headers:{authorization:`Bearer ${storedSession}`}}); const sessionBody=await sessionResponse.json().catch(()=>({})); const sessionPhone=String(sessionBody?.provider?.phone||"").replace(/\D/g,"").replace(/^966/,""); if(sessionResponse.ok&&sessionPhone===form.phone){setProviderSessionToken(storedSession);setStep("review");return;} } const storedAdminSession=localStorage.getItem("duyuf_admin_session")||""; const storedAdmin=(()=>{try{return JSON.parse(localStorage.getItem("duyuf_admin")||"null");}catch(_){return null;}})(); const storedAdminPhone=String(storedAdmin?.phone||"").replace(/\D/g,"").replace(/^966/,""); if(!currentUser&&storedAdminSession&&storedAdminPhone===form.phone){setAdminSessionToken(storedAdminSession);setStep("review");return;} if (window.duyufCheckoutRecaptcha) window.duyufCheckoutRecaptcha.clear(); const recaptchaHost=document.getElementById("duyuf-checkout-recaptcha"); if(recaptchaHost)recaptchaHost.replaceChildren(); firebase.auth().languageCode=rtl?"ar":"en"; window.duyufCheckoutRecaptcha = new firebase.auth.RecaptchaVerifier("duyuf-checkout-recaptcha",{size:"invisible"}); await window.duyufCheckoutRecaptcha.render(); window.duyufCheckoutConfirmation = await firebase.auth().signInWithPhoneNumber(`+966${form.phone}`,window.duyufCheckoutRecaptcha); setStep("otp"); } catch (error) { console.error("Checkout OTP failed",error); const errorCode=String(error?.code||error?.message||"unknown").replace(/^auth\//,""); setMessage(error.message==="different_signed_in_phone"?(rtl?"أنت مسجل الدخول برقم جوال مختلف. استخدم الرقم المسجل حاليًا أو سجل الخروج أولًا.":"You are signed in with a different mobile number. Use it or sign out first."):(rtl ? `تعذر إرسال رمز التحقق (${errorCode}).` : `Could not send the verification code (${errorCode}).`)); } finally { setBusy(false); } }; const verifyCode = async (codeValue=otp) => { if (otpVerifyingRef.current) return; if (!/^\d{6}$/.test(codeValue)) { setMessage(rtl ? "أدخل رمز التحقق المكوّن من 6 أرقام." : "Enter the 6-digit code."); return; } otpVerifyingRef.current=true; setBusy(true); setMessage(""); try { if (!window.duyufCheckoutConfirmation) throw new Error("missing_confirmation"); const credential = await window.duyufCheckoutConfirmation.confirm(codeValue); const idToken = await credential.user.getIdToken(true); localStorage.setItem("duyuf_customer",JSON.stringify({phone:credential.user.phoneNumber})); setFirebaseIdToken(idToken); setStep("review"); } catch (_) { setMessage(rtl ? "رمز التحقق غير صحيح أو انتهت صلاحيته." : "The verification code is invalid or expired."); } finally { setBusy(false); otpVerifyingRef.current=false; } }; const confirmBooking = async () => { if (pkg?.paymentConfirmationMode === "prepay_required") { setMessage(rtl ? "يتطلب هذا العرض الدفع مسبقًا لتأكيد الحجز. سيتم إتاحة المتابعة عند تفعيل بوابة الدفع." : "This offer requires prepayment. Checkout will be available when online payment is enabled."); return; } setBusy(true); setMessage(""); try { const r = await fetch(`${window.DUYUF_API_URL}/v1/public/bookings`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ packageId: pkg?.id, ...form, phone: `+966${form.phone}`, firebaseIdToken,providerSessionToken,adminSessionToken }) }); const b = await r.json(); if (!r.ok || !b.booking) throw new Error(b.error); localStorage.setItem("duyuf_last_booking", JSON.stringify(b.booking)); setBookingConfirmation(b.booking); setPage("booking-confirmation"); } catch (error) { setMessage(error.message === "insufficient_capacity" ? (rtl ? "لا توجد سعة كافية لهذا العدد." : "Not enough capacity.") : (rtl ? "تعذر إتمام الحجز. حاول مرة أخرى." : "Could not complete the booking.")); } finally { setBusy(false); } }; const counter = (key, label, min = 0) =>
{label}
{form[key]}
; const steps = [["details", rtl ? "البيانات" : "Details"], ["otp", rtl ? "التحقق" : "Verify"], ["review", rtl ? "المراجعة" : "Review"]]; return (
{steps.map(([id,label],i) => { const current = steps.findIndex(x => x[0] === step); return
{label}
; })}

{step === "details" ? (rtl ? "بيانات الحجز" : "Booking details") : step === "otp" ? (rtl ? "تحقق من رقم الجوال" : "Verify mobile") : (rtl ? "راجع طلبك" : "Review booking")}

{pkg?.title} · {pkg?.hotel}

{step === "details" &&
setForm(v=>({...v,fullName:e.target.value}))} style={fieldStyle}/>setForm(v=>({...v,email:e.target.value}))} style={fieldStyle}/>
{counter("adults",rtl?"البالغون":"Adults",1)}{counter("children",rtl?"الأطفال":"Children")}{counter("infants",rtl?"الرضّع":"Infants")}
{rtl?"الغرف المطلوبة":"Required rooms"}{availableRooms.map(room=>
{rtl?`غرفة ${room.ar}`:room.en} · {room.price.toLocaleString()} SAR
{form.rooms?.[room.key]||0}
)}{pkg?.roomOptions?.length>0&&}
{rtl?`${roomCount} غرفة · سعة ${roomCapacity} أشخاص`:`${roomCount} rooms · capacity ${roomCapacity}`}{estimatedTotal.toLocaleString()} SAR
} {step === "otp" &&
{rtl?<>أرسلنا رمزًا إلى +966 {form.phone}:<>We sent a code to +966 {form.phone}}
{const next=e.target.value.replace(/\D/g,"").slice(0,6);setOtp(next);if(next.length===6)setTimeout(()=>verifyCode(next),0);}} placeholder="• • • • • •" style={{...fieldStyle,textAlign:"center",fontSize:24,letterSpacing:8,direction:"ltr"}}/>
} {step === "review" &&
{form.fullName}+966 {form.phone}{form.email?` · ${form.email}`:""}{rtl?"المسافرون":"Travelers"}: {form.adults} {rtl?"بالغ":"adult"} · {form.children} {rtl?"طفل":"child"} · {form.infants} {rtl?"رضيع":"infant"}{rtl?"الغرف":"Rooms"}: {selectedRooms.map(x=>`${x.count}× ${rtl?x.ar:x.en}`).join(" · ")}{form.roomPreference&&{rtl?"تفضيل الغرفة":"Room preference"}: {form.roomPreference}}{rtl?"الفندق":"Hotel"}: {pkg?.hotel||"—"}{rtl?"الإقامة":"Stay"}: {pkg?.nights||pkg?.durationDays||"—"} {rtl?"ليالٍ":"nights"} · {pkg?.mealPlan||"—"}{rtl?"مسار الرحلة":"Route"}: {pkg?.originCity||"—"} ← {pkg?.destinationCity||pkg?.flightArrivalCity||"—"}{rtl?"موعد الانطلاق":"Departure"}: {pkg?.departureAt?new Date(pkg.departureAt).toLocaleString(rtl?"ar-SA":"en-US"):"—"}{rtl?"موعد العودة":"Return"}: {pkg?.returnAt?new Date(pkg.returnAt).toLocaleString(rtl?"ar-SA":"en-US"):"—"}{pkg?.flightNumber&&{rtl?"رحلة الذهاب":"Outbound flight"}: {pkg.airline||"—"} · {pkg.flightNumber}}{pkg?.returnFlightNumber&&{rtl?"رحلة العودة":"Return flight"}: {pkg.returnAirline||"—"} · {pkg.returnFlightNumber}}{rtl?"إجمالي الحجز":"Booking total"}: {estimatedTotal.toLocaleString()} SAR{rtl?"طريقة السداد":"Payment method"}: {pkg?.paymentConfirmationMode==="prepay_required"?(rtl?"الدفع مسبقًا لتأكيد الحجز":"Prepayment required"):(rtl?"الدفع عند الوصول":"Pay on arrival")}{pkg?.cancellationPolicy&&{rtl?"سياسة الإلغاء":"Cancellation policy"}: {pkg.cancellationPolicy}}
{rtl?"بعد إتمام الحجز سيصدر رقم التأكيد ويمكنك تنزيل نموذج PDF الرسمي.":"After completion, a confirmation number and official PDF will be available."}
{pkg?.paymentConfirmationMode==="prepay_required"&&

{rtl?"يتطلب هذا العرض الدفع قبل إصدار تأكيد الحجز.":"Payment is required before this booking can be confirmed."}

}
} {message &&

{message}

}
); } function WebBookingConfirmationPage({ theme, lang }) { const T = theme, rtl = isRTL(lang), { bookingConfirmation, setPage } = useWeb(); const booking = bookingConfirmation || (() => { try { return JSON.parse(localStorage.getItem("duyuf_last_booking") || "null"); } catch (_) { return null; } })(); const [busy, setBusy] = React.useState(false); const voucherRef = React.useRef(null); if (!booking) return
; const p = booking.package || {}; const formatDate = value => value ? new Date(value).toLocaleString(rtl ? "ar-SA" : "en-US", { dateStyle:"medium", timeStyle:value.includes?.("T")?"short":undefined }) : "—"; const downloadPdf = async () => { setBusy(true); try { const url=`${window.DUYUF_API_URL}/v1/public/bookings/${encodeURIComponent(booking.bookingReference)}/pdf?token=${encodeURIComponent(booking.publicToken)}`; const response=await fetch(url); if(!response.ok) throw new Error("pdf_failed"); const blob=await response.blob(),objectUrl=URL.createObjectURL(blob),link=document.createElement("a"); link.href=objectUrl;link.download=`Duyuf-${booking.bookingReference}.pdf`;document.body.appendChild(link);link.click();link.remove();setTimeout(()=>URL.revokeObjectURL(objectUrl),30000); } finally { setBusy(false); } }; const row = (label,value) =>
{label}{value || "—"}
; const milestones = p.travelMode === "flight" ? [ { title:rtl?"رحلة الذهاب":"Outbound flight", detail:`${rtl?"من":"From"} ${p.originCity||"—"} ${rtl?"إلى":"to"} ${p.flightArrivalCity||"—"} · ${p.airline||"—"} · ${p.flightNumber||"—"}`, date:formatDate(p.departureAt) }, { title:rtl?"الوصول والانتقال إلى الفندق":"Arrival and hotel transfer", detail:p.airportTransferType||"—", date:p.flightArrivalCity||"—" }, { title:rtl?"الإقامة":"Hotel stay", detail:`${p.hotel||"—"} · ${p.roomType||"—"}`, date:`${p.durationDays||"—"} ${rtl?"أيام":"days"}` }, { title:rtl?"المغادرة من الفندق":"Hotel checkout", detail:p.hotel||"—", date:formatDate(p.returnAt) }, { title:rtl?"رحلة العودة":"Return flight", detail:`${rtl?"من":"From"} ${p.returnOriginCity||"—"} ${rtl?"إلى":"to"} ${p.returnDestinationCity||"—"} · ${p.returnAirline||"—"} · ${p.returnFlightNumber||"—"}`, date:formatDate(p.returnDepartureAt) }, ] : [ { title:rtl?"الانطلاق بالنقل البري":"Ground transport departure", detail:`${rtl?"من":"From"} ${p.originCity||"—"} ${rtl?"إلى":"to"} ${p.destinationCity||"—"} · ${p.busType||"—"}`, date:formatDate(p.departureAt) }, { title:rtl?"الوصول والإقامة":"Arrival and stay", detail:`${p.hotel||"—"} · ${p.roomType||"—"}`, date:`${p.durationDays||"—"} ${rtl?"أيام":"days"}` }, { title:rtl?"العودة":"Return", detail:p.destinationCity||"—", date:formatDate(p.returnAt) }, ]; return
✓ {rtl?"تم تأكيد حجزك":"Booking confirmed"}

{p.title}

{p.provider} · {p.hotel}

{booking.bookingReference}

{rtl?"ملخص الحجز":"Booking summary"}

{rtl?"الاسم الكامل":"Full name"}{booking.customerName}
{rtl?"رقم الجوال":"Mobile"}{booking.customerPhone}
{rtl?"المسافرون":"Travelers"}{booking.adults} {rtl?"بالغ":"adult"} · {booking.children} {rtl?"طفل":"child"}
{rtl?"الغرف":"Rooms"}{booking.rooms?.map(x=>`${x.count}× ${rtl?({single:"مفردة",double:"مزدوجة",triple:"ثلاثية",quad:"رباعية"}[x.type]||x.type):x.type}`).join(" · ")}
{rtl?"طريقة السداد":"Payment"}{rtl?"الدفع عند الوصول":"Pay on arrival"}
{rtl?"تاريخ التأكيد":"Confirmed"}{new Date(booking.confirmedAt).toLocaleDateString(rtl?"ar-SA":"en-US")}

{rtl?"برنامج الرحلة":"Trip itinerary"}

{milestones.map((m,i)=>
{m.title}{m.detail}{m.date}
)}
{rtl?"إجمالي الحجز":"Total"}{Number(booking.totalAmount).toLocaleString()} SAR
{rtl?"المبلغ المتبقي":"Amount due"}{Number(booking.balanceAmount??booking.totalAmount).toLocaleString()} SAR
DUYUF · {rtl?"رحلة موثوقة إلى بيت الله":"A trusted journey"}
✓ {rtl?"حجز مؤكد":"Confirmed"}
{booking.bookingReference}

{rtl?"تأكيد حجز باقة عمرة":"Umrah Package Booking Confirmation"}

{p.title} · {p.provider}

{row(rtl?"الاسم الكامل":"Full name",booking.customerName)}{row(rtl?"رقم الجوال":"Mobile",booking.customerPhone)}{booking.customerEmail?row(rtl?"البريد الإلكتروني":"Email",booking.customerEmail):null}{row(rtl?"المسافرون":"Travelers",`${booking.travelers} (${booking.adults}/${booking.children}/${booking.infants})`)}{booking.rooms?.length?row(rtl?"الغرف":"Rooms",booking.rooms.map(x=>`${x.count}× ${rtl?({single:"مفردة",double:"مزدوجة",triple:"ثلاثية",quad:"رباعية"}[x.type]||x.type):x.type}`).join(" · ")):null}{row(rtl?"طريقة السداد":"Payment method",rtl?"الدفع عند الوصول":"Pay on arrival")}{row(rtl?"تاريخ التأكيد":"Confirmed",new Date(booking.confirmedAt).toLocaleString(rtl?"ar-SA":"en-US"))}

{rtl?"تفاصيل الباقة":"Package details"}

{row(rtl?"الفندق":"Hotel",p.hotel)}{row(rtl?"نوع الغرفة":"Room",p.roomType)}{row(rtl?"الإعاشة":"Meals",p.mealPlan)}{row(rtl?"المدة":"Duration",`${p.durationDays||"—"} ${rtl?"أيام":"days"}`)}{row(rtl?"الانطلاق":"Departure",formatDate(p.departureAt))}{row(rtl?"العودة":"Return",formatDate(p.returnAt))}

{rtl?"برنامج الرحلة":"Trip itinerary"}

{milestones.map((m,i)=>
{m.title}{m.detail}
{m.date}
)}
{rtl?"إجمالي الحجز":"Booking total"}{Number(booking.totalAmount).toLocaleString()} SAR
{rtl?"المبلغ المتبقي":"Amount due"}{Number(booking.balanceAmount ?? booking.totalAmount).toLocaleString()} SAR
{rtl?"يرجى إبراز هذا التأكيد عند الطلب. تطبق سياسة المزود وشروط الباقة.":"Present this confirmation when requested. Provider terms apply."}duyufsa.com · +966 55 614 7561
; } function WebPaymentResultPage({ theme, lang }) { const T = theme, rtl = isRTL(lang), { setPage } = useWeb(); const orderId = new URLSearchParams(window.location.search).get("order") || ""; const [status, setStatus] = React.useState(null); React.useEffect(() => { if (orderId) fetch( `${window.DUYUF_API_URL}/v1/payments/status?order=${encodeURIComponent(orderId)}`, ) .then((r) => r.json()) .then((b) => setStatus(b.order?.status || "pending")) .catch(() => setStatus("pending")); }, [orderId]); return (

{rtl ? "جارٍ التحقق من عملية السداد" : "Verifying payment"}

{rtl ? "لا نعتبر العملية ناجحة حتى يصل تأكيد آمن من بوابة الدفع. يمكنك العودة للموقع الآن." : "The payment is not considered successful until a secure gateway confirmation arrives."}

{orderId && (

{orderId} · {status || "..."}

)}
); } // ─── Become provider page ────────────────────────────────────────────── function WebProviderSignupPage({ theme, lang }) { const T = theme; const l = L[lang]; const rtl = isRTL(lang); const { setPage } = useWeb(); const [businessName, setBusinessName] = React.useState(""); const [phone, setPhone] = React.useState(""); const [otp, setOtp] = React.useState(""); const [step, setStep] = React.useState("details"); const [busy, setBusy] = React.useState(false); const [message, setMessage] = React.useState(""); const verifyingRef = React.useRef(false); const normalizeSaudiPhone = (value) => { const digits = String(value || "").replace(/\D/g, ""); if (/^05\d{8}$/.test(digits)) return `+966${digits.slice(1)}`; if (/^9665\d{8}$/.test(digits)) return `+${digits}`; if (/^5\d{8}$/.test(digits)) return `+966${digits}`; return ""; }; const sendOtp = async () => { const formattedPhone = normalizeSaudiPhone(phone); if (!businessName.trim() || !formattedPhone) { setMessage( rtl ? "أدخل الاسم التجاري ورقم جوال سعودي صحيح." : "Enter a business name and valid Saudi mobile.", ); return; } setBusy(true); setMessage(""); try { if (window.duyufRecaptcha) window.duyufRecaptcha.clear(); window.duyufRecaptcha = new firebase.auth.RecaptchaVerifier( "duyuf-recaptcha", { size: "invisible" }, ); window.duyufConfirmation = await firebase .auth() .signInWithPhoneNumber(formattedPhone, window.duyufRecaptcha); setStep("otp"); setMessage( rtl ? "أرسلنا رمز التحقق إلى جوالك." : "We sent a verification code to your phone.", ); } catch (error) { console.error(error); setMessage( rtl ? "تعذّر إرسال الرمز. تأكد من تفعيل Phone وإضافة الدومين في Firebase." : "Could not send code. Check Phone Auth and Firebase authorized domains.", ); } finally { setBusy(false); } }; const verifyOtp = async (codeValue = otp) => { if ( !/^\d{6}$/.test(codeValue) || !window.duyufConfirmation || verifyingRef.current ) return; verifyingRef.current = true; setBusy(true); setMessage(""); let credential; try { credential = await window.duyufConfirmation.confirm(codeValue); } catch (error) { console.error("Firebase OTP confirmation failed", error); setMessage( rtl ? "الرمز غير صحيح أو انتهت صلاحيته. اطلب رمزاً جديداً." : "The code is invalid or expired. Request a new one.", ); setBusy(false); verifyingRef.current = false; return; } try { const idToken = await credential.user.getIdToken(true); const response = await fetch( `${window.DUYUF_API_URL}/v1/auth/firebase-provider`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ idToken, businessName: businessName.trim() }), }, ); const result = await response.json(); if (!response.ok || !result.ok) throw new Error(result.error || "provider_login_failed"); localStorage.setItem("duyuf_provider_session", result.sessionToken); localStorage.setItem("duyuf_provider", JSON.stringify(result.provider)); setPage("provider-dashboard"); } catch (error) { console.error("Provider account creation failed", error); setMessage( rtl ? "تم توثيق الجوال، لكن تعذّر إنشاء حساب المزود. حاول مرة أخرى دون طلب رمز جديد." : "Phone verified, but provider account creation failed. Try again without requesting a new code.", ); } finally { setBusy(false); verifyingRef.current = false; } }; return ( <>
{/* Form */}

{rtl ? "بيانات الشركة" : "Company information"}

{step === "details" ? rtl ? "ابدأ بالاسم التجاري والجوال فقط" : "Start with business name and mobile only" : step === "otp" ? rtl ? "تحقق من رقم الجوال" : "Verify mobile" : rtl ? "تم التسجيل بنجاح" : "Registration complete"}

{step === "details" && ( <> setBusinessName(e.target.value)} placeholder={ rtl ? "مثال: رحلات النور" : "Example: Al Noor Travel" } style={{ height: 46, padding: "0 14px", borderRadius: 12, background: T.bg, border: `1px solid ${T.hairlineStrong}`, color: T.text, fontFamily: "Alexandria", fontSize: 14, }} />
🇸🇦 +966
setPhone(e.target.value.replace(/\D/g, "").slice(0, 10)) } inputMode="numeric" autoComplete="tel-national" placeholder="5xxxxxxxx" aria-label={rtl ? "رقم الجوال" : "Mobile number"} style={{ minWidth: 0, flex: 1, height: "100%", padding: "0 12px", border: 0, outline: 0, background: "transparent", color: T.text, fontFamily: "Alexandria", fontSize: 16, textAlign: "left", }} />
)} {step === "otp" && ( <> { const value = e.target.value .replace(/\D/g, "") .slice(0, 6); setOtp(value); if (value.length === 6) setTimeout(() => verifyOtp(value), 0); }} inputMode="numeric" autoComplete="one-time-code" placeholder="••••••" dir="ltr" style={{ height: 54, padding: "0 14px", borderRadius: 12, textAlign: "center", letterSpacing: 8, background: T.bg, border: `1px solid ${T.hairlineStrong}`, color: T.text, fontSize: 22, }} /> )} {step === "done" && (
{rtl ? "حالة الملف: غير مكتمل. ستجد داخل لوحة المزود السجل التجاري والترخيص والبريد والوثائق المطلوبة." : "Profile status: incomplete. Complete registration, license, email and documents from your dashboard."}
)}
{message && (
{message}
)}
{/* Side info */}

{rtl ? "ماذا تحصل عليه" : "What you'll get"}

{[ { i: "trend-up", t: rtl ? "لوحة إحصائيات لحظية" : "Live analytics dashboard", d: rtl ? "تابع الإيرادات والحجوزات بالوقت الفعلي" : "Track revenue and bookings in real time", }, { i: "kaaba", t: rtl ? "معالج تكوين الباقات" : "Package wizard", d: rtl ? "انشر باقات جديدة خلال دقائق" : "Publish new packages in minutes", }, { i: "scan", t: rtl ? "تطبيق التشغيل" : "Operations app", d: rtl ? "تحقّق من العملاء وتنبيهات أثناء الرحلة" : "Check in travelers and send in-trip alerts", }, { i: "card", t: rtl ? "دفعات مجدولة وآمنة" : "Scheduled secure payouts", d: rtl ? "تحويل أسبوعي لحسابك البنكي" : "Weekly payouts to your bank account", }, ].map((b, i) => (
{b.t}
{b.d}
))}
{rtl ? "العمولة الشفافة" : "Transparent commission"}
{rtl ? "عمولة ضيوف 8% فقط من سعر الباقة الأساسي. لا رسوم مخفية، لا اشتراك شهري." : "Just 8% Duyuf commission on base package price. No hidden fees, no monthly subscription."}
); } // ─── Contact page ────────────────────────────────────────────────────── function WebContactPage({ theme, lang }) { const T = theme; const rtl = isRTL(lang); const submitContact = (e) => { e.preventDefault(); const data = new FormData(e.currentTarget); const subject = encodeURIComponent( data.get("subject") || "Duyuf website enquiry", ); const body = encodeURIComponent( `${data.get("name")}\n${data.get("email")}\n\n${data.get("message")}`, ); window.location.href = `mailto:info@duyufsa.com?subject=${subject}&body=${body}`; }; return ( <>
{[ { i: "phone", t: rtl ? "واتساب" : "WhatsApp", sub: "+966 800 200 0", meta: rtl ? "الرد خلال دقائق" : "Reply in minutes", tone: "green", }, { i: "mail", t: rtl ? "بريد إلكتروني" : "Email", sub: "support@duyuf.app", meta: rtl ? "الرد خلال 24 ساعة" : "Reply in 24h", tone: "camel", }, { i: "shield", t: rtl ? "للمزوّدين" : "Providers", sub: "providers@duyuf.app", meta: rtl ? "فريق مخصّص" : "Dedicated team", tone: "ink", }, ].map((c, i) => (
{c.t}
{c.sub}
{c.meta}
))}
{/* Contact form */}

{rtl ? "أرسل لنا رسالة" : "Send us a message"}

{[ { n: "name", l: rtl ? "الاسم" : "Name", type: "text" }, { n: "email", l: rtl ? "البريد" : "Email", type: "email" }, ].map((f, i) => (
))}