// Waka generated runtime role: public window.WAKA_RUNTIME_ROLE = "public"; window.WAKA_ALLOWED_RUNTIME_TABS = ["passenger","rider"]; // Runtime configuration, market constants, launch economics, and static domain catalogs. function configuredRuntimeRole() { const explicitRole = typeof window === "undefined" ? "" : String(window.WAKA_RUNTIME_ROLE || "").toLowerCase(); if (explicitRole === "admin") return "admin"; if (explicitRole === "passenger") return "passenger"; if (explicitRole === "rider") return "rider"; if (explicitRole === "public") return "public"; const shellRole = typeof document === "undefined" ? "" : String(document.documentElement.dataset.wakaShell || document.body?.dataset.wakaShell || "").toLowerCase(); if (["admin", "passenger", "rider"].includes(shellRole)) return shellRole; return "public"; } const runtimeRole = configuredRuntimeRole(); function adminRuntimeAvailable() { return runtimeRole === "admin"; } function runtimeAllowsWorkspaceTab(tab) { const allowedTabs = typeof window !== "undefined" && Array.isArray(window.WAKA_ALLOWED_RUNTIME_TABS) ? window.WAKA_ALLOWED_RUNTIME_TABS : []; if (allowedTabs.length) return allowedTabs.includes(tab); if (runtimeRole === "admin") return tab === "admin"; if (runtimeRole === "passenger") return tab === "passenger"; if (runtimeRole === "rider") return tab === "rider"; return tab !== "admin"; } function defaultRuntimeTab() { if (runtimeRole === "admin") return "admin"; if (runtimeRole === "rider") return "rider"; return "passenger"; } const countryCities = { Algeria: ["Algiers", "Oran", "Constantine", "Annaba", "Blida"], Angola: ["Luanda", "Huambo", "Lobito", "Benguela", "Lubango"], Benin: ["Cotonou", "Porto-Novo", "Parakou", "Abomey-Calavi", "Djougou"], Botswana: ["Gaborone", "Francistown", "Maun", "Molepolole", "Serowe"], "Burkina Faso": ["Ouagadougou", "Bobo-Dioulasso", "Koudougou", "Banfora", "Ouahigouya"], Burundi: ["Bujumbura", "Gitega", "Ngozi", "Rumonge", "Muyinga"], "Cabo Verde": ["Praia", "Mindelo", "Santa Maria", "Assomada", "Espargos"], Cameroon: ["Douala", "Yaounde", "Bamenda", "Bafoussam", "Limbe", "Buea"], "Central African Republic": ["Bangui", "Bimbo", "Berberati", "Bambari", "Bouar"], Chad: ["N'Djamena", "Moundou", "Abeche", "Sarh", "Kelo"], Comoros: ["Moroni", "Mutsamudu", "Fomboni", "Domoni", "Ouani"], Congo: ["Brazzaville", "Pointe-Noire", "Dolisie", "Nkayi", "Owando"], "Democratic Republic of the Congo": ["Kinshasa", "Lubumbashi", "Mbuji-Mayi", "Goma", "Kisangani", "Bukavu"], "Cote d'Ivoire": ["Abidjan", "Bouake", "Yamoussoukro", "San Pedro", "Korhogo"], Djibouti: ["Djibouti City", "Ali Sabieh", "Tadjoura", "Dikhil", "Obock"], Egypt: ["Cairo", "Alexandria", "Giza", "Luxor", "Aswan", "Mansoura"], "Equatorial Guinea": ["Malabo", "Bata", "Ebebiyin", "Aconibe", "Luba"], Eritrea: ["Asmara", "Keren", "Massawa", "Assab", "Mendefera"], Eswatini: ["Mbabane", "Manzini", "Lobamba", "Nhlangano", "Siteki"], Ethiopia: ["Addis Ababa", "Dire Dawa", "Mekelle", "Gondar", "Bahir Dar", "Hawassa"], Gabon: ["Libreville", "Port-Gentil", "Franceville", "Oyem", "Moanda"], Gambia: ["Banjul", "Serekunda", "Brikama", "Bakau", "Farafenni"], Ghana: ["Accra", "Kumasi", "Tamale", "Takoradi", "Tema", "Cape Coast"], Guinea: ["Conakry", "Kankan", "Nzerekore", "Kindia", "Labe"], "Guinea-Bissau": ["Bissau", "Bafata", "Gabu", "Cacheu", "Bissora"], Kenya: ["Nairobi", "Mombasa", "Kisumu", "Nakuru", "Eldoret", "Thika"], Lesotho: ["Maseru", "Teyateyaneng", "Mafeteng", "Leribe", "Mohale's Hoek"], Liberia: ["Monrovia", "Gbarnga", "Buchanan", "Ganta", "Kakata"], Libya: ["Tripoli", "Benghazi", "Misrata", "Zawiya", "Sabha"], Madagascar: ["Antananarivo", "Toamasina", "Antsirabe", "Mahajanga", "Fianarantsoa"], Malawi: ["Lilongwe", "Blantyre", "Mzuzu", "Zomba", "Kasungu"], Mali: ["Bamako", "Sikasso", "Mopti", "Segou", "Kayes"], Mauritania: ["Nouakchott", "Nouadhibou", "Kiffa", "Kaedi", "Rosso"], Mauritius: ["Port Louis", "Beau Bassin-Rose Hill", "Vacoas-Phoenix", "Curepipe", "Quatre Bornes"], Morocco: ["Casablanca", "Rabat", "Marrakesh", "Fes", "Tangier", "Agadir"], Mozambique: ["Maputo", "Matola", "Beira", "Nampula", "Chimoio"], Namibia: ["Windhoek", "Walvis Bay", "Swakopmund", "Oshakati", "Rundu"], Niger: ["Niamey", "Zinder", "Maradi", "Agadez", "Tahoua"], Nigeria: ["Lagos", "Abuja", "Kano", "Ibadan", "Port Harcourt", "Enugu", "Kaduna"], Rwanda: ["Kigali", "Butare", "Gisenyi", "Musanze", "Rwamagana"], "Sao Tome and Principe": ["Sao Tome", "Santo Antonio", "Trindade", "Neves", "Santana"], Senegal: ["Dakar", "Thies", "Touba", "Saint-Louis", "Kaolack", "Ziguinchor"], Seychelles: ["Victoria", "Anse Boileau", "Beau Vallon", "Takamaka", "Anse Royale"], "Sierra Leone": ["Freetown", "Bo", "Kenema", "Makeni", "Koidu"], Somalia: ["Mogadishu", "Hargeisa", "Bosaso", "Kismayo", "Baidoa"], "South Africa": ["Johannesburg", "Cape Town", "Durban", "Pretoria", "Port Elizabeth", "Bloemfontein"], "South Sudan": ["Juba", "Wau", "Malakal", "Yei", "Aweil"], Sudan: ["Khartoum", "Omdurman", "Port Sudan", "Kassala", "El Obeid"], Tanzania: ["Dar es Salaam", "Dodoma", "Mwanza", "Arusha", "Zanzibar City"], Togo: ["Lome", "Sokode", "Kara", "Kpalime", "Atakpame"], Tunisia: ["Tunis", "Sfax", "Sousse", "Kairouan", "Bizerte"], Uganda: ["Kampala", "Gulu", "Mbarara", "Jinja", "Mbale"], Zambia: ["Lusaka", "Kitwe", "Ndola", "Livingstone", "Kabwe"], Zimbabwe: ["Harare", "Bulawayo", "Chitungwiza", "Mutare", "Gweru"], Argentina: ["Buenos Aires", "Cordoba", "Rosario", "Mendoza", "La Plata"], Bahamas: ["Nassau", "Freeport", "West End", "Coopers Town", "Marsh Harbour"], Barbados: ["Bridgetown", "Speightstown", "Oistins", "Holetown", "Bathsheba"], Belize: ["Belize City", "Belmopan", "San Ignacio", "Orange Walk", "Dangriga"], Bolivia: ["La Paz", "Santa Cruz", "Cochabamba", "Sucre", "El Alto"], Brazil: ["Sao Paulo", "Rio de Janeiro", "Brasilia", "Salvador", "Fortaleza", "Belo Horizonte"], Canada: ["Toronto", "Montreal", "Vancouver", "Calgary", "Ottawa", "Edmonton"], Chile: ["Santiago", "Valparaiso", "Concepcion", "La Serena", "Antofagasta"], Colombia: ["Bogota", "Medellin", "Cali", "Barranquilla", "Cartagena"], "Costa Rica": ["San Jose", "Alajuela", "Cartago", "Heredia", "Liberia"], Cuba: ["Havana", "Santiago de Cuba", "Camaguey", "Holguin", "Santa Clara"], "Dominican Republic": ["Santo Domingo", "Santiago", "La Romana", "Puerto Plata", "San Pedro de Macoris"], Ecuador: ["Quito", "Guayaquil", "Cuenca", "Santo Domingo", "Machala"], "El Salvador": ["San Salvador", "Santa Ana", "San Miguel", "Soyapango", "Mejicanos"], Guatemala: ["Guatemala City", "Quetzaltenango", "Mixco", "Villa Nueva", "Escuintla"], Guyana: ["Georgetown", "Linden", "New Amsterdam", "Anna Regina", "Bartica"], Haiti: ["Port-au-Prince", "Cap-Haitien", "Carrefour", "Delmas", "Petion-Ville"], Honduras: ["Tegucigalpa", "San Pedro Sula", "La Ceiba", "Choloma", "El Progreso"], Jamaica: ["Kingston", "Montego Bay", "Spanish Town", "Portmore", "Mandeville"], Mexico: ["Mexico City", "Guadalajara", "Monterrey", "Puebla", "Tijuana", "Merida"], Nicaragua: ["Managua", "Leon", "Masaya", "Matagalpa", "Chinandega"], Panama: ["Panama City", "San Miguelito", "Tocumen", "David", "Colon"], Paraguay: ["Asuncion", "Ciudad del Este", "San Lorenzo", "Luque", "Capiata"], Peru: ["Lima", "Arequipa", "Trujillo", "Chiclayo", "Cusco"], Suriname: ["Paramaribo", "Lelydorp", "Nieuw Nickerie", "Moengo", "Meerzorg"], "Trinidad and Tobago": ["Port of Spain", "San Fernando", "Chaguanas", "Arima", "Point Fortin"], "United States": [ "Maryland", "Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", "District of Columbia", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", "Maine", "Massachusetts", "Michigan", "Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming" ], Uruguay: ["Montevideo", "Salto", "Paysandu", "Las Piedras", "Maldonado"], Venezuela: ["Caracas", "Maracaibo", "Valencia", "Barquisimeto", "Maracay"], Albania: ["Tirana", "Durres", "Vlore", "Shkoder", "Fier"], Austria: ["Vienna", "Graz", "Linz", "Salzburg", "Innsbruck"], Belgium: ["Brussels", "Antwerp", "Ghent", "Charleroi", "Liege"], Bulgaria: ["Sofia", "Plovdiv", "Varna", "Burgas", "Ruse"], Croatia: ["Zagreb", "Split", "Rijeka", "Osijek", "Zadar"], Czechia: ["Prague", "Brno", "Ostrava", "Plzen", "Liberec"], Denmark: ["Copenhagen", "Aarhus", "Odense", "Aalborg", "Esbjerg"], Finland: ["Helsinki", "Espoo", "Tampere", "Vantaa", "Turku"], France: ["Paris", "Marseille", "Lyon", "Toulouse", "Nice", "Nantes"], Germany: ["Berlin", "Hamburg", "Munich", "Cologne", "Frankfurt", "Stuttgart"], Greece: ["Athens", "Thessaloniki", "Patras", "Heraklion", "Larissa"], Hungary: ["Budapest", "Debrecen", "Szeged", "Miskolc", "Pecs"], Ireland: ["Dublin", "Cork", "Limerick", "Galway", "Waterford"], Italy: ["Rome", "Milan", "Naples", "Turin", "Palermo", "Florence"], Netherlands: ["Amsterdam", "Rotterdam", "The Hague", "Utrecht", "Eindhoven"], Norway: ["Oslo", "Bergen", "Trondheim", "Stavanger", "Drammen"], Poland: ["Warsaw", "Krakow", "Lodz", "Wroclaw", "Poznan"], Portugal: ["Lisbon", "Porto", "Braga", "Coimbra", "Faro"], Romania: ["Bucharest", "Cluj-Napoca", "Timisoara", "Iasi", "Constanta"], Serbia: ["Belgrade", "Novi Sad", "Nis", "Kragujevac", "Subotica"], Spain: ["Madrid", "Barcelona", "Valencia", "Seville", "Bilbao", "Malaga"], Sweden: ["Stockholm", "Gothenburg", "Malmo", "Uppsala", "Vasteras"], Switzerland: ["Zurich", "Geneva", "Basel", "Lausanne", "Bern"], Turkey: ["Istanbul", "Ankara", "Izmir", "Bursa", "Antalya"], Ukraine: ["Kyiv", "Kharkiv", "Odesa", "Dnipro", "Lviv"], "United Kingdom": ["London", "Birmingham", "Manchester", "Glasgow", "Liverpool", "Leeds"], Bangladesh: ["Dhaka", "Chittagong", "Khulna", "Sylhet", "Rajshahi"], China: ["Shanghai", "Beijing", "Guangzhou", "Shenzhen", "Chengdu", "Wuhan"], India: ["Delhi", "Mumbai", "Bengaluru", "Hyderabad", "Chennai", "Kolkata"], Indonesia: ["Jakarta", "Surabaya", "Bandung", "Medan", "Makassar"], Iran: ["Tehran", "Mashhad", "Isfahan", "Shiraz", "Tabriz"], Iraq: ["Baghdad", "Basra", "Mosul", "Erbil", "Najaf"], Israel: ["Tel Aviv", "Jerusalem", "Haifa", "Beersheba", "Netanya"], Japan: ["Tokyo", "Osaka", "Yokohama", "Nagoya", "Sapporo", "Fukuoka"], Jordan: ["Amman", "Zarqa", "Irbid", "Aqaba", "Madaba"], Kazakhstan: ["Almaty", "Astana", "Shymkent", "Karaganda", "Aktobe"], Kuwait: ["Kuwait City", "Hawalli", "Salmiya", "Farwaniya", "Jahra"], Lebanon: ["Beirut", "Tripoli", "Sidon", "Tyre", "Zahle"], Malaysia: ["Kuala Lumpur", "George Town", "Johor Bahru", "Ipoh", "Kota Kinabalu"], Nepal: ["Kathmandu", "Pokhara", "Lalitpur", "Biratnagar", "Birgunj"], Pakistan: ["Karachi", "Lahore", "Islamabad", "Rawalpindi", "Faisalabad"], Philippines: ["Manila", "Quezon City", "Cebu City", "Davao City", "Caloocan"], Qatar: ["Doha", "Al Rayyan", "Al Wakrah", "Umm Salal", "Al Khor"], "Saudi Arabia": ["Riyadh", "Jeddah", "Mecca", "Medina", "Dammam"], Singapore: ["Singapore", "Jurong East", "Tampines", "Woodlands", "Yishun"], "South Korea": ["Seoul", "Busan", "Incheon", "Daegu", "Daejeon"], "Sri Lanka": ["Colombo", "Kandy", "Galle", "Jaffna", "Negombo"], Thailand: ["Bangkok", "Chiang Mai", "Pattaya", "Phuket", "Nonthaburi"], "United Arab Emirates": ["Dubai", "Abu Dhabi", "Sharjah", "Ajman", "Al Ain"], Uzbekistan: ["Tashkent", "Samarkand", "Bukhara", "Namangan", "Andijan"], Vietnam: ["Ho Chi Minh City", "Hanoi", "Da Nang", "Can Tho", "Hai Phong"] }; const africanRidePaymentCountries = new Set([ "Algeria", "Angola", "Benin", "Botswana", "Burkina Faso", "Burundi", "Cabo Verde", "Cameroon", "Central African Republic", "Chad", "Comoros", "Congo", "Democratic Republic of the Congo", "Cote d'Ivoire", "Djibouti", "Egypt", "Equatorial Guinea", "Eritrea", "Eswatini", "Ethiopia", "Gabon", "Gambia", "Ghana", "Guinea", "Guinea-Bissau", "Kenya", "Lesotho", "Liberia", "Libya", "Madagascar", "Malawi", "Mali", "Mauritania", "Mauritius", "Morocco", "Mozambique", "Namibia", "Niger", "Nigeria", "Rwanda", "Sao Tome and Principe", "Senegal", "Seychelles", "Sierra Leone", "Somalia", "South Africa", "South Sudan", "Sudan", "Tanzania", "Togo", "Tunisia", "Uganda", "Zambia", "Zimbabwe" ]); const onlineRidePaymentValues = new Set(["online_card", "online_wallet"]); const productionOnlineRidePaymentProviderPattern = /\b(stripe|adyen|paypal|checkout|paystack|flutterwave|rapyd)\b/i; const marylandMapBounds = { minLatitude: 37.8, maxLatitude: 39.8, minLongitude: -79.6, maxLongitude: -75.0 }; function marylandPlaceMapX(longitude) { const span = marylandMapBounds.maxLongitude - marylandMapBounds.minLongitude; return Math.max(2, Math.min(98, Math.round(((longitude - marylandMapBounds.minLongitude) / span) * 100))); } function marylandPlaceMapY(latitude) { const span = marylandMapBounds.maxLatitude - marylandMapBounds.minLatitude; return Math.max(2, Math.min(98, Math.round(((marylandMapBounds.maxLatitude - latitude) / span) * 100))); } // Maryland launches statewide: keep the pilot towns first, then include every 2024 Census place. const marylandLaunchPlaces = [ { name: "Baltimore", latitude: 39.2904, longitude: -76.6122, x: 58, y: 28 }, { name: "Towson", latitude: 39.4015, longitude: -76.6019, x: 57, y: 20 }, { name: "Pikesville", latitude: 39.3743, longitude: -76.7225, x: 50, y: 24 }, { name: "Parkville", latitude: 39.3773, longitude: -76.5397, x: 61, y: 23 }, { name: "Catonsville", latitude: 39.2721, longitude: -76.7319, x: 51, y: 32 }, { name: "Dundalk", latitude: 39.2507, longitude: -76.5205, x: 64, y: 32 }, { name: "Essex", latitude: 39.3093, longitude: -76.4747, x: 66, y: 28 }, { name: "Glen Burnie", latitude: 39.1626, longitude: -76.6247, x: 62, y: 43 }, { name: "Severn", latitude: 39.1371, longitude: -76.6983, x: 58, y: 45 }, { name: "Hanover", latitude: 39.1929, longitude: -76.7241, x: 56, y: 44 }, { name: "Columbia", latitude: 39.2037, longitude: -76.861, x: 50, y: 39 }, { name: "Ellicott City", latitude: 39.2673, longitude: -76.7983, x: 47, y: 37 }, { name: "Laurel", latitude: 39.0993, longitude: -76.8483, x: 51, y: 48 }, { name: "Beltsville", latitude: 39.0348, longitude: -76.9075, x: 48, y: 52 }, { name: "Greenbelt", latitude: 39.0046, longitude: -76.8755, x: 52, y: 56 }, { name: "College Park", latitude: 38.9807, longitude: -76.9369, x: 48, y: 58 }, { name: "Hyattsville", latitude: 38.9559, longitude: -76.9455, x: 47, y: 61 }, { name: "Riverdale Park", latitude: 38.9634, longitude: -76.9316, x: 49, y: 60 }, { name: "New Carrollton", latitude: 38.9698, longitude: -76.8797, x: 53, y: 60 }, { name: "Lanham", latitude: 38.9688, longitude: -76.8634, x: 55, y: 60 }, { name: "Landover", latitude: 38.934, longitude: -76.8966, x: 53, y: 63 }, { name: "Largo", latitude: 38.8976, longitude: -76.8303, x: 58, y: 64 }, { name: "Capitol Heights", latitude: 38.8851, longitude: -76.9158, x: 54, y: 67 }, { name: "District Heights", latitude: 38.8576, longitude: -76.8894, x: 56, y: 68 }, { name: "Suitland", latitude: 38.8487, longitude: -76.9239, x: 54, y: 70 }, { name: "Temple Hills", latitude: 38.814, longitude: -76.9458, x: 51, y: 70 }, { name: "Oxon Hill", latitude: 38.8034, longitude: -76.9897, x: 50, y: 73 }, { name: "Clinton", latitude: 38.7651, longitude: -76.8983, x: 55, y: 75 }, { name: "Upper Marlboro", latitude: 38.8159, longitude: -76.7497, x: 61, y: 69 }, { name: "Bowie", latitude: 38.9428, longitude: -76.7303, x: 62, y: 59 }, { name: "Crofton", latitude: 39.0018, longitude: -76.6875, x: 65, y: 54 }, { name: "Odenton", latitude: 39.084, longitude: -76.7002, x: 61, y: 48 }, { name: "Annapolis", latitude: 38.9784, longitude: -76.4922, x: 72, y: 55 }, { name: "Silver Spring", latitude: 38.9907, longitude: -77.0261, x: 42, y: 61 }, { name: "Takoma Park", latitude: 38.9779, longitude: -77.0075, x: 43, y: 63 }, { name: "Wheaton", latitude: 39.0398, longitude: -77.0553, x: 40, y: 58 }, { name: "Kensington", latitude: 39.0257, longitude: -77.0764, x: 38, y: 58 }, { name: "Rockville", latitude: 39.084, longitude: -77.1528, x: 33, y: 55 }, { name: "Bethesda", latitude: 38.9847, longitude: -77.0947, x: 38, y: 64 }, { name: "Gaithersburg", latitude: 39.1434, longitude: -77.2014, x: 27, y: 49 }, { name: "Germantown", latitude: 39.174252, longitude: -77.263754, x: 24, y: 46 }, { name: "Olney", latitude: 39.14434, longitude: -77.071429, x: 36, y: 48 }, { name: "Potomac", latitude: 39.013332, longitude: -77.193579, x: 32, y: 59 }, { name: "Frederick", latitude: 39.434089, longitude: -77.414539, x: 22, y: 32 }, { name: "Urbana", latitude: 39.32577, longitude: -77.341519, x: 25, y: 38 }, { name: "Mount Airy", latitude: 39.374693, longitude: -77.153807, x: 31, y: 34 }, { name: "Westminster", latitude: 39.579154, longitude: -77.006691, x: 38, y: 21 }, { name: "Bel Air", latitude: 39.534942, longitude: -76.346323, x: 70, y: 16 }, { name: "Aberdeen", latitude: 39.516403, longitude: -76.174518, x: 76, y: 18 }, { name: "Havre de Grace", latitude: 39.546572, longitude: -76.113576, x: 78, y: 15 }, { name: "Hagerstown", latitude: 39.640182, longitude: -77.722691, x: 10, y: 29 }, { name: "Waldorf", latitude: 38.609172, longitude: -76.919776, x: 55, y: 83 }, { name: "La Plata", latitude: 38.543467, longitude: -76.96979, x: 54, y: 88 }, { name: "Lexington Park", latitude: 38.249403, longitude: -76.443556, x: 70, y: 93 }, { name: "Salisbury", latitude: 38.375328, longitude: -75.585925, x: 91, y: 82 }, { name: "Ocean City", latitude: 38.393125, longitude: -75.071208, x: 98, y: 82 }, { name: "Aberdeen Proving Ground", latitude: 39.454232, longitude: -76.133542 }, { name: "Abingdon", latitude: 39.463472, longitude: -76.27314 }, { name: "Accident", latitude: 39.625688, longitude: -79.319958 }, { name: "Accokeek", latitude: 38.676533, longitude: -77.000518 }, { name: "Adamstown", latitude: 39.307001, longitude: -77.469296 }, { name: "Adelphi", latitude: 38.997067, longitude: -76.966783 }, { name: "Algonquin", latitude: 38.589853, longitude: -76.091558 }, { name: "Allen", latitude: 38.288387, longitude: -75.693051 }, { name: "Andrews AFB", latitude: 38.809803, longitude: -76.869158 }, { name: "Annapolis Neck", latitude: 38.936562, longitude: -76.498084 }, { name: "Antietam", latitude: 39.415082, longitude: -77.736484 }, { name: "Aquasco", latitude: 38.591534, longitude: -76.697125 }, { name: "Arbutus", latitude: 39.242673, longitude: -76.692133 }, { name: "Arden on the Severn", latitude: 39.06771, longitude: -76.596488 }, { name: "Arnold", latitude: 39.043227, longitude: -76.504985 }, { name: "Ashton-Sandy Spring", latitude: 39.148685, longitude: -76.999085 }, { name: "Aspen Hill", latitude: 39.093635, longitude: -77.081985 }, { name: "Baden", latitude: 38.669485, longitude: -76.744221 }, { name: "Bagtown", latitude: 39.582982, longitude: -77.613885 }, { name: "Bakersville", latitude: 39.514829, longitude: -77.757048 }, { name: "Ballenger Creek", latitude: 39.381131, longitude: -77.420179 }, { name: "Baltimore Highlands", latitude: 39.235932, longitude: -76.637402 }, { name: "Barclay", latitude: 39.146454, longitude: -75.864433 }, { name: "Barnesville", latitude: 39.22392, longitude: -77.376106 }, { name: "Barrelville", latitude: 39.702653, longitude: -78.842454 }, { name: "Barton", latitude: 39.532283, longitude: -79.016785 }, { name: "Bartonsville", latitude: 39.388795, longitude: -77.352961 }, { name: "Beaver Creek", latitude: 39.581545, longitude: -77.651338 }, { name: "Bel Air North", latitude: 39.553404, longitude: -76.372942 }, { name: "Bel Air South", latitude: 39.508061, longitude: -76.309365 }, { name: "Benedict", latitude: 38.511529, longitude: -76.67967 }, { name: "Bensville", latitude: 38.612971, longitude: -77.004735 }, { name: "Berlin", latitude: 38.331533, longitude: -75.215434 }, { name: "Berwyn Heights", latitude: 38.992854, longitude: -76.913451 }, { name: "Betterton", latitude: 39.367072, longitude: -76.072495 }, { name: "Bier", latitude: 39.55513, longitude: -78.871122 }, { name: "Big Pool", latitude: 39.625094, longitude: -78.016168 }, { name: "Big Spring", latitude: 39.625902, longitude: -77.939632 }, { name: "Bishopville", latitude: 38.438642, longitude: -75.209507 }, { name: "Bivalve", latitude: 38.306205, longitude: -75.880628 }, { name: "Bladensburg", latitude: 38.942368, longitude: -76.92591 }, { name: "Bloomington", latitude: 39.481331, longitude: -79.07851 }, { name: "Boonsboro", latitude: 39.510648, longitude: -77.664745 }, { name: "Bowleys Quarters", latitude: 39.31281, longitude: -76.38227 }, { name: "Bowling Green", latitude: 39.62713, longitude: -78.805046 }, { name: "Bowmans Addition", latitude: 39.686344, longitude: -78.755028 }, { name: "Braddock Heights", latitude: 39.409522, longitude: -77.493564 }, { name: "Brandywine", latitude: 38.682077, longitude: -76.885314 }, { name: "Breathedsville", latitude: 39.54601, longitude: -77.724548 }, { name: "Brentwood", latitude: 38.943883, longitude: -76.957046 }, { name: "Brock Hall", latitude: 38.855852, longitude: -76.742435 }, { name: "Brookeville", latitude: 39.182514, longitude: -77.059725 }, { name: "Brooklyn Park", latitude: 39.2195, longitude: -76.620921 }, { name: "Brookmont", latitude: 38.953831, longitude: -77.12902 }, { name: "Brookview", latitude: 38.574011, longitude: -75.792963 }, { name: "Broomes Island", latitude: 38.411433, longitude: -76.548803 }, { name: "Brown Station", latitude: 38.855332, longitude: -76.797065 }, { name: "Brownsville", latitude: 39.378174, longitude: -77.661555 }, { name: "Brunswick", latitude: 39.31799, longitude: -77.62528 }, { name: "Bryans Road", latitude: 38.6044, longitude: -77.092279 }, { name: "Bryantown", latitude: 38.548839, longitude: -76.842182 }, { name: "Buckeystown", latitude: 39.324138, longitude: -77.428135 }, { name: "Burkittsville", latitude: 39.393497, longitude: -77.627788 }, { name: "Burnt Mills", latitude: 39.033984, longitude: -76.998272 }, { name: "Burtonsville", latitude: 39.120921, longitude: -76.936675 }, { name: "Butlertown", latitude: 39.282194, longitude: -76.099165 }, { name: "Cabin John", latitude: 38.974014, longitude: -77.163841 }, { name: "California", latitude: 38.297599, longitude: -76.492143 }, { name: "Callaway", latitude: 38.233918, longitude: -76.528932 }, { name: "Calvert Beach", latitude: 38.472799, longitude: -76.489676 }, { name: "Calverton", latitude: 39.058085, longitude: -76.950848 }, { name: "Cambridge", latitude: 38.554063, longitude: -76.077116 }, { name: "Camp Springs", latitude: 38.805072, longitude: -76.918565 }, { name: "Cape St. Claire", latitude: 39.043634, longitude: -76.445339 }, { name: "Carlos", latitude: 39.623743, longitude: -78.95668 }, { name: "Carney", latitude: 39.404896, longitude: -76.522725 }, { name: "Cavetown", latitude: 39.642658, longitude: -77.5932 }, { name: "Cearfoss", latitude: 39.699133, longitude: -77.77634 }, { name: "Cecilton", latitude: 39.404824, longitude: -75.867412 }, { name: "Cedar Heights", latitude: 38.903738, longitude: -76.905879 }, { name: "Cedarville", latitude: 38.656912, longitude: -76.82201 }, { name: "Centreville", latitude: 39.042265, longitude: -76.062538 }, { name: "Chance", latitude: 38.17804, longitude: -75.938788 }, { name: "Charlestown", latitude: 39.582121, longitude: -75.986712 }, { name: "Charlotte Hall", latitude: 38.468236, longitude: -76.782645 }, { name: "Charlton", latitude: 39.634401, longitude: -77.894364 }, { name: "Chesapeake Beach", latitude: 38.689456, longitude: -76.554404 }, { name: "Chesapeake City", latitude: 39.527212, longitude: -75.811653 }, { name: "Chesapeake Landing", latitude: 39.269347, longitude: -76.157236 }, { name: "Chesapeake Ranch Estates", latitude: 38.357625, longitude: -76.417351 }, { name: "Chester", latitude: 38.960094, longitude: -76.287616 }, { name: "Chestertown", latitude: 39.218383, longitude: -76.074022 }, { name: "Cheverly", latitude: 38.925931, longitude: -76.913468 }, { name: "Chevy Chase", latitude: 38.992542, longitude: -77.074905 }, { name: "Chevy Chase Section Five", latitude: 38.98398, longitude: -77.074027 }, { name: "Chevy Chase Section Three", latitude: 38.979259, longitude: -77.074187 }, { name: "Chevy Chase View", latitude: 39.019201, longitude: -77.081104 }, { name: "Chevy Chase Village", latitude: 38.969781, longitude: -77.07934 }, { name: "Chewsville", latitude: 39.648415, longitude: -77.631239 }, { name: "Chillum", latitude: 38.966667, longitude: -76.978885 }, { name: "Choptank", latitude: 38.682282, longitude: -75.949398 }, { name: "Church Creek", latitude: 38.504888, longitude: -76.15403 }, { name: "Church Hill", latitude: 39.145, longitude: -75.98077 }, { name: "Clarksburg", latitude: 39.222893, longitude: -77.266079 }, { name: "Clarysville", latitude: 39.642026, longitude: -78.888947 }, { name: "Clear Spring", latitude: 39.65605, longitude: -77.930373 }, { name: "Cloverly", latitude: 39.103947, longitude: -76.994829 }, { name: "Cobb Island", latitude: 38.263698, longitude: -76.848986 }, { name: "Cockeysville", latitude: 39.478003, longitude: -76.630796 }, { name: "Colesville", latitude: 39.07284, longitude: -77.00058 }, { name: "Colmar Manor", latitude: 38.926801, longitude: -76.943876 }, { name: "Coral Hills", latitude: 38.872313, longitude: -76.920979 }, { name: "Cordova", latitude: 38.86801, longitude: -75.998885 }, { name: "Corriganville", latitude: 39.694646, longitude: -78.79726 }, { name: "Cottage City", latitude: 38.938399, longitude: -76.949445 }, { name: "Crellin", latitude: 39.388631, longitude: -79.468471 }, { name: "Cresaptown", latitude: 39.593506, longitude: -78.83505 }, { name: "Crisfield", latitude: 37.975457, longitude: -75.854542 }, { name: "Croom", latitude: 38.739799, longitude: -76.755192 }, { name: "Crownsville", latitude: 39.022462, longitude: -76.590377 }, { name: "Crumpton", latitude: 39.233174, longitude: -75.922049 }, { name: "Cumberland", latitude: 39.65131, longitude: -78.757959 }, { name: "Damascus", latitude: 39.270995, longitude: -77.196834 }, { name: "Dames Quarter", latitude: 38.17292, longitude: -75.890024 }, { name: "Danville", latitude: 39.510165, longitude: -78.917545 }, { name: "Dargan", latitude: 39.376676, longitude: -77.734045 }, { name: "Darlington", latitude: 39.642448, longitude: -76.203521 }, { name: "Darnestown", latitude: 39.088709, longitude: -77.311779 }, { name: "Dawson", latitude: 39.477083, longitude: -78.945207 }, { name: "Deale", latitude: 38.786091, longitude: -76.540708 }, { name: "Deal Island", latitude: 38.152793, longitude: -75.939954 }, { name: "Deer Park", latitude: 39.423961, longitude: -79.325986 }, { name: "Delmar", latitude: 38.442149, longitude: -75.560422 }, { name: "Denton", latitude: 38.879421, longitude: -75.824452 }, { name: "Derwood", latitude: 39.114116, longitude: -77.150228 }, { name: "Detmold", latitude: 39.557365, longitude: -78.991143 }, { name: "Downsville", latitude: 39.55482, longitude: -77.80163 }, { name: "Drum Point", latitude: 38.330395, longitude: -76.435985 }, { name: "Dunkirk", latitude: 38.718193, longitude: -76.676782 }, { name: "Eagle Harbor", latitude: 38.567577, longitude: -76.687137 }, { name: "Eakles Mill", latitude: 39.467067, longitude: -77.68596 }, { name: "East New Market", latitude: 38.597025, longitude: -75.923126 }, { name: "Easton", latitude: 38.776262, longitude: -76.070478 }, { name: "East Riverdale", latitude: 38.959762, longitude: -76.91102 }, { name: "Eckhart Mines", latitude: 39.655233, longitude: -78.89411 }, { name: "Eden", latitude: 38.278223, longitude: -75.654776 }, { name: "Edesville", latitude: 39.153861, longitude: -76.207235 }, { name: "Edgemere", latitude: 39.220671, longitude: -76.456375 }, { name: "Edgemont", latitude: 39.676611, longitude: -77.546948 }, { name: "Edgewater", latitude: 38.939653, longitude: -76.551401 }, { name: "Edgewood", latitude: 39.42029, longitude: -76.296846 }, { name: "Edmonston", latitude: 38.949965, longitude: -76.932196 }, { name: "Eldersburg", latitude: 39.404192, longitude: -76.952585 }, { name: "Eldorado", latitude: 38.58304, longitude: -75.790281 }, { name: "Elkridge", latitude: 39.195483, longitude: -76.741082 }, { name: "Elkton", latitude: 39.605776, longitude: -75.821686 }, { name: "Ellerslie", latitude: 39.713709, longitude: -78.776181 }, { name: "Elliott", latitude: 38.308141, longitude: -76.010718 }, { name: "Emmitsburg", latitude: 39.705192, longitude: -77.321301 }, { name: "Ernstville", latitude: 39.630387, longitude: -78.023955 }, { name: "Fairland", latitude: 39.08085, longitude: -76.952544 }, { name: "Fairlee", latitude: 39.22587, longitude: -76.166332 }, { name: "Fairmount", latitude: 38.109765, longitude: -75.820564 }, { name: "Fairmount Heights", latitude: 38.901591, longitude: -76.915339 }, { name: "Fairplay", latitude: 39.53567, longitude: -77.74631 }, { name: "Fairview", latitude: 39.711079, longitude: -77.840616 }, { name: "Fairwood", latitude: 38.954713, longitude: -76.776544 }, { name: "Fallston", latitude: 39.533804, longitude: -76.438545 }, { name: "Federalsburg", latitude: 38.692265, longitude: -75.772465 }, { name: "Ferndale", latitude: 39.18696, longitude: -76.633107 }, { name: "Finzel", latitude: 39.701721, longitude: -78.951997 }, { name: "Fishing Creek", latitude: 38.336953, longitude: -76.221683 }, { name: "Flintstone", latitude: 39.703531, longitude: -78.575786 }, { name: "Flower Hill", latitude: 39.168443, longitude: -77.184443 }, { name: "Forest Glen", latitude: 39.018914, longitude: -77.045085 }, { name: "Forest Heights", latitude: 38.803776, longitude: -77.011691 }, { name: "Forestville", latitude: 38.851737, longitude: -76.870765 }, { name: "Fort Meade", latitude: 39.1057, longitude: -76.743278 }, { name: "Fort Ritchie", latitude: 39.703659, longitude: -77.506389 }, { name: "Fort Washington", latitude: 38.731868, longitude: -77.008662 }, { name: "Fountainhead-Orchard Hills", latitude: 39.687838, longitude: -77.717298 }, { name: "Four Corners", latitude: 39.023312, longitude: -77.010204 }, { name: "Franklin", latitude: 39.498927, longitude: -79.051562 }, { name: "Frenchtown-Rumbly", latitude: 38.07445, longitude: -75.853323 }, { name: "Friendly", latitude: 38.760284, longitude: -76.966952 }, { name: "Friendship", latitude: 38.735845, longitude: -76.58782 }, { name: "Friendship Heights Village", latitude: 38.963311, longitude: -77.089792 }, { name: "Friendsville", latitude: 39.662448, longitude: -79.404414 }, { name: "Frostburg", latitude: 39.650523, longitude: -78.926793 }, { name: "Fruitland", latitude: 38.320393, longitude: -75.626502 }, { name: "Fulton", latitude: 39.15311, longitude: -76.911629 }, { name: "Funkstown", latitude: 39.606743, longitude: -77.705137 }, { name: "Galena", latitude: 39.342465, longitude: -75.878873 }, { name: "Galestown", latitude: 38.562606, longitude: -75.715814 }, { name: "Galesville", latitude: 38.84077, longitude: -76.554368 }, { name: "Gambrills", latitude: 39.092725, longitude: -76.651034 }, { name: "Gapland", latitude: 39.40196, longitude: -77.658288 }, { name: "Garrett Park", latitude: 39.036129, longitude: -77.093547 }, { name: "Garretts Mill", latitude: 39.353335, longitude: -77.688911 }, { name: "Garrison", latitude: 39.402274, longitude: -76.751847 }, { name: "Georgetown", latitude: 39.221577, longitude: -76.191772 }, { name: "Gilmore", latitude: 39.58326, longitude: -78.951127 }, { name: "Girdletree", latitude: 38.098879, longitude: -75.400257 }, { name: "Glassmanor", latitude: 38.818138, longitude: -76.983152 }, { name: "Glenarden", latitude: 38.929288, longitude: -76.857691 }, { name: "Glen Echo", latitude: 38.968167, longitude: -77.141101 }, { name: "Glenmont", latitude: 39.070413, longitude: -77.046628 }, { name: "Glenn Dale", latitude: 38.984395, longitude: -76.800322 }, { name: "Golden Beach", latitude: 38.489734, longitude: -76.700689 }, { name: "Goldsboro", latitude: 39.030003, longitude: -75.780955 }, { name: "Gorman", latitude: 39.292702, longitude: -79.35275 }, { name: "Graceham", latitude: 39.617423, longitude: -77.387024 }, { name: "Grahamtown", latitude: 39.644861, longitude: -78.922288 }, { name: "Grantsville", latitude: 39.696916, longitude: -79.152849 }, { name: "Grasonville", latitude: 38.957459, longitude: -76.197803 }, { name: "Greensboro", latitude: 38.976354, longitude: -75.808076 }, { name: "Greensburg", latitude: 39.68075, longitude: -77.561545 }, { name: "Green Valley", latitude: 39.341841, longitude: -77.240301 }, { name: "Halfway", latitude: 39.61567, longitude: -77.771068 }, { name: "Hampstead", latitude: 39.614597, longitude: -76.854801 }, { name: "Hampton", latitude: 39.424788, longitude: -76.566473 }, { name: "Hancock", latitude: 39.704578, longitude: -78.16327 }, { name: "Hebron", latitude: 38.42426, longitude: -75.687163 }, { name: "Henderson", latitude: 39.074942, longitude: -75.766077 }, { name: "Herald Harbor", latitude: 39.051265, longitude: -76.57492 }, { name: "Highfield-Cascade", latitude: 39.713592, longitude: -77.494566 }, { name: "Highland", latitude: 39.187223, longitude: -76.956731 }, { name: "Highland Beach", latitude: 38.931139, longitude: -76.467018 }, { name: "Hillandale", latitude: 39.026341, longitude: -76.975202 }, { name: "Hillcrest Heights", latitude: 38.836453, longitude: -76.970583 }, { name: "Hillsboro", latitude: 38.917043, longitude: -75.941779 }, { name: "Honeygo", latitude: 39.404969, longitude: -76.430016 }, { name: "Hughesville", latitude: 38.53742, longitude: -76.772789 }, { name: "Huntingtown", latitude: 38.61235, longitude: -76.62154 }, { name: "Hurlock", latitude: 38.625712, longitude: -75.867186 }, { name: "Hutton", latitude: 39.414494, longitude: -79.480044 }, { name: "Ilchester", latitude: 39.21611, longitude: -76.762058 }, { name: "Indian Head", latitude: 38.598622, longitude: -77.155654 }, { name: "Indian Springs", latitude: 39.645634, longitude: -78.007402 }, { name: "Jarrettsville", latitude: 39.600745, longitude: -76.47241 }, { name: "Jefferson", latitude: 39.365345, longitude: -77.540671 }, { name: "Jennings", latitude: 39.648657, longitude: -79.183497 }, { name: "Jessup", latitude: 39.149094, longitude: -76.776388 }, { name: "Jesterville", latitude: 38.290479, longitude: -75.889929 }, { name: "Joppatowne", latitude: 39.416594, longitude: -76.352171 }, { name: "Jugtown", latitude: 39.614277, longitude: -77.5943 }, { name: "Keedysville", latitude: 39.486338, longitude: -77.697552 }, { name: "Kemp Mill", latitude: 39.041101, longitude: -77.021995 }, { name: "Kemps Mill", latitude: 39.62687, longitude: -77.813902 }, { name: "Kennedyville", latitude: 39.303213, longitude: -75.994244 }, { name: "Kent Narrows", latitude: 38.976483, longitude: -76.243412 }, { name: "Kettering", latitude: 38.889656, longitude: -76.790302 }, { name: "Kingstown", latitude: 39.207626, longitude: -76.04391 }, { name: "Kingsville", latitude: 39.451907, longitude: -76.430345 }, { name: "Kitzmiller", latitude: 39.389159, longitude: -79.183284 }, { name: "Klondike", latitude: 39.610247, longitude: -78.96314 }, { name: "Konterra", latitude: 39.080797, longitude: -76.899575 }, { name: "Lake Arbor", latitude: 38.90957, longitude: -76.830236 }, { name: "Lake Shore", latitude: 39.091978, longitude: -76.489628 }, { name: "Landover Hills", latitude: 38.942465, longitude: -76.894505 }, { name: "Langley Park", latitude: 38.989499, longitude: -76.980742 }, { name: "Lansdowne", latitude: 39.235573, longitude: -76.664629 }, { name: "La Vale", latitude: 39.67164, longitude: -78.826759 }, { name: "Layhill", latitude: 39.089652, longitude: -77.039971 }, { name: "Laytonsville", latitude: 39.208748, longitude: -77.135313 }, { name: "Leisure World", latitude: 39.104079, longitude: -77.06894 }, { name: "Leitersburg", latitude: 39.692806, longitude: -77.620642 }, { name: "Leonardtown", latitude: 38.303891, longitude: -76.639653 }, { name: "Lewistown", latitude: 39.54021, longitude: -77.420555 }, { name: "Libertytown", latitude: 39.48911, longitude: -77.256919 }, { name: "Linganore", latitude: 39.413664, longitude: -77.301232 }, { name: "Linthicum", latitude: 39.209567, longitude: -76.664741 }, { name: "Lisbon", latitude: 39.336634, longitude: -77.070491 }, { name: "Little Orleans", latitude: 39.630509, longitude: -78.395843 }, { name: "Lochearn", latitude: 39.34793, longitude: -76.727214 }, { name: "Loch Lynn Heights", latitude: 39.391832, longitude: -79.372671 }, { name: "Lonaconing", latitude: 39.565581, longitude: -78.978791 }, { name: "Long Beach", latitude: 38.45884, longitude: -76.473643 }, { name: "Luke", latitude: 39.478781, longitude: -79.058903 }, { name: "Lusby", latitude: 38.362356, longitude: -76.437915 }, { name: "Lutherville", latitude: 39.423965, longitude: -76.61767 }, { name: "McCoole", latitude: 39.453357, longitude: -78.973088 }, { name: "Madison", latitude: 38.508928, longitude: -76.204222 }, { name: "Manchester", latitude: 39.653159, longitude: -76.886735 }, { name: "Mapleville", latitude: 39.535702, longitude: -77.646244 }, { name: "Mardela Springs", latitude: 38.457629, longitude: -75.755482 }, { name: "Marlboro Meadows", latitude: 38.841853, longitude: -76.711958 }, { name: "Marlboro Village", latitude: 38.834902, longitude: -76.768824 }, { name: "Marlow Heights", latitude: 38.821119, longitude: -76.944294 }, { name: "Marlton", latitude: 38.761088, longitude: -76.786536 }, { name: "Martin's Additions", latitude: 38.979552, longitude: -77.069235 }, { name: "Marydel", latitude: 39.113071, longitude: -75.74951 }, { name: "Maryland City", latitude: 39.101609, longitude: -76.805188 }, { name: "Maryland Park", latitude: 38.889009, longitude: -76.907551 }, { name: "Maugansville", latitude: 39.693681, longitude: -77.747151 }, { name: "Mayo", latitude: 38.904436, longitude: -76.512842 }, { name: "Mays Chapel", latitude: 39.442523, longitude: -76.658627 }, { name: "Mechanicsville", latitude: 38.42743, longitude: -76.74664 }, { name: "Melwood", latitude: 38.801893, longitude: -76.841624 }, { name: "Mercersville", latitude: 39.49921, longitude: -77.765825 }, { name: "Middleburg", latitude: 39.71768, longitude: -77.723878 }, { name: "Middle River", latitude: 39.339519, longitude: -76.428702 }, { name: "Middletown", latitude: 39.44134, longitude: -77.535216 }, { name: "Midland", latitude: 39.589623, longitude: -78.9487 }, { name: "Midlothian", latitude: 39.631868, longitude: -78.951501 }, { name: "Milford Mill", latitude: 39.348002, longitude: -76.769409 }, { name: "Millington", latitude: 39.261444, longitude: -75.84259 }, { name: "Mitchellville", latitude: 38.936432, longitude: -76.808246 }, { name: "Monrovia", latitude: 39.359462, longitude: -77.274891 }, { name: "Montgomery Village", latitude: 39.188512, longitude: -77.205143 }, { name: "Morningside", latitude: 38.826587, longitude: -76.889622 }, { name: "Moscow", latitude: 39.538292, longitude: -79.009674 }, { name: "Mount Aetna", latitude: 39.59888, longitude: -77.612768 }, { name: "Mountain Lake Park", latitude: 39.40003, longitude: -79.381051 }, { name: "Mount Briar", latitude: 39.442571, longitude: -77.687346 }, { name: "Mount Lena", latitude: 39.554029, longitude: -77.621891 }, { name: "Mount Rainier", latitude: 38.942361, longitude: -76.964578 }, { name: "Mount Savage", latitude: 39.696684, longitude: -78.876833 }, { name: "Mount Vernon", latitude: 38.239562, longitude: -75.785308 }, { name: "Myersville", latitude: 39.510233, longitude: -77.571363 }, { name: "Nanticoke", latitude: 38.265488, longitude: -75.886973 }, { name: "Nanticoke Acres", latitude: 38.257873, longitude: -75.905841 }, { name: "National", latitude: 39.611928, longitude: -78.940444 }, { name: "National Harbor", latitude: 38.783231, longitude: -77.008114 }, { name: "Naval Academy", latitude: 38.984784, longitude: -76.482628 }, { name: "Newark", latitude: 38.268109, longitude: -75.288638 }, { name: "New Market", latitude: 39.39142, longitude: -77.273431 }, { name: "New Windsor", latitude: 39.54295, longitude: -77.099157 }, { name: "Nikep", latitude: 39.551568, longitude: -78.99773 }, { name: "North Beach", latitude: 38.707736, longitude: -76.5345 }, { name: "North Bethesda", latitude: 39.036753, longitude: -77.120336 }, { name: "North Brentwood", latitude: 38.944806, longitude: -76.950742 }, { name: "North Chevy Chase", latitude: 39.002145, longitude: -77.074145 }, { name: "North East", latitude: 39.608037, longitude: -75.941655 }, { name: "North Kensington", latitude: 39.039196, longitude: -77.071562 }, { name: "North Laurel", latitude: 39.128849, longitude: -76.846597 }, { name: "North Potomac", latitude: 39.09677, longitude: -77.238503 }, { name: "Oakland", latitude: 39.417143, longitude: -79.402352 }, { name: "Ocean", latitude: 39.601533, longitude: -78.945033 }, { name: "Ocean Pines", latitude: 38.384784, longitude: -75.148009 }, { name: "Oldtown", latitude: 39.544966, longitude: -78.614743 }, { name: "Overlea", latitude: 39.364143, longitude: -76.517548 }, { name: "Owings", latitude: 38.711906, longitude: -76.603723 }, { name: "Owings Mills", latitude: 39.410494, longitude: -76.789371 }, { name: "Oxford", latitude: 38.687407, longitude: -76.168182 }, { name: "Paramount-Long Meadow", latitude: 39.679776, longitude: -77.692106 }, { name: "Parole", latitude: 38.987941, longitude: -76.552716 }, { name: "Parsonsburg", latitude: 38.391673, longitude: -75.479581 }, { name: "Pasadena", latitude: 39.155703, longitude: -76.559807 }, { name: "Pecktonville", latitude: 39.665971, longitude: -78.048184 }, { name: "Peppermill Village", latitude: 38.893998, longitude: -76.887694 }, { name: "Perry Hall", latitude: 39.406741, longitude: -76.477793 }, { name: "Perryman", latitude: 39.464679, longitude: -76.215865 }, { name: "Perryville", latitude: 39.572968, longitude: -76.064923 }, { name: "Pinesburg", latitude: 39.626891, longitude: -77.856121 }, { name: "Piney Point", latitude: 38.147854, longitude: -76.522283 }, { name: "Pittsville", latitude: 38.394002, longitude: -75.407178 }, { name: "Pleasant Grove", latitude: 39.680201, longitude: -78.690514 }, { name: "Pleasant Hills", latitude: 39.486182, longitude: -76.393933 }, { name: "Pocomoke City", latitude: 38.063177, longitude: -75.559199 }, { name: "Point of Rocks", latitude: 39.278194, longitude: -77.529101 }, { name: "Pomfret", latitude: 38.569904, longitude: -77.030372 }, { name: "Pondsville", latitude: 39.62341, longitude: -77.591619 }, { name: "Poolesville", latitude: 39.141827, longitude: -77.410508 }, { name: "Port Deposit", latitude: 39.611015, longitude: -76.099006 }, { name: "Port Tobacco Village", latitude: 38.512292, longitude: -77.020487 }, { name: "Potomac Heights", latitude: 38.598207, longitude: -77.132328 }, { name: "Potomac Park", latitude: 39.612588, longitude: -78.808251 }, { name: "Powellville", latitude: 38.330069, longitude: -75.374698 }, { name: "Preston", latitude: 38.710212, longitude: -75.907528 }, { name: "Prince Frederick", latitude: 38.543791, longitude: -76.587906 }, { name: "Princess Anne", latitude: 38.205137, longitude: -75.696384 }, { name: "Pylesville", latitude: 39.687921, longitude: -76.388427 }, { name: "Quantico", latitude: 38.379983, longitude: -75.755014 }, { name: "Queen Anne", latitude: 38.91907, longitude: -75.953567 }, { name: "Queensland", latitude: 38.795192, longitude: -76.795634 }, { name: "Queenstown", latitude: 38.986731, longitude: -76.163342 }, { name: "Randallstown", latitude: 39.371868, longitude: -76.801926 }, { name: "Rawlings", latitude: 39.539229, longitude: -78.886762 }, { name: "Redland", latitude: 39.132868, longitude: -77.145448 }, { name: "Reid", latitude: 39.712508, longitude: -77.679313 }, { name: "Reisterstown", latitude: 39.454112, longitude: -76.816023 }, { name: "Ridgely", latitude: 38.952933, longitude: -75.882704 }, { name: "Ringgold", latitude: 39.709169, longitude: -77.568892 }, { name: "Rising Sun", latitude: 39.701401, longitude: -76.060181 }, { name: "Riva", latitude: 38.945219, longitude: -76.589928 }, { name: "Riverside", latitude: 39.480652, longitude: -76.241866 }, { name: "Riviera Beach", latitude: 39.164543, longitude: -76.527418 }, { name: "Robinwood", latitude: 39.62647, longitude: -77.662668 }, { name: "Rock Hall", latitude: 39.140541, longitude: -76.240491 }, { name: "Rock Point", latitude: 38.275467, longitude: -76.84275 }, { name: "Rohrersville", latitude: 39.434897, longitude: -77.665474 }, { name: "Romancoke", latitude: 38.891825, longitude: -76.36078 }, { name: "Rosaryville", latitude: 38.767603, longitude: -76.828092 }, { name: "Rosedale", latitude: 39.327159, longitude: -76.508377 }, { name: "Rosemont", latitude: 39.333729, longitude: -77.62276 }, { name: "Rossville", latitude: 39.356566, longitude: -76.477894 }, { name: "Sabillasville", latitude: 39.699364, longitude: -77.457277 }, { name: "St. George Island", latitude: 38.11554, longitude: -76.476965 }, { name: "St. James", latitude: 39.573816, longitude: -77.748248 }, { name: "St. Leonard", latitude: 38.467369, longitude: -76.49863 }, { name: "St. Michaels", latitude: 38.788451, longitude: -76.222622 }, { name: "Sandy Hook", latitude: 39.328505, longitude: -77.705149 }, { name: "San Mar", latitude: 39.552216, longitude: -77.640901 }, { name: "Savage", latitude: 39.146463, longitude: -76.821637 }, { name: "Scaggsville", latitude: 39.140653, longitude: -76.882576 }, { name: "Seabrook", latitude: 38.982111, longitude: -76.851115 }, { name: "Seat Pleasant", latitude: 38.894689, longitude: -76.899451 }, { name: "Secretary", latitude: 38.608193, longitude: -75.946955 }, { name: "Severna Park", latitude: 39.086633, longitude: -76.564883 }, { name: "Shady Side", latitude: 38.829011, longitude: -76.519647 }, { name: "Shaft", latitude: 39.622319, longitude: -78.942893 }, { name: "Sharpsburg", latitude: 39.457618, longitude: -77.749312 }, { name: "Sharptown", latitude: 38.538188, longitude: -75.718957 }, { name: "Silver Hill", latitude: 38.839841, longitude: -76.938593 }, { name: "Smith Island", latitude: 37.97722, longitude: -76.028791 }, { name: "Smithsburg", latitude: 39.654733, longitude: -77.579941 }, { name: "Snow Hill", latitude: 38.172519, longitude: -75.390403 }, { name: "Solomons", latitude: 38.339089, longitude: -76.461901 }, { name: "Somerset", latitude: 38.96657, longitude: -77.096319 }, { name: "South Kensington", latitude: 39.016149, longitude: -77.066293 }, { name: "South Laurel", latitude: 39.061466, longitude: -76.846194 }, { name: "Spencerville", latitude: 39.120024, longitude: -76.983527 }, { name: "Springdale", latitude: 38.935802, longitude: -76.844233 }, { name: "Spring Gap", latitude: 39.565335, longitude: -78.705332 }, { name: "Spring Ridge", latitude: 39.405041, longitude: -77.33988 }, { name: "Stevensville", latitude: 38.974441, longitude: -76.318511 }, { name: "Still Pond", latitude: 39.325957, longitude: -76.042355 }, { name: "Stockton", latitude: 38.065387, longitude: -75.40842 }, { name: "Sudlersville", latitude: 39.183261, longitude: -75.85354 }, { name: "Summerfield", latitude: 38.904538, longitude: -76.868297 }, { name: "Swanton", latitude: 39.459864, longitude: -79.232695 }, { name: "Sykesville", latitude: 39.372069, longitude: -76.97172 }, { name: "Tall Timbers", latitude: 38.169777, longitude: -76.539277 }, { name: "Taneytown", latitude: 39.657516, longitude: -77.168973 }, { name: "Taylors Island", latitude: 38.471135, longitude: -76.316009 }, { name: "Templeville", latitude: 39.136364, longitude: -75.766849 }, { name: "Ten Mile Creek", latitude: 39.229361, longitude: -77.298206 }, { name: "Thurmont", latitude: 39.619976, longitude: -77.407695 }, { name: "Tilghman Island", latitude: 38.703188, longitude: -76.336282 }, { name: "Tilghmanton", latitude: 39.528604, longitude: -77.743683 }, { name: "Timonium", latitude: 39.445744, longitude: -76.600758 }, { name: "Tolchester", latitude: 39.218004, longitude: -76.232083 }, { name: "Trappe", latitude: 38.663461, longitude: -76.05192 }, { name: "Travilah", latitude: 39.059263, longitude: -77.250633 }, { name: "Trego-Rohrersville Station", latitude: 39.429107, longitude: -77.674853 }, { name: "Tyaskin", latitude: 38.320408, longitude: -75.872635 }, { name: "Union Bridge", latitude: 39.571028, longitude: -77.172753 }, { name: "University Park", latitude: 38.971867, longitude: -76.944432 }, { name: "Vale Summit", latitude: 39.615229, longitude: -78.908354 }, { name: "Vienna", latitude: 38.481132, longitude: -75.831813 }, { name: "Walker Mill", latitude: 38.875642, longitude: -76.885442 }, { name: "Walkersville", latitude: 39.474323, longitude: -77.363352 }, { name: "Washington Grove", latitude: 39.140699, longitude: -77.174533 }, { name: "Waterview", latitude: 38.248736, longitude: -75.900009 }, { name: "West Denton", latitude: 38.889605, longitude: -75.83884 }, { name: "Westernport", latitude: 39.487468, longitude: -79.042087 }, { name: "West Laurel", latitude: 39.115711, longitude: -76.892291 }, { name: "West Ocean City", latitude: 38.347321, longitude: -75.111243 }, { name: "Westphalia", latitude: 38.840142, longitude: -76.824037 }, { name: "West Pocomoke", latitude: 38.096527, longitude: -75.579197 }, { name: "Whaleyville", latitude: 38.387037, longitude: -75.297538 }, { name: "Whitehaven", latitude: 38.269761, longitude: -75.79474 }, { name: "White Marsh", latitude: 39.381546, longitude: -76.45842 }, { name: "White Oak", latitude: 39.044212, longitude: -76.987251 }, { name: "Wildewood", latitude: 38.30351, longitude: -76.547052 }, { name: "Willards", latitude: 38.392425, longitude: -75.349406 }, { name: "Williamsport", latitude: 39.597345, longitude: -77.817972 }, { name: "Williston", latitude: 38.830321, longitude: -75.851454 }, { name: "Wilson-Conococheague", latitude: 39.651247, longitude: -77.827072 }, { name: "Woodland", latitude: 39.608341, longitude: -78.950111 }, { name: "Woodlawn", latitude: 38.950327, longitude: -76.900261 }, { name: "Woodmore", latitude: 38.923412, longitude: -76.777931 }, { name: "Woodsboro", latitude: 39.534026, longitude: -77.30991 }, { name: "Worton", latitude: 39.270657, longitude: -76.091824 }, { name: "Yarrowsburg", latitude: 39.376172, longitude: -77.684346 }, { name: "Zihlman", latitude: 39.674021, longitude: -78.913129 } ]; const cityAreaOverrides = { Cameroon: { Douala: [ { name: "Akwa", x: 47, y: 48 }, { name: "Bonaberi", x: 19, y: 56 }, { name: "Bonamoussadi", x: 58, y: 25 }, { name: "Bepanda", x: 39, y: 32 }, { name: "Deido", x: 35, y: 44 }, { name: "Logbessou", x: 70, y: 19 }, { name: "Ndokoti", x: 65, y: 55 }, { name: "Makepe", x: 55, y: 36 } ], Yaounde: [ { name: "Bastos", x: 44, y: 29 }, { name: "Mvan", x: 57, y: 70 }, { name: "Biyem-Assi", x: 31, y: 61 }, { name: "Mokolo", x: 38, y: 44 }, { name: "Essos", x: 60, y: 42 }, { name: "Etoudi", x: 51, y: 20 }, { name: "Nlongkak", x: 48, y: 38 } ], Bamenda: [ { name: "Commercial Avenue", x: 46, y: 50 }, { name: "Nkwen", x: 60, y: 31 }, { name: "Mile 4", x: 36, y: 62 }, { name: "Up Station", x: 42, y: 27 }, { name: "Food Market", x: 53, y: 56 } ], Buea: [ { name: "Molyko", x: 55, y: 47 }, { name: "Mile 17", x: 42, y: 61 }, { name: "Great Soppo", x: 47, y: 37 }, { name: "Buea Town", x: 36, y: 49 }, { name: "Bonduma", x: 62, y: 33 } ], Kumba: [ { name: "Kumba Town", x: 48, y: 50 }, { name: "Fiango", x: 60, y: 43 }, { name: "Mbonge Road", x: 36, y: 60 }, { name: "Buea Road", x: 56, y: 30 }, { name: "Main Market", x: 45, y: 56 } ], Limbe: [ { name: "Down Beach", x: 54, y: 66 }, { name: "Mile 4", x: 43, y: 43 }, { name: "Bota", x: 36, y: 55 }, { name: "New Town", x: 59, y: 42 }, { name: "Church Street", x: 48, y: 52 } ], Bafoussam: [ { name: "Tamja", x: 45, y: 39 }, { name: "Kamkop", x: 57, y: 27 }, { name: "Banengo", x: 39, y: 57 }, { name: "Djeleng", x: 65, y: 62 }, { name: "Tougang", x: 30, y: 36 } ] }, Ghana: { Accra: [ { name: "Osu", x: 56, y: 59 }, { name: "Madina", x: 63, y: 32 }, { name: "Kaneshie", x: 36, y: 55 }, { name: "Airport", x: 51, y: 39 }, { name: "Tema", x: 78, y: 50 } ] }, Kenya: { Nairobi: [ { name: "CBD", x: 50, y: 50 }, { name: "Westlands", x: 38, y: 38 }, { name: "Kilimani", x: 44, y: 62 }, { name: "Eastleigh", x: 62, y: 39 }, { name: "Embakasi", x: 75, y: 58 } ] }, Nigeria: { Lagos: [ { name: "Ikeja", x: 48, y: 32 }, { name: "Yaba", x: 44, y: 55 }, { name: "Lekki", x: 70, y: 68 }, { name: "Surulere", x: 35, y: 54 }, { name: "Victoria Island", x: 58, y: 72 } ] }, Senegal: { Dakar: [ { name: "Plateau", x: 71, y: 66 }, { name: "Medina", x: 62, y: 58 }, { name: "Grand Yoff", x: 43, y: 44 }, { name: "Ouakam", x: 32, y: 33 }, { name: "Pikine", x: 22, y: 52 } ] }, "United States": { Maryland: marylandLaunchPlaces.map((place) => ({ name: place.name, x: place.x ?? marylandPlaceMapX(place.longitude), y: place.y ?? marylandPlaceMapY(place.latitude) })) } }; const launchGpsTownCenters = { "United States": { Maryland: marylandLaunchPlaces.map((place) => ({ name: place.name, latitude: place.latitude, longitude: place.longitude })) } }; function genericAreas(city) { return [ { name: `${city} Center`, x: 48, y: 50 }, { name: "Main Market", x: 37, y: 57 }, { name: "Bus Station", x: 59, y: 61 }, { name: "University Area", x: 43, y: 34 }, { name: "Airport Area", x: 68, y: 39 } ]; } function buildCountries() { return Object.fromEntries( Object.entries(countryCities).map(([country, cities]) => [ country, Object.fromEntries(cities.map((city) => [city, cityAreaOverrides[country]?.[city] ?? genericAreas(city)])) ]) ); } const countries = buildCountries(); const riderPickupEtaSpeedKmh = { car: 48 }; const riderPickupMaxEtaMinutes = 15; const scheduledRiderPickupMaxEtaMinutes = 30; const riderDestinationFilterNeighborRadiusMiles = 12; const riderProximityLimit = { car: 9.0 }; const carMakeCatalog = { Toyota: ["Camry", "Corolla", "RAV4", "Highlander", "Sienna", "Prius"], Honda: ["Accord", "Civic", "CR-V", "Pilot", "Odyssey", "HR-V"], Nissan: ["Altima", "Sentra", "Rogue", "Pathfinder", "Murano", "Versa"], Hyundai: ["Elantra", "Sonata", "Tucson", "Santa Fe", "Kona", "Venue"], Kia: ["Forte", "K5", "Sportage", "Sorento", "Soul", "Telluride"], Ford: ["Fusion", "Escape", "Explorer", "Edge", "Focus", "Taurus"], Chevrolet: ["Malibu", "Equinox", "Traverse", "Impala", "Trax", "Suburban"], Subaru: ["Outback", "Forester", "Impreza", "Legacy", "Crosstrek", "Ascent"], Mazda: ["Mazda3", "Mazda6", "CX-5", "CX-9", "CX-30", "CX-50"], Volkswagen: ["Jetta", "Passat", "Tiguan", "Atlas", "Golf", "Taos"], Tesla: ["Model 3", "Model Y", "Model S", "Model X"], Mercedes: ["C-Class", "E-Class", "GLC", "GLE", "A-Class", "Metris"], BMW: ["3 Series", "5 Series", "X1", "X3", "X5", "X7"], Audi: ["A3", "A4", "A6", "Q3", "Q5", "Q7"], Lexus: ["ES", "IS", "RX", "NX", "GX", "UX"], Acura: ["TLX", "ILX", "RDX", "MDX", "Integra"], Infiniti: ["Q50", "QX50", "QX60", "QX80"], Volvo: ["S60", "S90", "XC40", "XC60", "XC90"], Other: ["Sedan", "SUV", "Minivan", "Wagon", "Hatchback"] }; const carColors = ["Black", "White", "Silver", "Gray", "Blue", "Red", "Green", "Brown", "Gold", "Other"]; const carBodyTypeOptions = [ { value: "sedan", label: "Sedan" }, { value: "suv", label: "SUV" }, { value: "hatchback", label: "Hatchback" }, { value: "minivan", label: "Minivan" }, { value: "wagon", label: "Wagon" }, { value: "pickup", label: "Pickup" }, { value: "coupe", label: "Coupe" }, { value: "convertible", label: "Convertible" }, { value: "luxury", label: "Luxury" } ]; const carTypePreferenceOptions = [ { value: "sedan", label: "Normal" }, { value: "suv", label: "XL/Special" } ]; const riderVehicleDesignationOptions = [ { value: "normal", label: "Normal" }, { value: "xl_special", label: "XL/Special" }, { value: "both", label: "Both" } ]; const rideStopsMaxCount = 4; const rideStopMaxLength = 160; const riderOfferNoteMaxLength = 240; const minimumVehicleYear = 2008; const fareGuidanceConfig = { baseFareUsd: 4, perMileUsd: 0.78, perMinuteUsd: 0.28, perStopUsd: 2.5, perStopMinutes: 8, stopDistanceMultiplier: 0.08, minFareUsd: 8, maxMultiplier: 1.25, minMultiplier: 0.9, fuelIndex: 1.03, distanceStepMiles: 0.5, minuteStep: 5, benchmarkTripMinutes: "30-36", benchmarkTripFareUsd: "$25-$27" }; const insurancePricingConfig = { enabled: true, defaultPerActiveMileUsd: 0.25, defaultPickupMileAllowance: 0.8, minTripInsuranceUsd: 0.75, tripDistanceMultiplier: 1, regionalRules: { "United States|Maryland": { perActiveMileUsd: 0.25, pickupMileAllowance: 0.8, minTripInsuranceUsd: 0.75, commercialLiabilityLimitUsd: 1000000, requiresTelematicsSdk: true }, "United States|New Jersey": { perActiveMileUsd: 0.35, pickupMileAllowance: 1, minTripInsuranceUsd: 1, commercialLiabilityLimitUsd: 1000000, requiresTelematicsSdk: true }, "United States|New York": { perActiveMileUsd: 0.35, pickupMileAllowance: 1.2, minTripInsuranceUsd: 1.25, commercialLiabilityLimitUsd: 1000000, requiresTelematicsSdk: true } } }; const insuranceTelemetryPeriods = Object.freeze({ period1: "p1_app_open", period2: "p2_match_accepted", period3: "p3_passenger_in_car" }); const kmToMiles = 0.621371; const metersToMiles = 0.000621371; const riderMonthlySubscriptionFee = 160; const riderWeeklySubscriptionFee = 50; const riderMonthlyAccessDays = 30; const riderWeeklyAccessDays = 7; const businessMonthlySubscriptionFee = 0; const businessPartnerMonthlySubscriptionFee = 200; const businessFreeTrialDays = 30; const businessRideServiceFeeRate = 0.1; const businessStarterPlanCode = "starter_10_percent"; const businessPartnerPlanCode = "partner_monthly"; const riderFacilitationFeeRate = 0; const subscriptionRenewalNoticeDays = 3; const stripeProcessingFeeRate = 0.029; const stripeProcessingFixedUsd = 0.3; const destinationUpdateTravelFraction = 2 / 7; const routeChangeFareConfig = { minAdditionalFareUsd: 2, minStopFareUsd: 3, afterPickupSurchargeRate: 0.15, billableMinutesPerAddedMile: 2, trafficTimeChargeMultiplier: 0.35, detourReviewMiles: 15, detourReviewMinutes: 45 }; const fareProposalAttemptLimit = 3; const passengerCancellationFeeConfig = { graceMinutes: 2, matchedBaseUsd: 3, arrivedBaseUsd: 5, perMinuteUsd: 1, capFareRatio: 0.35 }; const inProgressCancellationCompensationConfig = { fallbackEstimatedMinutes: 30, minimumFareRatio: 0.25, maximumFareRatio: 0.9 }; const riderPickupEtaRoadFactor = 1.35; const riderLiveGpsFreshMinutes = 15; const riderLiveGpsMaxAccuracyMeters = 500; const riderAvailabilityInactivityTimeoutMinutes = 8 * 60; const passengerPickupGpsFreshMinutes = 3; const passengerPickupGpsMaxAccuracyMeters = 50; const marketplaceNegotiationRefreshIntervalMs = 15 * 1000; const passengerApproachRefreshIntervalMs = 15 * 1000; const passengerNearbyRiderCountsRefreshIntervalMs = 15 * 1000; const riderMarketplaceRefreshIntervalMs = 15 * 1000; const riderMarketplaceSignedInHeartbeatIntervalMs = 5 * 60 * 1000; const riderMarketplaceActivatedRecoveryIntervalMs = 60 * 1000; const marketplaceVisibleResumeRefreshStaleMs = 5 * 1000; const marketplaceRealtimeRefreshDebounceMs = 0; const marketplaceRealtimeReconnectDelayMs = 3 * 1000; const marketplaceLoadedNoticePopupMaxAgeMs = 10 * 60 * 1000; const accountNotificationActiveRefreshIntervalMs = 1 * 1000; const accountNotificationIdleRefreshIntervalMs = 15 * 1000; const riderDropoffRequestLeadMinutes = 7; const riderAutoGpsIdleSyncIntervalMs = 5 * 60 * 1000; const riderAutoGpsMovingSyncIntervalMs = 60 * 1000; const riderAutoGpsActiveRideSyncIntervalMs = 15 * 1000; const riderAutoGpsActiveRideMinElapsedMs = 5 * 1000; const riderAutoGpsIdleHeartbeatMeters = 150; const riderAutoGpsMovingMinMovementMeters = 30; const riderAutoGpsActiveRideMinMovementMeters = 15; const riderAutoGpsSyncIntervalMs = riderAutoGpsMovingSyncIntervalMs; const riderAutoGpsMinMovementMeters = riderAutoGpsMovingMinMovementMeters; const placeDetailsCacheLimit = 100; const addressSearchRateLimitPauseMs = 30 * 1000; const testingAddressSearchRateLimitPauseMs = 0; const adminSlowRpcWarningMs = 1000; const marketplaceSyncLoadLimits = { ride_requests: 100, ride_cancellation_charges: 100, ride_payment_settlements: 100, finance_adjustments: 100, ride_tips: 100, ride_offers: 250, ride_chats: 250, ride_route_changes: 100, admin_notifications: 100, business_accounts: 50, business_subscriptions: 50, rider_tax_identity_references: 50, rider_tax_documents: 100, ride_ratings: 250, insurance_telemetry_segments: 100 }; const riderMarketplacePageSize = 40; const defaultCitySpanKm = 14; const cityDistanceSpanKm = { Cameroon: { Douala: 18, Yaounde: 16, Bamenda: 10, Bafoussam: 10, Buea: 9, Limbe: 9 }, Ghana: { Accra: 20 }, Kenya: { Nairobi: 18 }, Nigeria: { Lagos: 24 }, Senegal: { Dakar: 16 }, "United States": { Maryland: 90 } }; const rideLifecycleChatStatuses = ["matched", "arrived", "in_progress"]; const rideReportStatuses = ["matched", "arrived", "in_progress", "completed"]; const preStartCancellationStatuses = ["open", "matched", "arrived"]; const storageKey = "waka-negotiated-market-v1"; const runtimeConfigStorageKey = "waka-runtime-config-v1"; const supabaseSdkUrl = "https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"; const supabaseRequestTimeoutMs = 20000; const supabaseProfileSaveTimeoutMs = 12000; const optionalSupabaseRequestTimeoutMs = 8000; const runtimeConfigTimeoutMs = 5000; const phoneOtpCooldownMs = 60 * 1000; let appConfig = { appName: "Waka Cameroon", projectName: "waka-cameroon", mode: "demo", mapsMode: "landmarks-first", routeEstimatesProvider: "zone", routeEstimateFunctionName: "route-estimate", routeEstimateMaxUncachedPerHour: 6, routeEstimateMaxUncachedPerDay: 20, requireRouteEstimateBeforePublish: false, placesAutocompleteProvider: "none", placesAutocompleteFunctionName: "place-autocomplete", placesAutocompleteMaxRequestsPerMinute: 8, placesAutocompleteMaxRequestsPerDay: 60, placesDetailsMaxRequestsPerMinute: 4, placesDetailsMaxRequestsPerDay: 30, relaxAddressSearchLimitsForTesting: false, autoPickupGpsEnabled: true, autoRiderGpsEnabled: true, mapboxAccessToken: "", mapboxAccessTokenRestrictedToOrigins: false, mapboxAllowedOrigins: [], mapboxStyleId: "mapbox/streets-v12", mapboxTileMapsEnabled: false, passengerRequestTileMapEnabled: false, passengerApproachTileMapEnabled: false, mapboxTileRequestMonthlySoftLimit: 0, mapboxTileRequestMonthlyHardLimit: 0, riderInitializeMapZoom: 13, insuranceTelemetryEnabled: false, insurancePricingEnabled: false, telematicsSdkProvider: "", runtimeConfigFile: "", strictProductionMode: false, supabaseProjectModel: "single-project", authIsolationMode: "strict-single-tenant", enablePhoneOtpSignIn: false, turnstileRequired: true, relaxSmsVerificationForTesting: false, relaxPaymentSetupForTesting: false, relaxBackgroundCheckForTesting: false, passwordResetPhoneOtpRequired: true, passwordResetPhoneOtpFunctionName: "password-reset-phone-otp", passwordResetCompleteFunctionName: "password-reset-complete", relaxPasswordResetPhoneOtpForTesting: false, firstLaunchCountry: "Cameroon", firstLaunchCity: "Bamenda", enabledLaunchCountries: ["Cameroon"], phoneVerificationMode: "supabase", passengerOnboardingSubmitFunctionName: "passenger-onboarding-submit", riderOnboardingSubmitFunctionName: "rider-onboarding-submit", passwordResetRequestFunctionName: "password-reset-request", paymentProvider: "mtn-orange-manual", ridePaymentSettlementFunctionName: "manual-direct-payment", contactRelayFunctionName: "ride-contact-relay", backgroundCheckProvider: "manual-admin-review", taxOnboardingProvider: "not-required-for-cameroon-mvp", taxOnboardingMode: "disabled", supportPhone: "+237600000000", pushNotificationPublicKey: "", notificationDeliveryFunctionName: "notification-delivery", subscriptionReminderFunctionName: "subscription-reminders", clientEventIngestFunctionName: "client-event-ingest", clientErrorTelemetryEnabled: true, supabaseUrl: "", supabaseAnonKey: "", buckets: { riderDocuments: "rider-documents", rideImages: "ride-images", profilePhotos: "profile-photos" }, ...(window.WAKA_CONFIG ?? {}) }; let runtimeConfigSource = window.WAKA_CONFIG_SOURCE ?? (window.WAKA_CONFIG ? "window" : "default"); // Translation catalogs and language application helpers. const translations = { en: { tagline: "Negotiated car rides", passenger: "Passenger", rider: "Rider", admin: "Admin", language: "Language", installApp: "Install app", createPassenger: "Create passenger account", savePassenger: "Save passenger", postRide: "Post ride request", publishRequest: "Publish request", riderApplication: "Rider application", submitReview: "Submit for admin review", subscription: "Rider access", paySubscription: "Open rider access checkout", respondRequest: "Respond to selected request", sendOffer: "Send accept or counter-offer", passengerSignIn: "Passenger sign-in", riderSignIn: "Rider sign-in", signIn: "Sign in" }, fr: { tagline: "Courses negociees en voiture", passenger: "Passager", rider: "Conducteur", admin: "Admin", language: "Langue", installApp: "Installer", createPassenger: "Creer un compte passager", savePassenger: "Enregistrer", postRide: "Publier une demande", publishRequest: "Publier", riderApplication: "Demande conducteur", submitReview: "Envoyer pour validation", subscription: "Abonnement", paySubscription: "Ouvrir le paiement automatique", respondRequest: "Repondre a la demande", sendOffer: "Envoyer l'offre", passengerSignIn: "Connexion passager", riderSignIn: "Connexion conducteur", signIn: "Connexion" }, pcm: { tagline: "Negotiate car rides", passenger: "Passenger", rider: "Rider", admin: "Admin", language: "Language", installApp: "Install app", createPassenger: "Open passenger account", savePassenger: "Save passenger", postRide: "Post ride", publishRequest: "Publish ride", riderApplication: "Rider application", submitReview: "Send for admin check", subscription: "Subscription", paySubscription: "Open rider access checkout", respondRequest: "Answer ride request", sendOffer: "Send offer", passengerSignIn: "Passenger sign-in", riderSignIn: "Rider sign-in", signIn: "Sign in" }, de: { tagline: "Verhandelte Autofahrten", passenger: "Fahrgast", rider: "Fahrer", admin: "Admin", language: "Sprache", installApp: "App installieren", pageTitle: "Waka Verhandelte Fahrten", installed: "Installiert", chooseAccountType: "Kontotyp waehlen", continueAsPassenger: "Als Fahrgast fortfahren", continueAsRider: "Als Fahrer fortfahren", signInOrCreate: "Anmelden oder Konto erstellen", createAccount: "Konto erstellen", createPassenger: "Fahrgastkonto erstellen", savePassenger: "Fahrgast speichern", postRide: "Fahrtanfrage erstellen", publishRequest: "Anfrage veroeffentlichen", riderApplication: "Fahrerbewerbung", submitReview: "Zur Waka-Pruefung senden", subscription: "Fahrerzugang", paySubscription: "Fahrerzugang bezahlen", respondRequest: "Auf ausgewaehlte Anfrage antworten", sendOffer: "Annehmen oder Gegenangebot senden", passengerSignIn: "Fahrgast-Anmeldung", riderSignIn: "Fahrer-Anmeldung", signIn: "Anmelden", passengerPanelSubtitle: "Fahrt anfragen und bestes Angebot waehlen", riderPanelSubtitle: "Bewerben, Zugang aktivieren und Fahrten verhandeln", email: "E-Mail", password: "Passwort", phoneNumber: "Telefonnummer", otpCode: "OTP-Code", sendOtp: "OTP senden", sendCode: "Code senden", verify: "Pruefen", signOut: "Abmelden", fullName: "Vollstaendiger Name", profilePicture: "Profilbild", phoneVerificationCode: "Telefon-Bestaetigungscode", nationalIdNumber: "Identitaetsreferenz", identityReference: "Identitaetsreferenz", driverLicenseNumber: "Fuehrerscheinnummer", dateOfBirth: "Geburtsdatum", country: "Land", city: "Stadt", passengerSignInHelp: "Melden Sie sich mit E-Mail und Passwort an, bevor Sie Fahrten anfragen.", riderSignInHelp: "Melden Sie sich mit E-Mail und Passwort an, bevor Sie auf Fahrten antworten.", passengerWorkspace: "Fahrgastbereich", riderWorkspace: "Fahrerbereich", passengerSignedIn: "Fahrgast angemeldet", riderSignedIn: "Fahrer angemeldet", readyToRequestRides: "Bereit, Fahrten anzufragen.", applicationStatusWillAppear: "Der Bewerbungsstatus erscheint hier.", noPassengerSaved: "Noch kein Fahrgast gespeichert.", noRiderApplication: "Noch keine Fahrerbewerbung gespeichert.", pickupArea: "Abholgebiet", pickupDescription: "Abholbeschreibung", destination: "Ziel", rideTiming: "Fahrtzeit", asSoonAsPossible: "So bald wie moeglich", scheduleAhead: "Vorausplanen", scheduledDateTime: "Geplantes Datum und Uhrzeit", vehicle: "Fahrzeug", vehicleType: "Fahrzeugtyp", bike: "Auto", car: "Auto", bikeOrCar: "Auto", fareOffer: "Fahrpreisangebot", paymentPreference: "Zahlungsart", cashInHand: "Barzahlung", mtnMoney: "MTN Mobile Money", orangeMoney: "Orange Money", agreeWithRider: "Vor der Fahrt mit dem Fahrer einigen", optional: "Freiwillig", record: "Speichern", clear: "Loeschen", riderAccess: "Fahrerzugang", applicationStatus: "Bewerbungsstatus", riderPlatformStatus: "Ihr Fahrerstatus erscheint hier.", operatingArea: "Einsatzgebiet", credentialNumber: "Fuehrerscheinnummer", vehicleMake: "Fahrzeugmarke", vehicleModel: "Fahrzeugmodell", bodyType: "Karosserieform", vehicleDesignation: "Fahrzeugbezeichnung", yearOfManufacture: "Baujahr", vehicleColor: "Farbe", vehicleVin: "Fahrzeug-VIN", vehicleRegistration: "Kennzeichen", driverLicenseDocument: "Fuehrerscheindokument", vehicleRegistrationDocument: "Zulassungsdokument", nationalIdDocument: "Versicherungsdokument", subscriptionIntro: "Freigegebene Fahrer erhalten 30 Tage kostenlos, danach ist woechentlicher oder monatlicher Waka Fahrerzugang erforderlich.", paymentProvider: "Versicherer", paymentPhone: "Zahlungstelefon", transactionReference: "Versicherungspolicennummer", subscriptionPaymentHelp: "Waka Fahrerzugang wird vom Anbieter gehostet. Manuelle Vorauszahlung und automatische Verlaengerung sind nach der kostenlosen Testphase verfuegbar.", yourFare: "Ihr Fahrpreis", messageBeforeSelection: "Hinweis an den Fahrgast vor der Auswahl", openRequests: "Offene Anfragen", passengers: "Fahrgaeste", riders: "Fahrer", pendingRiders: "Ausstehende Fahrer", subscribed: "Abonniert", loadDemoMarket: "Demo-Marktplatz laden", clearDemoData: "Lokale Demo-Daten loeschen", selectOrPublish: "Anfrage auswaehlen oder veroeffentlichen", refreshMarket: "Marktplatz aktualisieren", all: "Alle", rideRequests: "Fahrtanfragen", riderOffers: "Fahrerangebote", accountDetail: "Kontodetail", postSelectionChat: "Chat nach Auswahl", locked: "Gesperrt", send: "Senden", chooseRider: "Fahrer waehlen", openFullReview: "Vollstaendige Pruefung oeffnen", approve: "Freigeben", decline: "Ablehnen", passengerNamePlaceholder: "Name des Fahrgasts", passwordPlaceholder: "Passwort", createPasswordPlaceholder: "Passwort erstellen", codePlaceholder: "6-stelliger Code", nationalIdPlaceholder: "Fuehrerschein, Ausweis oder Passreferenz", driverLicensePlaceholder: "Fuehrerscheinnummer", pickupDescriptionPlaceholder: "Orientierungspunkt, Gebaeudefarbe, Markt, Kreuzung, Ladenname", destinationPlaceholder: "Zielgebiet, Orientierungspunkt oder Adresse", riderNamePlaceholder: "Fahrername", credentialPlaceholder: "Fuehrerscheinnummer", registrationPlaceholder: "Kennzeichen oder Zulassungsnummer", transactionReferencePlaceholder: "Policennummer", counterFarePlaceholder: "Hoeheres Gegenangebot eingeben", counterNotePlaceholder: "Optional: kurzer Fahrzeug- oder Preishinweis fuer den Fahrgast", supabasePasswordPlaceholder: "Supabase-Passwort", chatPlaceholder: "Chat oeffnet erst, nachdem der Fahrgast einen Fahrer gewaehlt hat", safetyReportDetailsPlaceholder: "Beschreiben Sie, was Waka pruefen soll", offlineReady: "Offline bereit", onlineDemo: "Online-Demo", localMode: "Lokaler Modus", supabaseReady: "Supabase bereit", supabaseConfigNeeded: "Supabase-Konfiguration erforderlich", supabaseConnecting: "Supabase verbindet", supabaseSdkUnavailable: "Supabase SDK nicht verfuegbar", manualPhoneVerified: "Manueller Pilotmodus: Telefon als verifiziert markiert. SMS-OTP vor oeffentlichem Start konfigurieren.", smsVerificationRelaxedForTesting: "Testmodus: SMS-Telefonpruefung uebersprungen. Kontoerstellung per E-Mail/Passwort kann fortgesetzt werden; echte SMS-OTP vor oeffentlichem Start aktivieren.", validPhoneRequired: "Geben Sie eine gueltige Telefonnummer ein, bevor Sie einen Code anfordern.", validDateOfBirthRequired: "Geben Sie ein gueltiges Geburtsdatum im Format JJJJ-MM-TT ein. Sie koennen nur Ziffern tippen; Waka fuegt die Bindestriche hinzu.", checkingPassengerAccount: "Fahrgastkonto wird geprueft...", checkingRiderApplication: "Fahrerbewerbung wird geprueft...", accountMissingFields: "Fuellen Sie diese Felder vor dem Speichern aus: {fields}.", phoneOtpCooldown: "Bitte warten Sie {seconds}s, bevor Sie einen weiteren Telefoncode anfordern.", phoneOtpRateLimited: "Zu viele Telefoncode-Versuche. Warten Sie etwas und pruefen Sie Supabase Auth-Limits, falls dies weiter passiert.", sendingVerificationCode: "Bestaetigungscode wird gesendet...", verificationCodeSent: "Bestaetigungscode an {phone} gesendet.", demoCode: "Demo-Code: {code} fuer {phone}", freshVerificationCodeRequired: "Fordern Sie einen neuen Bestaetigungscode fuer diese Telefonnummer an.", verifyingPhoneNumber: "Telefonnummer wird geprueft...", phoneNumberVerified: "Telefonnummer verifiziert. Das prueft nur das Telefon; druecken Sie Speichern oder Senden, um das Waka-Konto fertigzustellen.", verificationCodeIncorrect: "Der Bestaetigungscode ist nicht korrekt.", phoneOtpManualSignIn: "Telefon-OTP-Anmeldung ist im manuellen Pilotmodus deaktiviert. Verwenden Sie E-Mail und Passwort.", sendingSignInCode: "Anmeldecode wird gesendet...", signInCodeSent: "Anmeldecode an {phone} gesendet.", demoSignInCode: "Demo-Anmeldecode: {code} fuer {phone}", signingInPassword: "Anmeldung mit E-Mail und Passwort...", loadingWakaProfile: "Waka-Profil wird geladen...", supabaseProfileMissing: "Anmeldung erfolgreich. Dieses Login braucht noch ein Waka-Profil; fuellen Sie dieses Formular aus und speichern Sie es, um das Konto zu verbinden.", wrongProfileRole: "Dieses Konto ist als {role} registriert, nicht als {type}.", wrongProfileRoleStrict: "Dieses Login ist als {role} registriert, nicht als {type}. In der aktuellen WakaGood-Bereitstellung mit einem Auth-Projekt kann dieselbe E-Mail nicht sicher beide Rollen offnen. Verwenden Sie jetzt eine separate {type}-E-Mail/Telefonnummer oder schliessen Sie die Fahrgast/Fahrer-Auth-Trennung ab, bevor diese E-Mail fur beide Rollen verwendet wird.", adminPublicPortalBlocked: "Admin-Konten koennen sich nicht ueber Fahrgast- oder Fahrerportale anmelden. Nutzen Sie den Admin-Bereich oder erstellen Sie ein separates Fahrgast-/Fahrerkonto zum Testen.", signedInPassengerLoaded: "Angemeldet als {email}. Fahrgastprofil geladen. Fuegen Sie vor Fahrtanfragen eine Fahrgast-Zahlungsmethode hinzu.", signedInRiderLoaded: "Angemeldet als {email}. Fahrerprofil geladen.", signedInAs: "Angemeldet als {identity}.", freshSignInCodeRequired: "Fordern Sie einen neuen Anmeldecode fuer diese Telefonnummer an.", signInCodeRequired: "Melden Sie sich mit E-Mail und Passwort an. Telefon-OTP ist nur fuer die erstmalige Telefonpruefung.", passwordSignInOnly: "Melden Sie sich mit E-Mail und Passwort an. Telefon-OTP ist nur fuer die erstmalige Telefonpruefung.", signInEmailPasswordRequired: "Geben Sie E-Mail und Passwort fuer dieses Konto ein. Telefoncode-Anmeldung ist im manuellen Pilotmodus deaktiviert.", signingIn: "Anmeldung...", signInCodeIncorrect: "Der Anmeldecode ist nicht korrekt.", localSignInAccountMissing: "Kein gespeichertes {type}-Konto passt zu dieser Telefonnummer. Erstellen und speichern Sie zuerst das {type}-Konto, dann melden Sie sich an.", signedOut: "Abgemeldet.", passengerPhoneBeforeSave: "Verifizieren Sie die Fahrgast-Telefonnummer vor dem Speichern.", riderPhoneBeforeReview: "Verifizieren Sie die Fahrer-Telefonnummer vor dem Einreichen.", passengerPaymentRequired: "Fuegen Sie unter Zahlung eine Fahrgast-Zahlungsmethode hinzu, bevor Sie Fahrten veroeffentlichen.", riderPaymentRequired: "Speichern Sie ein Fahrer-Zahlungskonto, bevor Sie Anfragen erhalten.", riderDailyRegionsRequired: "Bevorzugte Zielregionen sind fuer Fahrer optional.", riderLiveGpsRequired: "Teilen Sie Live-Fahrer-GPS, bevor Sie Anfragen erhalten.", startingPassengerSupabase: "Fahrgast in Supabase wird gespeichert...", savingPassenger: "Fahrgast wird gespeichert...", passengerCreated: "{name} Fahrgastkonto erfolgreich erstellt. Fuegen Sie als Naechstes eine Zahlungsmethode hinzu und fragen Sie dann Fahrten an.", passengerCreatedEmailPending: "{name} Fahrgastkonto erfolgreich erstellt. Fuegen Sie als Naechstes eine Zahlungsmethode hinzu; E-Mail/Passwort-Anmeldung kann Supabase-E-Mail-Bestaetigung oder Einrichtung benoetigen.", passengerAccountFailed: "Fahrgastkonto wurde nicht erstellt: {message}", passengerSyncing: "{name} Fahrgastkonto erfolgreich erstellt. Fuegen Sie als Naechstes eine Zahlungsmethode hinzu und fragen Sie dann Fahrten an.", startingRiderSupabase: "Fahrer in Supabase wird gespeichert...", savingRiderApplication: "Fahrerbewerbung wird gespeichert...", submittingRiderApplication: "Fahrerbewerbung wird zur Admin-Freigabe gesendet...", riderCreatedPending: "{name} Konto erstellt. Fahrerbewerbung wartet auf Admin-Freigabe. Bei Freigabe startet die 30-taegige Testphase vor woechentlichem oder monatlichem Waka Fahrerzugang.", riderAccountFailed: "Fahrerkonto wurde nicht eingereicht: {message}", missingRiderDocuments: "Laden Sie diese erforderlichen Fahrerdokumente vor der Admin-Pruefung hoch: {documents}.", passengerAccountRequired: "Erstellen Sie ein Fahrgastkonto, bevor Sie eine Fahrtanfrage veroeffentlichen.", passengerSignInRequired: "Fahrgast-Anmeldung ist erforderlich, bevor Fahrten veroeffentlicht werden.", passengerPhoneRequired: "Telefonverifizierung des Fahrgasts ist erforderlich, bevor Fahrten veroeffentlicht werden.", realisticFareRequired: "Geben Sie ein realistisches Fahrpreisangebot ein.", fareBelowGuidance: "Dieser Preis liegt unter der vorgeschlagenen Spanne {min}-{max}. Fahrer koennen ihn ueberspringen oder langsamer antworten. Trotzdem fortfahren?", fareOutsideGuidance: "Diese Routenschaetzung empfiehlt {min}-{max}. Niedrigere Preise koennen laengere Wartezeiten verursachen.", scheduledTimeRequired: "Waehlen Sie ein gueltiges Datum und eine gueltige Uhrzeit fuer die geplante Fahrt.", scheduleThirtyMinutes: "Planen Sie die Fahrt mindestens 30 Minuten ab jetzt.", ridePublishedSupabase: "Fahrtanfrage wurde in Supabase fuer berechtigte Fahrer veroeffentlicht.", ridePublishedLocal: "Fahrtanfrage lokal veroeffentlicht.", publishRideFailed: "Diese Fahrtanfrage konnte nicht veroeffentlicht werden: {message}", subscriptionReferenceRequired: "Anbieter-Checkout ist fuer Waka Fahrerzugang erforderlich.", subscriptionAlreadyPending: "Ein Anbieter-Checkout ist bereits aktiv.", submittingPaymentSupabase: "Waka Fahrerzugang-Checkout wird geoeffnet...", savingPaymentReference: "Waka Fahrerzugang-Checkout wird geoeffnet...", paymentReferenceSubmitted: "Waka Fahrerzugang-Checkout geoeffnet. Bezahlter Zugang startet nach Testphase oder aktuellem Zugangszeitraum.", paymentReferenceFailed: "Fahrerzugang-Checkout konnte nicht geoeffnet werden: {message}", selectRideRequestFirst: "Waehlen Sie zuerst eine Fahrtanfrage aus.", createRiderFirst: "Erstellen Sie zuerst ein Fahrerkonto.", riderSignInRequired: "Fahrer-Anmeldung ist erforderlich, bevor auf Fahrten geantwortet wird.", riderApprovalRequired: "Admin-Freigabe ist erforderlich, bevor auf Fahrten geantwortet wird.", riderAccessRequired: "Ihre Testphase oder Ihr bezahlter Fahrerzugang muss aktiv sein, bevor Sie auf Fahrten antworten.", selectNearbyRequest: "Waehlen Sie eine nahe Anfrage, die zu Ihrem freigegebenen Fahrerkonto passt.", requestClosed: "Diese Anfrage ist nicht mehr offen.", offerSendFailed: "Dieses Angebot konnte nicht gesendet werden: {message}", passengerOwnRequestRequired: "Nur der Fahrgast, der diese Anfrage erstellt hat, kann einen Fahrer waehlen.", chooseRiderFailed: "Dieser Fahrer konnte nicht gewaehlt werden: {message}", safetyReportUnavailable: "Meldungen sind verfuegbar, nachdem ein Fahrgast einen Fahrer gewaehlt hat. Waka-Kontakt oeffnet sich nach der Fahrerauswahl.", safetyReportNeedsDetail: "Fuegen Sie genug Details hinzu, damit Waka die Anfrage versteht.", safetyReportSignInRequired: "Melden Sie sich erneut an, bevor Sie Waka kontaktieren.", submittingSafetySupabase: "Nachricht an Waka wird gesendet...", savingSafetyReport: "Nachricht wird fuer Waka-Pruefung gespeichert...", safetyReportSubmitted: "Sicherheitsmeldung zur Admin-Pruefung gesendet. Nachricht an Waka gesendet.", safetyReportFailed: "Meldung konnte nicht gesendet werden: {message}", suspendRiderConfirm: "Diesen Fahrer sperren? Er sieht und akzeptiert sofort keine Fahrtanfragen mehr.", clearDemoConfirm: "Alle lokal gespeicherten Demo-Daten loeschen?", requestConfirmationFailed: "Bestaetigung konnte nicht angefordert werden: {message}", confirmScheduledFailed: "Diese geplante Fahrt konnte nicht bestaetigt werden: {message}", reopenScheduledFailed: "Diese geplante Fahrt konnte nicht wieder geoeffnet werden: {message}", stop: "Stoppen", androidInstallHelp: "Auf Android diese Seite in Chrome oeffnen, Menue antippen und Zum Startbildschirm hinzufuegen oder App installieren waehlen." }, ar: { tagline: "رحلات دراجة وسيارة قابلة للتفاوض", passenger: "راكب", rider: "سائق", admin: "مشرف", language: "اللغة", installApp: "تثبيت", createPassenger: "انشاء حساب راكب", savePassenger: "حفظ الراكب", postRide: "طلب رحلة", publishRequest: "نشر الطلب", riderApplication: "طلب السائق", submitReview: "ارسال للمراجعة", subscription: "اشتراك", paySubscription: "Open rider access checkout", respondRequest: "الرد على الطلب", sendOffer: "ارسال العرض", passengerSignIn: "دخول الراكب", riderSignIn: "دخول السائق", signIn: "دخول" }, sw: { tagline: "Safari za gari kwa maelewano", passenger: "Abiria", rider: "Dereva", admin: "Msimamizi", language: "Lugha", installApp: "Sakinisha", createPassenger: "Fungua akaunti ya abiria", savePassenger: "Hifadhi abiria", postRide: "Tuma ombi la safari", publishRequest: "Chapisha ombi", riderApplication: "Ombi la dereva", submitReview: "Tuma kwa ukaguzi", subscription: "Usajili", paySubscription: "Fungua malipo ya usajili kiotomatiki", respondRequest: "Jibu ombi", sendOffer: "Tuma ofa", passengerSignIn: "Kuingia abiria", riderSignIn: "Kuingia dereva", signIn: "Ingia" }, pt: { tagline: "Viagens negociadas de carro", passenger: "Passageiro", rider: "Motorista", admin: "Admin", language: "Idioma", installApp: "Instalar", createPassenger: "Criar conta de passageiro", savePassenger: "Guardar passageiro", postRide: "Publicar pedido", publishRequest: "Publicar", riderApplication: "Pedido de motorista", submitReview: "Enviar para revisao", subscription: "Subscricao", paySubscription: "Abrir pagamento automatico da subscricao", respondRequest: "Responder ao pedido", sendOffer: "Enviar oferta", passengerSignIn: "Entrada passageiro", riderSignIn: "Entrada motorista", signIn: "Entrar" }, es: { tagline: "Viajes en auto negociados", passenger: "Pasajero", rider: "Conductor", admin: "Admin", language: "Idioma", installApp: "Instalar", createPassenger: "Crear cuenta de pasajero", savePassenger: "Guardar pasajero", postRide: "Publicar solicitud", publishRequest: "Publicar", riderApplication: "Solicitud de conductor", submitReview: "Enviar para revision", subscription: "Suscripcion", paySubscription: "Abrir pago automatico de suscripcion", respondRequest: "Responder a solicitud", sendOffer: "Enviar oferta", passengerSignIn: "Ingreso de pasajero", riderSignIn: "Ingreso de conductor", signIn: "Ingresar" } }; const translationAdditions = { en: { pageTitle: "Waka Negotiated Rides", installed: "Installed", chooseAccountType: "Choose account type", continueAsPassenger: "Continue as passenger", continueAsRider: "Continue as rider", signInOrCreate: "Sign in or create account", createAccount: "Create account", passengerPanelSubtitle: "Request a ride and choose the best offer", riderPanelSubtitle: "Apply, subscribe, then negotiate rides", email: "Email", password: "Password", phoneNumber: "Phone number", otpCode: "OTP code", sendOtp: "Send OTP", sendCode: "Send code", verify: "Verify", signOut: "Sign out", fullName: "Full name", profilePicture: "Profile picture", phoneVerificationCode: "Phone verification code", nationalIdNumber: "Identity reference", identityReference: "Identity reference", driverLicenseNumber: "Driver's license number", dateOfBirth: "Date of birth", country: "Country", city: "City", passengerSignInHelp: "Use email and password to sign in before requesting rides.", riderSignInHelp: "Use email and password to sign in before responding to rides.", passengerWorkspace: "Passenger workspace", riderWorkspace: "Rider workspace", passengerSignedIn: "Passenger signed in", riderSignedIn: "Rider signed in", readyToRequestRides: "Ready to request rides.", applicationStatusWillAppear: "Application status will appear here.", noPassengerSaved: "No passenger saved yet.", noRiderApplication: "No rider application saved yet.", pickupArea: "Pickup area", pickupDescription: "Pickup description", destination: "Destination", rideTiming: "Ride timing", asSoonAsPossible: "As soon as possible", scheduleAhead: "Schedule ahead", scheduledDateTime: "Scheduled date and time", vehicle: "Vehicle", vehicleType: "Vehicle designation", bike: "Car", car: "Car", bikeOrCar: "Car", fareOffer: "Fare offer", paymentPreference: "Payment preference", cashInHand: "Cash in hand", mtnMoney: "MTN Mobile Money", orangeMoney: "Orange Money", agreeWithRider: "Agree with rider before ride", optional: "Optional", record: "Record", clear: "Clear", riderAccess: "Rider access", applicationStatus: "Application status", riderPlatformStatus: "Your rider platform status will appear here.", operatingArea: "Operating area", credentialNumber: "Driver's license number", vehicleMake: "Vehicle make", vehicleModel: "Vehicle model", bodyType: "Body type", vehicleDesignation: "Vehicle designation", yearOfManufacture: "Year of manufacture", vehicleColor: "Color", vehicleVin: "Vehicle VIN", vehicleRegistration: "Plate number", driverLicenseDocument: "License document upload", vehicleRegistrationDocument: "Registration document", nationalIdDocument: "Insurance document", subscriptionIntro: "Approved riders get a 30-day free trial before weekly or monthly Waka Rider Access is required.", paymentProvider: "Insurance provider", paymentPhone: "Payment phone", transactionReference: "Insurance policy number", subscriptionPaymentHelp: "Waka Rider Access is provider-hosted. Manual upfront payments and automatic renewal are available after the free trial.", yourFare: "Your fare", messageBeforeSelection: "Note to passenger before selection", openRequests: "Open requests", passengers: "Passengers", riders: "Riders", pendingRiders: "Pending riders", subscribed: "Subscribed", loadDemoMarket: "Load demo market", clearDemoData: "Clear local demo data", selectOrPublish: "Select or publish a request", refreshMarket: "Refresh market", all: "All", rideRequests: "Ride requests", riderOffers: "Rider offers", accountDetail: "Account detail", postSelectionChat: "Post-selection chat", locked: "Locked", send: "Send", chooseRider: "Choose rider", openFullReview: "Open full review", approve: "Approve", decline: "Decline", passengerNamePlaceholder: "Passenger name", passwordPlaceholder: "Password", createPasswordPlaceholder: "Create a password", codePlaceholder: "6-digit code", nationalIdPlaceholder: "Driver license, state ID, or passport reference", driverLicensePlaceholder: "Driver's license number", pickupDescriptionPlaceholder: "Landmark, building color, market, junction, shop name", destinationPlaceholder: "Destination area, landmark, or address", riderNamePlaceholder: "Rider or driver name", credentialPlaceholder: "Driver's license number", registrationPlaceholder: "Plate or registration number", transactionReferencePlaceholder: "Policy number", counterFarePlaceholder: "Enter a higher counter-offer fare", counterNotePlaceholder: "Optional: brief vehicle or fare note for the passenger", supabasePasswordPlaceholder: "Supabase password", chatPlaceholder: "Chat opens only after passenger chooses a rider", safetyReportDetailsPlaceholder: "Describe what Waka should review", offlineReady: "Offline-ready", onlineDemo: "Online demo", localMode: "Local mode", supabaseReady: "Supabase ready", supabaseConfigNeeded: "Supabase config needed", supabaseConnecting: "Supabase connecting", supabaseSdkUnavailable: "Supabase SDK unavailable", manualPhoneVerified: "Manual pilot mode: phone marked verified. Configure SMS OTP before public launch.", smsVerificationRelaxedForTesting: "Testing mode: SMS phone verification skipped. Email/password account creation can continue; enable real SMS OTP before public launch.", validPhoneRequired: "Enter a valid phone number before requesting a code.", validDateOfBirthRequired: "Enter a valid date of birth as YYYY-MM-DD. You can type only digits and Waka will add the dashes.", checkingPassengerAccount: "Checking passenger account details...", checkingRiderApplication: "Checking rider application details...", accountMissingFields: "Complete these fields before saving: {fields}.", phoneOtpCooldown: "Please wait {seconds}s before requesting another phone code.", phoneOtpRateLimited: "Too many phone-code attempts. Wait a while before requesting another code, and check Supabase Auth rate limits if this continues.", sendingVerificationCode: "Sending verification code...", verificationCodeSent: "Verification code sent to {phone}.", demoCode: "Demo code: {code} for {phone}", freshVerificationCodeRequired: "Request a fresh verification code for this phone number.", verifyingPhoneNumber: "Verifying phone number...", phoneNumberVerified: "Phone number verified. This only verifies the phone; press Save or Submit to finish creating the Waka account.", verificationCodeIncorrect: "Verification code is not correct.", phoneOtpManualSignIn: "Phone OTP sign-in is disabled in manual pilot mode. Use email and password to sign in.", sendingSignInCode: "Sending sign-in code...", signInCodeSent: "Sign-in code sent to {phone}.", demoSignInCode: "Demo sign-in code: {code} for {phone}", signingInPassword: "Signing in with email and password...", loadingWakaProfile: "Loading Waka profile...", supabaseProfileMissing: "Sign-in worked. This login still needs a Waka profile; complete and save this form to finish linking the account.", wrongProfileRole: "This account is registered as {role}, not {type}.", wrongProfileRoleStrict: "This login is registered as {role}, not {type}. In this WakaGood deployment's single Auth project, the same email cannot safely open both roles. Use a separate {type} email/phone now, or complete the passenger/rider Auth split before reusing this email for both roles.", adminPublicPortalBlocked: "Admin accounts cannot sign in through passenger or rider portals. Use the Admin workspace, or create a separate passenger/rider account for testing.", signedInPassengerLoaded: "Signed in as {email}. Passenger profile loaded. Add a passenger payment method before requesting rides.", signedInRiderLoaded: "Signed in as {email}. Rider profile loaded.", signedInAs: "Signed in as {identity}.", freshSignInCodeRequired: "Request a fresh sign-in code for this phone number.", signInCodeRequired: "Use email and password to sign in. Phone OTP is only for first-time phone verification.", passwordSignInOnly: "Use email and password to sign in. Phone OTP is only for first-time phone verification.", signInEmailPasswordRequired: "Enter the email and password for this account. Phone-code sign-in is disabled in manual pilot mode.", signingIn: "Signing in...", signInCodeIncorrect: "Sign-in code is not correct.", localSignInAccountMissing: "No saved {type} account matches this phone number. Create and save the {type} account first, then sign in.", signedOut: "Signed out.", passengerPhoneBeforeSave: "Verify the passenger phone number before saving the account.", riderPhoneBeforeReview: "Verify the rider phone number before submitting for review.", passengerPaymentRequired: "Add a passenger payment method under Payment before publishing rides.", riderPaymentRequired: "Save a rider payment account before receiving requests.", riderDailyRegionsRequired: "Preferred destination regions are optional for riders.", riderLiveGpsRequired: "Share live rider GPS before receiving requests.", startingPassengerSupabase: "Starting passenger save in Supabase...", savingPassenger: "Saving passenger...", passengerCreated: "{name} passenger account created successfully. Add a passenger payment method next, then request rides.", passengerCreatedEmailPending: "{name} passenger account created successfully. Add a passenger payment method next; email/password sign-in may need Supabase email confirmation or setup.", passengerAccountFailed: "Passenger account was not created: {message}", passengerSyncing: "{name} passenger account created successfully. Add a passenger payment method next, then request rides.", startingRiderSupabase: "Starting rider save in Supabase...", savingRiderApplication: "Saving rider application...", submittingRiderApplication: "Submitting rider application for admin approval...", riderCreatedPending: "{name} account created. Rider application is pending admin approval. If approved, the 30-day free trial starts before weekly or monthly Waka Rider Access is required.", riderAccountFailed: "Rider account was not submitted: {message}", missingRiderDocuments: "Upload these required rider documents before admin review: {documents}.", passengerAccountRequired: "Create a passenger account before publishing a ride request.", passengerSignInRequired: "Passenger sign-in is required before publishing rides.", passengerPhoneRequired: "Passenger phone verification is required before publishing rides.", realisticFareRequired: "Enter a realistic fare offer.", fareBelowGuidance: "This fare is below the suggested {min}-{max} range. Riders may skip it or respond more slowly. Continue anyway?", fareOutsideGuidance: "This route estimate suggests {min}-{max}. Lower fares may take longer to match.", scheduledTimeRequired: "Choose a valid date and time for the scheduled ride.", scheduleThirtyMinutes: "Schedule the ride at least 30 minutes from now.", ridePublishedSupabase: "Ride request published to Supabase for eligible riders.", ridePublishedLocal: "Ride request published locally.", publishRideFailed: "Could not publish this ride request: {message}", subscriptionReferenceRequired: "Provider checkout is required for Waka Rider Access.", subscriptionAlreadyPending: "A provider subscription checkout is already in progress.", submittingPaymentSupabase: "Opening Waka Rider Access checkout...", savingPaymentReference: "Opening Waka Rider Access checkout...", paymentReferenceSubmitted: "Waka Rider Access checkout opened. Paid access starts after the free trial or current access period ends.", paymentReferenceFailed: "Could not open rider access checkout: {message}", selectRideRequestFirst: "Select a ride request first.", createRiderFirst: "Create a rider account first.", riderSignInRequired: "Rider sign-in is required before responding to rides.", riderApprovalRequired: "Admin approval is required before responding to rides.", riderAccessRequired: "Your free trial or paid rider access must be active before responding to rides.", selectNearbyRequest: "Select a nearby request that matches your approved rider account.", requestClosed: "This request is no longer open.", offerSendFailed: "Could not send this offer: {message}", passengerOwnRequestRequired: "Only the passenger who posted this request can choose a rider.", chooseRiderFailed: "Could not choose this rider: {message}", safetyReportUnavailable: "Reports are available after a passenger chooses a rider. Contact Waka opens after a rider is selected.", safetyReportNeedsDetail: "Add enough detail for Waka to understand the request.", safetyReportSignInRequired: "Sign in again before contacting Waka.", submittingSafetySupabase: "Sending message to Waka...", savingSafetyReport: "Saving message for Waka review...", safetyReportSubmitted: "Safety report submitted for admin review. Message sent to Waka.", safetyReportFailed: "Could not submit report: {message}", suspendRiderConfirm: "Suspend this rider? They will stop seeing and accepting ride requests immediately.", clearDemoConfirm: "Clear all locally stored demo data?", requestConfirmationFailed: "Could not request confirmation: {message}", confirmScheduledFailed: "Could not confirm this scheduled ride: {message}", reopenScheduledFailed: "Could not reopen this scheduled ride: {message}", stop: "Stop", androidInstallHelp: "On Android, open this site in Chrome, tap the menu, then choose Add to Home screen or Install app." }, fr: { admin: "Administrateur", pageTitle: "Waka - Courses negociees", installed: "Installee", chooseAccountType: "Choisissez le type de compte", continueAsPassenger: "Continuer comme passager", continueAsRider: "Continuer comme conducteur", signInOrCreate: "Connexion ou creation de compte", createAccount: "Creer un compte", paySubscription: "Ouvrir le paiement automatique", passengerPanelSubtitle: "Demandez une course et choisissez la meilleure offre", riderPanelSubtitle: "Postulez, abonnez-vous, puis negociez les courses", email: "E-mail", password: "Mot de passe", phoneNumber: "Numero de telephone", otpCode: "Code OTP", sendOtp: "Envoyer OTP", sendCode: "Envoyer le code", verify: "Verifier", signOut: "Se deconnecter", fullName: "Nom complet", profilePicture: "Photo de profil", phoneVerificationCode: "Code de verification telephone", nationalIdNumber: "Numero d'identification nationale", identityReference: "Reference d'identite", driverLicenseNumber: "Numero de permis de conduire", dateOfBirth: "Date de naissance", country: "Pays", city: "Ville", passengerSignInHelp: "Connectez-vous avec l'e-mail et le mot de passe avant de demander une course.", riderSignInHelp: "Connectez-vous avec l'e-mail et le mot de passe avant de repondre aux courses.", passengerWorkspace: "Espace passager", riderWorkspace: "Espace conducteur", passengerSignedIn: "Passager connecte", riderSignedIn: "Conducteur connecte", readyToRequestRides: "Pret a demander des courses.", applicationStatusWillAppear: "Le statut de la demande apparaitra ici.", noPassengerSaved: "Aucun passager enregistre.", noRiderApplication: "Aucune demande conducteur enregistree.", pickupArea: "Zone de prise en charge", pickupDescription: "Description du lieu", destination: "Lieu de destination", rideTiming: "Moment de la course", asSoonAsPossible: "Des que possible", scheduleAhead: "Planifier", scheduledDateTime: "Date et heure prevues", vehicle: "Vehicule", vehicleType: "Designation du vehicule", bike: "Voiture", car: "Voiture", bikeOrCar: "Voiture", fareOffer: "Prix propose", paymentPreference: "Mode de paiement", cashInHand: "Especes", mtnMoney: "Argent mobile MTN", orangeMoney: "Argent Orange", agreeWithRider: "Accord avec le conducteur avant la course", optional: "Optionnel", record: "Enregistrer", clear: "Effacer", riderAccess: "Acces conducteur", applicationStatus: "Statut de la demande", riderPlatformStatus: "Le statut de votre espace conducteur apparaitra ici.", operatingArea: "Zone d'activite", credentialNumber: "Numero de permis de conduire", vehicleMake: "Marque du vehicule", vehicleModel: "Modele du vehicule", bodyType: "Type de carrosserie", vehicleDesignation: "Designation du vehicule", yearOfManufacture: "Annee de fabrication", vehicleColor: "Couleur", vehicleVin: "VIN du vehicule", vehicleRegistration: "Numero de plaque", driverLicenseDocument: "Document du permis de conduire", vehicleRegistrationDocument: "Document d'immatriculation", nationalIdDocument: "Document d'assurance", subscriptionIntro: "Les conducteurs approuves recoivent 30 jours gratuits avant l'abonnement mensuel.", paymentProvider: "Assureur", paymentPhone: "Telephone de paiement", transactionReference: "Numero de police d'assurance", subscriptionPaymentHelp: "L'admin verifie les paiements avant de prolonger l'acces.", yourFare: "Votre prix", messageBeforeSelection: "Message avant selection", openRequests: "Demandes ouvertes", passengers: "Passagers", riders: "Conducteurs", pendingRiders: "Conducteurs en attente", subscribed: "Abonnes", loadDemoMarket: "Charger le marche demo", clearDemoData: "Effacer les donnees demo", selectOrPublish: "Selectionnez ou publiez une demande", refreshMarket: "Actualiser le marche", all: "Tous", rideRequests: "Demandes de course", riderOffers: "Offres conducteurs", accountDetail: "Detail du compte", postSelectionChat: "Chat apres selection", locked: "Verrouille", send: "Envoyer", chooseRider: "Choisir conducteur", openFullReview: "Ouvrir la revue complete", approve: "Approuver", decline: "Refuser", passengerNamePlaceholder: "Nom du passager", passwordPlaceholder: "Mot de passe", createPasswordPlaceholder: "Creer un mot de passe", codePlaceholder: "Code a 6 chiffres", nationalIdPlaceholder: "Numero d'identification nationale", driverLicensePlaceholder: "Numero de permis de conduire", pickupDescriptionPlaceholder: "Repere, couleur du batiment, marche, carrefour, boutique", destinationPlaceholder: "Zone, repere ou adresse de destination", riderNamePlaceholder: "Nom du conducteur", credentialPlaceholder: "CNI, permis ou numero d'autorisation", registrationPlaceholder: "Plaque ou numero d'immatriculation", transactionReferencePlaceholder: "Numero de police", counterFarePlaceholder: "Entrez une contre-offre plus elevee", counterNotePlaceholder: "Facultatif: courte note vehicule ou tarif pour le passager", supabasePasswordPlaceholder: "Mot de passe Supabase", chatPlaceholder: "Le chat s'ouvre apres le choix du conducteur", safetyReportDetailsPlaceholder: "Decrivez le souci pour examen admin", offlineReady: "Pret hors ligne", onlineDemo: "Demo en ligne", localMode: "Mode local", supabaseReady: "Supabase pret", supabaseConfigNeeded: "Configuration Supabase requise", supabaseConnecting: "Connexion Supabase", supabaseSdkUnavailable: "SDK Supabase indisponible", manualPhoneVerified: "Mode pilote manuel: telephone marque verifie. Configurez le SMS OTP avant le lancement public.", smsVerificationRelaxedForTesting: "Mode test: verification SMS ignoree. La creation par email/mot de passe peut continuer; activez le vrai SMS OTP avant le lancement public.", validPhoneRequired: "Entrez un numero de telephone valide avant de demander un code.", validDateOfBirthRequired: "Entrez une date de naissance valide au format AAAA-MM-JJ. Vous pouvez saisir seulement les chiffres et Waka ajoutera les tirets.", checkingPassengerAccount: "Verification des details du compte passager...", checkingRiderApplication: "Verification des details de la demande conducteur...", accountMissingFields: "Completez ces champs avant d'enregistrer: {fields}.", phoneOtpCooldown: "Veuillez attendre {seconds}s avant de demander un autre code telephone.", phoneOtpRateLimited: "Trop de demandes de code telephone. Attendez avant de demander un autre code et verifiez les limites Auth Supabase si cela continue.", sendingVerificationCode: "Envoi du code de verification...", verificationCodeSent: "Code de verification envoye a {phone}.", demoCode: "Code demo: {code} pour {phone}", freshVerificationCodeRequired: "Demandez un nouveau code pour ce numero.", verifyingPhoneNumber: "Verification du telephone...", phoneNumberVerified: "Numero de telephone verifie. Cela verifie seulement le telephone; appuyez sur Enregistrer ou Envoyer pour terminer la creation du compte Waka.", verificationCodeIncorrect: "Le code de verification est incorrect.", phoneOtpManualSignIn: "La connexion OTP telephone est desactivee en mode pilote manuel. Utilisez l'e-mail et le mot de passe.", sendingSignInCode: "Envoi du code de connexion...", signInCodeSent: "Code de connexion envoye a {phone}.", demoSignInCode: "Code de connexion demo: {code} pour {phone}", signingInPassword: "Connexion avec e-mail et mot de passe...", loadingWakaProfile: "Chargement du profil Waka...", supabaseProfileMissing: "Connexion reussie. Ce login a encore besoin d'un profil Waka; completez et enregistrez ce formulaire pour lier le compte.", wrongProfileRole: "Ce compte est enregistre comme {role}, pas {type}.", wrongProfileRoleStrict: "Ce login est enregistre comme {role}, pas {type}. Dans le projet Auth unique de ce deploiement WakaGood, le meme e-mail ne peut pas ouvrir les deux roles en securite. Utilisez maintenant un e-mail/telephone {type} separe, ou terminez la separation Auth passager/conducteur avant de reutiliser cet e-mail pour les deux roles.", adminPublicPortalBlocked: "Les comptes admin ne peuvent pas se connecter aux portails passager ou conducteur. Utilisez l'espace Admin, ou creez un compte passager/conducteur separe pour les tests.", signedInPassengerLoaded: "Connecte comme {email}. Profil passager charge. Ajoutez un compte de paiement avant de demander une course.", signedInRiderLoaded: "Connecte comme {email}. Profil conducteur charge.", signedInAs: "Connecte comme {identity}.", freshSignInCodeRequired: "Demandez un nouveau code de connexion pour ce numero.", signInCodeRequired: "Utilisez l'e-mail et le mot de passe pour vous connecter. L'OTP telephone sert seulement a verifier le telephone la premiere fois.", passwordSignInOnly: "Utilisez l'e-mail et le mot de passe pour vous connecter. L'OTP telephone sert seulement a verifier le telephone la premiere fois.", signInEmailPasswordRequired: "Entrez l'e-mail et le mot de passe de ce compte. La connexion par code telephone est desactivee en mode pilote manuel.", signingIn: "Connexion...", signInCodeIncorrect: "Le code de connexion est incorrect.", localSignInAccountMissing: "Aucun compte {type} enregistre ne correspond a ce telephone. Creez et enregistrez le compte {type}, puis connectez-vous.", signedOut: "Deconnecte.", passengerPhoneBeforeSave: "Verifiez le telephone du passager avant d'enregistrer le compte.", riderPhoneBeforeReview: "Verifiez le telephone du conducteur avant d'envoyer la demande.", passengerPaymentRequired: "Ajoutez une methode de paiement passager sous Paiement avant de publier des courses.", riderPaymentRequired: "Enregistrez un compte de paiement conducteur avant de recevoir des demandes.", riderDailyRegionsRequired: "Les regions de destination preferees sont facultatives pour les conducteurs.", riderLiveGpsRequired: "Partagez le GPS conducteur en direct avant de recevoir des demandes.", startingPassengerSupabase: "Enregistrement passager dans Supabase...", savingPassenger: "Enregistrement du passager...", passengerCreated: "Compte passager {name} cree avec succes. Ajoutez ensuite un compte de paiement avant de demander une course.", passengerCreatedEmailPending: "Compte passager {name} cree avec succes. Ajoutez ensuite un compte de paiement; la connexion e-mail/mot de passe peut necessiter une confirmation ou configuration Supabase.", passengerAccountFailed: "Le compte passager n'a pas ete cree: {message}", passengerSyncing: "Compte passager {name} cree avec succes. Ajoutez ensuite un compte de paiement avant de demander une course.", startingRiderSupabase: "Enregistrement conducteur dans Supabase...", savingRiderApplication: "Enregistrement de la demande conducteur...", submittingRiderApplication: "Envoi de la demande conducteur pour validation admin...", riderCreatedPending: "Compte {name} cree. La demande conducteur attend la validation admin. Si elle est approuvee, choisissez l'acces Waka hebdomadaire ou mensuel avant de recevoir des demandes.", riderAccountFailed: "Le compte conducteur n'a pas ete soumis: {message}", missingRiderDocuments: "Ajoutez ces documents conducteur requis avant la validation admin: {documents}.", passengerAccountRequired: "Creez un compte passager avant de publier une demande de course.", passengerSignInRequired: "La connexion passager est requise avant de publier des courses.", passengerPhoneRequired: "La verification du telephone passager est requise avant de publier des courses.", realisticFareRequired: "Entrez un prix propose realiste.", fareBelowGuidance: "Ce prix est inferieur a la fourchette suggeree {min}-{max}. Les conducteurs peuvent l'ignorer ou repondre plus lentement. Continuer quand meme?", fareOutsideGuidance: "Cette estimation d'itineraire suggere {min}-{max}. Les prix plus bas peuvent prendre plus de temps a trouver un conducteur.", scheduledTimeRequired: "Choisissez une date et une heure valides pour la course planifiee.", scheduleThirtyMinutes: "Planifiez la course au moins 30 minutes a l'avance.", ridePublishedSupabase: "Demande de course publiee dans Supabase pour les conducteurs eligibles.", ridePublishedLocal: "Demande de course publiee localement.", publishRideFailed: "Impossible de publier cette demande: {message}", selectRideRequestFirst: "Selectionnez d'abord une demande de course.", createRiderFirst: "Creez d'abord un compte conducteur.", riderSignInRequired: "Connexion conducteur requise avant de repondre aux courses.", riderApprovalRequired: "Validation admin requise avant de repondre aux courses.", riderAccessRequired: "Votre essai ou abonnement doit etre actif avant de repondre aux courses.", selectNearbyRequest: "Selectionnez une demande proche qui correspond a votre compte conducteur approuve.", requestClosed: "Cette demande n'est plus ouverte.", offerSendFailed: "Impossible d'envoyer cette offre: {message}", passengerOwnRequestRequired: "Seul le passager qui a publie cette demande peut choisir un conducteur.", chooseRiderFailed: "Impossible de choisir ce conducteur: {message}", suspendRiderConfirm: "Suspendre ce conducteur? Il ne verra plus et n'acceptera plus les demandes immediatement.", clearDemoConfirm: "Effacer toutes les donnees demo stockees localement?", requestConfirmationFailed: "Impossible de demander la confirmation: {message}", confirmScheduledFailed: "Impossible de confirmer cette course planifiee: {message}", reopenScheduledFailed: "Impossible de rouvrir cette course planifiee: {message}", stop: "Arreter", subscriptionReferenceRequired: "Le paiement fournisseur est requis pour Waka Rider Access.", subscriptionAlreadyPending: "Une session de paiement automatique est deja en cours.", submittingPaymentSupabase: "Ouverture du paiement Waka Rider Access...", savingPaymentReference: "Ouverture du paiement automatique...", paymentReferenceSubmitted: "Paiement Waka Rider Access ouvert. L'acces se prolonge apres confirmation.", paymentReferenceFailed: "Impossible d'ouvrir le paiement: {message}", safetyReportUnavailable: "Les signalements sont disponibles apres le choix d'un conducteur.", safetyReportNeedsDetail: "Ajoutez assez de details pour que l'admin comprenne le souci.", safetyReportSignInRequired: "Reconnectez-vous avant d'envoyer un signalement.", submittingSafetySupabase: "Envoi du signalement a Supabase...", savingSafetyReport: "Enregistrement du signalement pour examen admin...", safetyReportSubmitted: "Signalement envoye pour examen admin.", safetyReportFailed: "Impossible d'envoyer le signalement: {message}", androidInstallHelp: "Sur Android, ouvrez ce site dans Chrome, touchez le menu, puis choisissez Ajouter a l'ecran d'accueil ou Installer l'application." }, ar: { tagline: "رحلات دراجة وسيارة قابلة للتفاوض", passenger: "راكب", rider: "سائق", admin: "مشرف", language: "اللغة", installApp: "تثبيت التطبيق", createPassenger: "إنشاء حساب راكب", savePassenger: "حفظ الراكب", postRide: "طلب رحلة", publishRequest: "نشر الطلب", riderApplication: "طلب السائق", submitReview: "إرسال للمراجعة", subscription: "اشتراك", paySubscription: "Open rider access checkout", respondRequest: "الرد على الطلب المحدد", sendOffer: "إرسال قبول أو عرض مقابل", passengerSignIn: "تسجيل دخول الراكب", riderSignIn: "تسجيل دخول السائق", signIn: "تسجيل الدخول", pageTitle: "رحلات Waka التفاوضية", installed: "مثبت", passengerPanelSubtitle: "اطلب رحلة واختر أفضل عرض", riderPanelSubtitle: "قدّم طلبك واشترك ثم تفاوض على الرحلات", email: "البريد الإلكتروني", password: "كلمة المرور", phoneNumber: "رقم الهاتف", otpCode: "رمز التحقق", sendOtp: "إرسال الرمز", sendCode: "إرسال الرمز", verify: "تحقق", signOut: "تسجيل الخروج", fullName: "الاسم الكامل", profilePicture: "صورة الملف الشخصي", phoneVerificationCode: "رمز تحقق الهاتف", nationalIdNumber: "رقم الهوية الوطنية", dateOfBirth: "تاريخ الميلاد", country: "الدولة", city: "المدينة", passengerSignInHelp: "سجل الدخول قبل طلب الرحلات.", riderSignInHelp: "سجل الدخول قبل الرد على الرحلات.", passengerWorkspace: "مساحة الراكب", riderWorkspace: "مساحة السائق", passengerSignedIn: "تم تسجيل دخول الراكب", riderSignedIn: "تم تسجيل دخول السائق", readyToRequestRides: "جاهز لطلب الرحلات.", applicationStatusWillAppear: "ستظهر حالة الطلب هنا.", noPassengerSaved: "لم يتم حفظ أي راكب بعد.", noRiderApplication: "لم يتم حفظ أي طلب سائق بعد.", pickupArea: "منطقة الالتقاء", pickupDescription: "وصف مكان الالتقاء", destination: "الوجهة", rideTiming: "وقت الرحلة", asSoonAsPossible: "في أقرب وقت ممكن", scheduleAhead: "الحجز مسبقاً", scheduledDateTime: "تاريخ ووقت الرحلة المجدولة", vehicle: "المركبة", vehicleType: "تصنيف المركبة", bike: "سيارة", car: "سيارة", bikeOrCar: "سيارة", fareOffer: "عرض الأجرة", paymentPreference: "طريقة الدفع المفضلة", cashInHand: "نقداً", mtnMoney: "MTN Mobile Money", orangeMoney: "Orange Money", agreeWithRider: "اتفق مع السائق قبل الرحلة", optional: "اختياري", record: "تسجيل", clear: "مسح", riderAccess: "وصول السائق", applicationStatus: "حالة الطلب", riderPlatformStatus: "ستظهر حالة منصة السائق هنا.", operatingArea: "منطقة العمل", credentialNumber: "رقم رخصة القيادة", vehicleRegistration: "رقم اللوحة", driverLicenseDocument: "وثيقة رخصة القيادة", vehicleRegistrationDocument: "وثيقة التسجيل", nationalIdDocument: "وثيقة التأمين", subscriptionIntro: "يحصل السائقون المعتمدون على 30 يوماً مجاناً قبل فرض رسوم شهرية للمنصة.", paymentProvider: "مزود التأمين", paymentPhone: "هاتف الدفع", transactionReference: "رقم وثيقة التأمين", subscriptionPaymentHelp: "يتحقق المشرف من مدفوعات اشتراك السائق قبل تمديد الوصول.", yourFare: "أجرتك", messageBeforeSelection: "ملاحظة للراكب قبل الاختيار", openRequests: "الطلبات المفتوحة", passengers: "الركاب", riders: "السائقون", pendingRiders: "سائقون بانتظار الاعتماد", subscribed: "مشتركون", loadDemoMarket: "تحميل سوق تجريبي", clearDemoData: "مسح بيانات التجربة المحلية", selectOrPublish: "اختر أو انشر طلباً", refreshMarket: "تحديث السوق", all: "الكل", rideRequests: "طلبات الرحلات", riderOffers: "عروض السائقين", accountDetail: "تفاصيل الحساب", postSelectionChat: "الدردشة بعد الاختيار", locked: "مقفل", send: "إرسال", chooseRider: "اختيار سائق", openFullReview: "فتح المراجعة الكاملة", approve: "اعتماد", decline: "رفض" }, pcm: { passengerPanelSubtitle: "Ask for ride and choose di best offer", paySubscription: "Open rider access checkout", riderPanelSubtitle: "Apply, pay subscription, then talk price", phoneNumber: "Phone number", sendCode: "Send code", verify: "Verify", signOut: "Sign out", fullName: "Full name", profilePicture: "Profile picture", nationalIdNumber: "National ID number", dateOfBirth: "Date of birth", passengerSignInHelp: "Sign in before you request ride.", riderSignInHelp: "Sign in before you answer ride.", pickupArea: "Place for pickup", pickupDescription: "Describe where you dey", rideTiming: "Ride time", asSoonAsPossible: "Now now", scheduleAhead: "Book ahead", fareOffer: "Money you offer", paymentPreference: "How you go pay", cashInHand: "Cash for hand", agreeWithRider: "Agree with rider before ride", optional: "If you want", vehicleType: "Vehicle designation", operatingArea: "Area wey you dey work", subscriptionIntro: "Approved riders get a 30-day free trial before weekly or monthly Waka Rider Access is required.", paymentProvider: "Insurance provider", paymentPhone: "Payment phone", transactionReference: "Insurance policy number", refreshMarket: "Refresh market", rideRequests: "Ride requests", riderOffers: "Rider offers", accountDetail: "Account details", send: "Send" }, sw: { passengerPanelSubtitle: "Omba safari na chagua ofa bora", paySubscription: "Fungua malipo ya usajili kiotomatiki", riderPanelSubtitle: "Tuma ombi, lipa usajili, kisha jadili safari", email: "Barua pepe", password: "Nenosiri", phoneNumber: "Namba ya simu", sendCode: "Tuma msimbo", verify: "Thibitisha", signOut: "Toka", fullName: "Jina kamili", profilePicture: "Picha ya wasifu", nationalIdNumber: "Namba ya kitambulisho", dateOfBirth: "Tarehe ya kuzaliwa", country: "Nchi", city: "Mji", pickupArea: "Eneo la kuchukuliwa", pickupDescription: "Maelezo ya mahali", destination: "Unakoenda", rideTiming: "Muda wa safari", asSoonAsPossible: "Haraka iwezekanavyo", scheduleAhead: "Panga baadaye", vehicle: "Chombo", bike: "Gari", car: "Gari", fareOffer: "Nauli unayotoa", paymentPreference: "Njia ya malipo", cashInHand: "Pesa taslimu", optional: "Si lazima", record: "Rekodi", clear: "Futa", operatingArea: "Eneo la kazi", paymentProvider: "Kampuni ya bima", paymentPhone: "Simu ya malipo", transactionReference: "Namba ya bima", passengers: "Abiria", riders: "Madereva", refreshMarket: "Sasisha soko", rideRequests: "Maombi ya safari", riderOffers: "Ofa za madereva", accountDetail: "Maelezo ya akaunti", send: "Tuma" }, pt: { passengerPanelSubtitle: "Pedir viagem e escolher a melhor oferta", paySubscription: "Abrir pagamento automatico da subscricao", riderPanelSubtitle: "Candidatar, subscrever e negociar viagens", email: "Email", password: "Palavra-passe", phoneNumber: "Numero de telefone", sendCode: "Enviar codigo", verify: "Verificar", signOut: "Sair", fullName: "Nome completo", profilePicture: "Foto de perfil", nationalIdNumber: "Numero de identificacao nacional", dateOfBirth: "Data de nascimento", country: "Pais", city: "Cidade", pickupArea: "Zona de recolha", pickupDescription: "Descricao do local", destination: "Destino", rideTiming: "Hora da viagem", asSoonAsPossible: "O mais cedo possivel", scheduleAhead: "Agendar", vehicle: "Veiculo", bike: "Carro", car: "Carro", fareOffer: "Oferta de tarifa", paymentPreference: "Preferencia de pagamento", cashInHand: "Dinheiro em mao", optional: "Opcional", record: "Gravar", clear: "Limpar", operatingArea: "Area de operacao", subscriptionIntro: "Motoristas aprovados recebem 30 dias gratis antes da taxa mensal.", paymentProvider: "Seguradora", paymentPhone: "Telefone de pagamento", transactionReference: "Numero da apolice", passengers: "Passageiros", riders: "Motoristas", refreshMarket: "Atualizar mercado", rideRequests: "Pedidos de viagem", riderOffers: "Ofertas de motoristas", accountDetail: "Detalhe da conta", send: "Enviar" }, es: { passengerPanelSubtitle: "Solicita un viaje y elige la mejor oferta", riderPanelSubtitle: "Aplica, suscribete y negocia viajes", createAccount: "Crear cuenta", email: "Correo", password: "Contrasena", phoneNumber: "Numero de telefono", sendCode: "Enviar codigo", verify: "Verificar", signOut: "Salir", fullName: "Nombre completo", profilePicture: "Foto de perfil", nationalIdNumber: "Referencia de identidad", identityReference: "Referencia de identidad", driverLicenseNumber: "Numero de licencia", dateOfBirth: "Fecha de nacimiento", country: "Pais", city: "Ciudad", pickupArea: "Zona de recogida", pickupDescription: "Descripcion de recogida", destination: "Destino", rideTiming: "Horario del viaje", asSoonAsPossible: "Lo antes posible", scheduleAhead: "Programar", vehicle: "Vehiculo", car: "Auto", fareOffer: "Oferta de tarifa", paymentPreference: "Preferencia de pago", operatingArea: "Zona de operacion", paymentProvider: "Aseguradora", passengers: "Pasajeros", riders: "Conductores", refreshMarket: "Actualizar mercado", rideRequests: "Solicitudes de viaje", riderOffers: "Ofertas de conductores", accountDetail: "Detalle de cuenta", send: "Enviar" } }; translations.en = { ...translations.en, ...translationAdditions.en }; Object.entries(translationAdditions).forEach(([language, entries]) => { if (language !== "en") translations[language] = { ...translations.en, ...(translations[language] ?? {}), ...entries }; }); const textTranslationKeys = { "Passenger": "passenger", "Rider": "rider", "Admin": "admin", "Request a ride and choose the best offer": "passengerPanelSubtitle", "Apply, subscribe, then negotiate rides": "riderPanelSubtitle", "Email": "email", "Password": "password", "Phone number": "phoneNumber", "OTP code": "otpCode", "Send OTP": "sendOtp", "Send code": "sendCode", "Verify": "verify", "Sign in": "signIn", "Sign out": "signOut", "Full name": "fullName", "Profile picture": "profilePicture", "Phone verification code": "phoneVerificationCode", "National ID number": "identityReference", "Identity reference": "identityReference", "Identity document number": "identityReference", "Driver's license number": "driverLicenseNumber", "Date of birth": "dateOfBirth", "Country": "country", "City": "city", "Use email and password to sign in before requesting rides.": "passengerSignInHelp", "Use email and password to sign in before responding to rides.": "riderSignInHelp", "Passenger workspace": "passengerWorkspace", "Rider workspace": "riderWorkspace", "Passenger signed in": "passengerSignedIn", "Rider signed in": "riderSignedIn", "Ready to request rides.": "readyToRequestRides", "Application status will appear here.": "applicationStatusWillAppear", "No passenger saved yet.": "noPassengerSaved", "No rider application saved yet.": "noRiderApplication", "Pickup area": "pickupArea", "Pickup description": "pickupDescription", "Destination": "destination", "Ride timing": "rideTiming", "As soon as possible": "asSoonAsPossible", "Schedule ahead": "scheduleAhead", "Scheduled date and time": "scheduledDateTime", "Vehicle": "vehicle", "Vehicle type": "vehicleType", "Vehicle class": "vehicleType", "Vehicle designation": "vehicleType", "Car": "car", "Car": "car", "Car only": "bikeOrCar", "Fare offer": "fareOffer", "Fare offer (USD)": "fareOffer", "Payment preference": "paymentPreference", "Cash in hand": "cashInHand", "MTN Mobile Money": "mtnMoney", "Orange Money": "orangeMoney", "Agree with rider before ride": "agreeWithRider", "Optional": "optional", "Record": "record", "Clear": "clear", "Rider access": "riderAccess", "Application status": "applicationStatus", "Your rider platform status will appear here.": "riderPlatformStatus", "Operating area": "operatingArea", "License or professional credential number": "credentialNumber", "Vehicle make": "vehicleMake", "Vehicle model": "vehicleModel", "Body type": "bodyType", "Year of manufacture": "yearOfManufacture", "Vehicle VIN": "vehicleVin", "Vehicle registration": "vehicleRegistration", "Plate number": "vehicleRegistration", "Driver's license document": "driverLicenseDocument", "License document upload": "driverLicenseDocument", "Vehicle registration document": "vehicleRegistrationDocument", "Registration document": "vehicleRegistrationDocument", "Insurance document": "nationalIdDocument", "Car make": "vehicleMake", "Car type/model": "vehicleModel", "Year": "yearOfManufacture", "Color": "vehicleColor", "Insurance provider": "paymentProvider", "Insurance policy number": "transactionReference", "Approved riders get a 30-day free trial before weekly or monthly Waka Rider Access is required.": "subscriptionIntro", "Payment phone": "paymentPhone", "Waka Rider Access is provider-hosted. Manual upfront payments and automatic renewal are available after the free trial.": "subscriptionPaymentHelp", "Your fare": "yourFare", "Note to passenger before selection": "messageBeforeSelection", "Open requests": "openRequests", "Passengers": "passengers", "Riders": "riders", "Pending riders": "pendingRiders", "Subscribed": "subscribed", "Load demo market": "loadDemoMarket", "Clear local demo data": "clearDemoData", "Select or publish a request": "selectOrPublish", "Refresh market": "refreshMarket", "All": "all", "Ride requests": "rideRequests", "Rider offers": "riderOffers", "Account detail": "accountDetail", "Post-selection chat": "postSelectionChat", "Locked": "locked", "Send": "send", "Choose rider": "chooseRider", "Open full review": "openFullReview", "Approve": "approve", "Decline": "decline" }; const placeholderTranslationKeys = { "Password": "passwordPlaceholder", "Create a password": "createPasswordPlaceholder", "6-digit code": "codePlaceholder", "Passenger name": "passengerNamePlaceholder", "National identification number": "nationalIdPlaceholder", "Driver license, state ID, or passport reference": "nationalIdPlaceholder", "Driver license, state ID, or passport": "nationalIdPlaceholder", "Driver's license number": "driverLicensePlaceholder", "Landmark, building color, market, junction, shop name": "pickupDescriptionPlaceholder", "Destination area, landmark, or address": "destinationPlaceholder", "Rider or driver name": "riderNamePlaceholder", "National ID, license, or permit number": "credentialPlaceholder", "17-character VIN": "credentialPlaceholder", "Vehicle color": "vehicle", "Plate or registration number": "registrationPlaceholder", "Plate number": "registrationPlaceholder", "Insurance company": "paymentProvider", "Policy number": "transactionReferencePlaceholder", "Enter a different counter-offer fare": "counterFarePlaceholder", "Enter a higher counter-offer fare": "counterFarePlaceholder", "Optional: brief vehicle or fare note for the passenger": "counterNotePlaceholder", "Supabase password": "supabasePasswordPlaceholder", "Chat opens only after passenger chooses a rider": "chatPlaceholder", "Describe the concern for admin review": "safetyReportDetailsPlaceholder" }; const translatedStaticTextNodes = []; const translatedStaticTextNodeSet = new WeakSet(); const productionTranslationTargetPercent = 100; const productionLaunchLanguages = ["en", "fr", "de"]; const translationSameAsEnglishKeys = new Set([ "admin", "mtnMoney", "orangeMoney" ]); const languageLabels = { en: "English", fr: "French", de: "German", pcm: "Pidgin", ar: "Arabic", sw: "Swahili", pt: "Portuguese", es: "Spanish" }; function translatedValue(key) { const dictionary = translations[state.language] ?? translations.en; return dictionary[key] ?? translations.en[key] ?? ""; } function translatedMessage(key, values = {}) { return translatedValue(key).replace(/\{([a-zA-Z0-9_]+)\}/g, (match, valueKey) => ( values[valueKey] ?? match )); } function translationCoverageFor(language) { const english = translations.en ?? {}; const dictionary = translations[language] ?? {}; const keys = Object.keys(english); const fallbackKeys = language === "en" ? [] : keys.filter((key) => ( !dictionary[key] || (dictionary[key] === english[key] && !translationSameAsEnglishKeys.has(key)) )); const reviewed = keys.length - fallbackKeys.length; return { language, label: languageLabels[language] ?? language.toUpperCase(), reviewed, fallback: fallbackKeys.length, total: keys.length, percent: keys.length ? Math.round((reviewed / keys.length) * 100) : 100, sampleFallbacks: fallbackKeys.slice(0, 4), fallbackKeys }; } function translationCoverageReport() { return Object.keys(translations).map(translationCoverageFor); } function translationCoverageGrid(report) { return `
${report.map((item) => { const ready = item.percent >= productionTranslationTargetPercent; return ` ${escapeHtml(item.label)} ${item.percent}% reviewed - ${item.fallback} fallback key${item.fallback === 1 ? "" : "s"} `; }).join("")}
`; } function setTranslatedStatus(node, key, values = {}) { if (!node) return; node.dataset.i18nDynamic = key; node.dataset.i18nValues = JSON.stringify(values); const text = translatedMessage(key, values); node.dataset.i18nRenderedText = text; node.textContent = text; } const wakaGoodDialogBrand = "WakaGood"; function ensureWakaGoodDialogStyles() { if (document.getElementById("wakaGoodDialogStyles")) return; const style = document.createElement("style"); style.id = "wakaGoodDialogStyles"; style.textContent = ` .wakagood-dialog-backdrop { position: fixed; inset: 0; z-index: 10000; display: grid; place-items: center; padding: 18px; background: rgba(23, 32, 29, 0.42); } .wakagood-dialog { width: min(420px, 100%); border: 1px solid var(--line, #d7e0dc); border-radius: 8px; box-shadow: var(--shadow, 0 18px 42px rgba(23, 32, 29, 0.12)); background: var(--surface, #fff); color: var(--ink, #17201d); overflow: hidden; } .wakagood-dialog-header { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid var(--line, #d7e0dc); font-weight: 800; } .wakagood-dialog-mark { width: 34px; height: 34px; display: grid; place-items: center; border-radius: 8px; background: var(--green, #0f766e); color: #fff; font-weight: 900; } .wakagood-dialog-body { padding: 16px; white-space: pre-line; line-height: 1.45; color: var(--ink, #17201d); } .wakagood-dialog-input { width: calc(100% - 32px); margin: 0 16px 16px; min-height: 48px; border: 1px solid var(--line, #d7e0dc); border-radius: 8px; padding: 11px 12px; background: #fff; color: var(--ink, #17201d); } .wakagood-dialog-actions { display: flex; justify-content: flex-end; gap: 10px; padding: 0 16px 16px; } .wakagood-dialog-actions button { min-height: 42px; border-radius: 8px; padding: 9px 14px; font-weight: 800; } .wakagood-dialog-cancel { border: 1px solid var(--line, #d7e0dc); background: var(--wash, #f3f7f4); color: var(--green-dark, #0b4f4a); } .wakagood-dialog-confirm { border: 1px solid var(--green, #0f766e); background: var(--green, #0f766e); color: #fff; } `; document.head.append(style); } function showWakaGoodDialog({ message, mode = "alert", defaultValue = "" } = {}) { if (typeof document === "undefined" || !document.body) { if (mode === "confirm") return Promise.resolve(confirm(String(message ?? ""))); if (mode === "prompt") return Promise.resolve(prompt(String(message ?? ""), defaultValue)); alert(String(message ?? "")); return Promise.resolve(true); } ensureWakaGoodDialogStyles(); return new Promise((resolve) => { const backdrop = document.createElement("div"); backdrop.className = "wakagood-dialog-backdrop"; backdrop.setAttribute("role", "presentation"); const dialog = document.createElement("div"); dialog.className = "wakagood-dialog"; dialog.setAttribute("role", "dialog"); dialog.setAttribute("aria-modal", "true"); dialog.setAttribute("aria-labelledby", "wakaGoodDialogTitle"); dialog.setAttribute("aria-describedby", "wakaGoodDialogMessage"); const header = document.createElement("div"); header.className = "wakagood-dialog-header"; const mark = document.createElement("span"); mark.className = "wakagood-dialog-mark"; mark.textContent = "W"; const title = document.createElement("strong"); title.id = "wakaGoodDialogTitle"; title.textContent = wakaGoodDialogBrand; header.append(mark, title); const body = document.createElement("div"); body.className = "wakagood-dialog-body"; body.id = "wakaGoodDialogMessage"; body.textContent = String(message ?? ""); const input = document.createElement("input"); input.className = "wakagood-dialog-input"; input.value = String(defaultValue ?? ""); input.hidden = mode !== "prompt"; input.setAttribute("aria-label", String(message ?? "WakaGood input")); const actions = document.createElement("div"); actions.className = "wakagood-dialog-actions"; const cancel = document.createElement("button"); cancel.type = "button"; cancel.className = "wakagood-dialog-cancel"; cancel.textContent = "Cancel"; cancel.hidden = mode === "alert"; const ok = document.createElement("button"); ok.type = "button"; ok.className = "wakagood-dialog-confirm"; ok.textContent = mode === "alert" ? "OK" : "Continue"; actions.append(cancel, ok); dialog.append(header, body); if (mode === "prompt") dialog.append(input); dialog.append(actions); backdrop.append(dialog); function finish(value) { document.removeEventListener("keydown", onKeyDown); backdrop.remove(); resolve(value); } function onKeyDown(event) { if (event.key === "Escape") { event.preventDefault(); finish(mode === "alert" ? true : null); } if (event.key === "Enter" && mode !== "alert") { event.preventDefault(); finish(mode === "prompt" ? input.value : true); } } cancel.addEventListener("click", () => finish(null)); ok.addEventListener("click", () => finish(mode === "prompt" ? input.value : true)); document.addEventListener("keydown", onKeyDown); document.body.append(backdrop); window.setTimeout(() => (mode === "prompt" ? input : ok).focus(), 0); }); } function showWakaGoodAlert(message) { return showWakaGoodDialog({ message, mode: "alert" }); } function showWakaGoodConfirm(message) { return showWakaGoodDialog({ message, mode: "confirm" }); } function showWakaGoodPrompt(message, defaultValue = "") { return showWakaGoodDialog({ message, mode: "prompt", defaultValue }); } function translatedAlert(key, values = {}) { void showWakaGoodAlert(translatedMessage(key, values)); } function translatedConfirm(key, values = {}) { return confirm(translatedMessage(key, values)); } function registerStaticTextTranslations() { const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, { acceptNode(node) { const text = node.nodeValue.trim(); if (!text || !textTranslationKeys[text] || translatedStaticTextNodeSet.has(node)) return NodeFilter.FILTER_REJECT; if (node.parentElement?.closest("script, style, [data-i18n]")) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } }); let node = walker.nextNode(); while (node) { translatedStaticTextNodeSet.add(node); translatedStaticTextNodes.push({ node, key: textTranslationKeys[node.nodeValue.trim()] }); node = walker.nextNode(); } } function setTranslatedTextNode(node, value) { const leading = node.nodeValue.match(/^\s*/)?.[0] ?? ""; const trailing = node.nodeValue.match(/\s*$/)?.[0] ?? ""; node.nodeValue = `${leading}${value}${trailing}`; } function applyLanguage() { registerStaticTextTranslations(); document.documentElement.lang = state.language; document.documentElement.dir = state.language === "ar" ? "rtl" : "ltr"; document.title = translatedValue("pageTitle"); document.querySelectorAll("[data-i18n]").forEach((node) => { const value = translatedValue(node.dataset.i18n); if (value) node.textContent = value; }); translatedStaticTextNodes.forEach(({ node, key }) => { const value = translatedValue(key); if (value && node.isConnected) setTranslatedTextNode(node, value); }); document.querySelectorAll("[placeholder]").forEach((node) => { const original = node.dataset.i18nPlaceholder || placeholderTranslationKeys[node.getAttribute("placeholder")]; if (!original) return; node.dataset.i18nPlaceholder = original; const value = translatedValue(original); if (value) node.setAttribute("placeholder", value); }); document.querySelectorAll("[data-i18n-dynamic]").forEach((node) => { if (node.dataset.i18nRenderedText && node.textContent !== node.dataset.i18nRenderedText) { delete node.dataset.i18nDynamic; delete node.dataset.i18nValues; delete node.dataset.i18nRenderedText; return; } let values = {}; try { values = JSON.parse(node.dataset.i18nValues || "{}"); } catch { values = {}; } const value = translatedMessage(node.dataset.i18nDynamic, values); if (value) { node.dataset.i18nRenderedText = value; node.textContent = value; } }); updateInstallButton(); } // Browser state, persistence scrubbing, lookup indexes, and runtime hardening. const persistedPassengerWorkspacePages = ["request", "trips", "payment", "business", "rewards", "profile", "notices", "support"]; const persistedRiderWorkspacePages = ["overview", "initialize", "checks", "requests", "destination", "earnings", "payment", "ratings", "rewards", "notices", "support", "profile"]; const defaultState = { activeTab: "passenger", showRoleEntry: true, accountMode: { passenger: "signin", rider: "signin" }, passwordReset: { role: "", active: false, startedAt: null, phoneOtpSentAt: null, phoneOtpMaskedPhone: "", phoneOtpVerified: false, phoneOtpVerifiedAt: null, recoverySessionUserId: null, recoverySessionActivatedAt: null }, passengerPage: "request", passengerFareMode: "negotiable", riderPage: "overview", workspaceUiMemory: { passenger: null, rider: null }, riderAvailabilityActivated: false, passengerMenuOpen: false, filter: "all", riderDestinationScope: "preferred", riderMarketplaceDestinationFilter: { enabled: false, consent: false, country: "", city: "", area: "", query: "", appliedAt: null }, riderWorkloadMode: "normal", riderDecisionQueue: [], riderNearbyRequestAlertedKeys: [], riderDismissedRequestKeys: [], riderClearedPrePickupCancellationKeys: [], riderNavigationPreferenceOverride: null, notificationPreferences: { passenger: { all: true, ride: true, chat: true, fare: true, admin: true }, rider: { all: true, ride: true, chat: true, fare: true, admin: true } }, pendingProfileRecovery: null, language: "en", verification: { passenger: null, rider: null, passengerSignIn: null, riderSignIn: null }, sessions: { passenger: null, rider: null }, adminSession: null, adminPage: "overview", adminDetail: null, adminDirectorySearch: "", adminDirectoryRegion: "", adminDirectoryPages: { passengers: 0, riders: 0 }, adminBoardPages: {}, adminTransactionPage: 0, adminSystemEventPage: 0, adminMessagesPage: 0, adminSupportInboxPage: 0, adminReferralRewardPage: 0, adminInsuranceTelemetryPage: 0, adminInsuranceTelemetryFilters: { view: "segments", period: "", exportStatus: "", startedFrom: "", startedTo: "" }, selectedRequestId: null, passengerSelectedOfferId: null, passenger: null, rider: null, passengers: [], requests: [], riders: [], demoSeeded: false, paymentRequests: [], paymentAccounts: [], businessAccounts: [], businessSubscriptions: [], rideSettlements: [], rideTips: [], riderCompletedMileageSegments: [], financeAdjustments: [], riderDayPreferences: [], backgroundChecks: [], taxIdentityReferences: [], taxDocuments: [], rideRatings: [], riderRatingSummary: null, routeChangeRequests: [], routeChangePromptedIds: [], riderDecisionQueue: [], rejectedOfferIds: [], offers: [], chats: [], notifications: [], notificationPopupIds: [], pushSubscriptions: [], supportTickets: [], safetyReports: [] }; const bundledDemoRiderIds = new Set(["rider-amina", "rider-patrick"]); const bundledDemoRequestIds = new Set(["request-akwa-bonaberi", "request-bepanda-makepe"]); const bundledDemoOfferIds = new Set(["offer-amina-akwa", "offer-patrick-bepanda"]); const bundledDemoRiderEmails = new Set(["amina@example.com", "patrick@example.com"]); const bundledDemoRiderPhones = new Set(["237690111222", "237675222333"]); const bundledDemoRiderNationalIds = new Set(["CNI-88210", "CNI-55221"]); const workspaceTabs = ["passenger", "rider", "admin"]; const notificationPreferenceDefaults = { all: true, ride: true, chat: true, fare: true, admin: true }; let state = normalizeState(loadState()); let storageWriteWarningShown = false; let stateLookupCache = null; const phoneOtpCooldowns = new Map(); let pendingPickupGps = null; let selectedCurrentPickupGps = null; let passengerPickupGpsPromise = null; let passengerPickupGpsWatchId = null; let passengerPickupGpsWatchStartedAt = 0; let passengerPickupGpsWatchFallbackTimer = null; let selectedPickupPlace = null; let selectedDestinationPlace = null; const selectedStopPlaces = new Map(); const destinationPlaceDetailsCache = new Map(); const placeAutocompleteCache = new Map(); let placesAutocompleteRateLimitedUntil = 0; let pickupAutocompleteTimer = null; let pickupAutocompleteSessionToken = null; let pickupAutocompleteRequestId = 0; let destinationAutocompleteTimer = null; let destinationAutocompleteSessionToken = null; let destinationAutocompleteRequestId = 0; let rideStopAutocompleteTimer = null; let rideStopAutocompleteSessionToken = null; let rideStopAutocompleteRequestId = 0; let fareGuidanceTimer = null; let fareGuidanceRequestId = 0; let fareGuidanceInFlightKey = ""; let lastRouteFareGuidance = null; let lastRouteFareGuidanceKey = ""; let lastRouteEstimateError = null; let lastRouteEstimateAttemptKey = ""; let stablePassengerFareGuidance = null; let stablePassengerFareGuidanceKey = ""; let pendingLowFareOverrideKey = ""; const riderInitiatedRideCancellationRequestIds = new Set(); const passengerInitiatedRideMatchRequestIds = new Set(); let useCurrentPickupActivationInFlight = false; let passengerApproachRefreshTimer = null; let riderMarketplaceRefreshTimer = null; let riderNearbyAlertActiveId = null; let riderGpsWatchId = null; let riderScreenWakeLock = null; let riderScreenWakeLockRetryAfter = 0; const riderScreenWakeLockRetryDelayMs = 10 * 1000; let riderAutoGpsPaused = !state.riderAvailabilityActivated; let riderAutoGpsSyncPromise = null; let lastRiderAutoGpsSyncAt = 0; let lastRiderAutoGpsSyncPoint = null; let deferredInstallPrompt = null; let locationUpdateRpcUnavailable = { passenger: false, rider: false, liveGps: false, clearLiveGps: false }; let lastLocationUpdateSource = "not used"; let profileOnboardingRpcUnavailable = { profile: false, photo: false, riderApplication: false, riderComplianceRenewal: false }; let lastProfileOnboardingSource = "not used"; function loadState() { try { const saved = JSON.parse(localStorage.getItem(storageKey)); return saved ? { ...defaultState, ...saved } : structuredClone(defaultState); } catch { return structuredClone(defaultState); } } const storageMinimalAccountKeys = new Set([ "id", "supabaseUserId", "preferredLanguage", "country", "city", "area", "vehicle", "vehicleDesignation", "navigationPreference", "carBodyType", "status", "approvedAt", "trialEndsAt", "subscriptionPaidUntil", "rating", "backgroundCheckStatus", "backgroundCheckDecision", "createdAt" ]); function minimizedPrivateStatePatch() { return { adminDetail: null, adminPage: "overview", adminDirectorySearch: "", adminDirectoryRegion: "", adminDirectoryPages: { passengers: 0, riders: 0 }, adminBoardPages: {}, adminTransactionPage: 0, adminSystemEventPage: 0, adminMessagesPage: 0, adminSupportInboxPage: 0, adminReferralRewardPage: 0, adminInsuranceTelemetryPage: 0, adminInsuranceTelemetryFilters: { view: "segments", period: "", exportStatus: "", startedFrom: "", startedTo: "" }, requests: [], paymentRequests: [], paymentAccounts: [], businessAccounts: [], businessSubscriptions: [], rideSettlements: [], rideTips: [], riderCompletedMileageSegments: [], financeAdjustments: [], riderDayPreferences: [], backgroundChecks: [], taxIdentityReferences: [], taxDocuments: [], rideRatings: [], riderRatingSummary: null, offers: [], chats: [], notifications: [], supportTickets: [], safetyReports: [] }; } function shouldMinimizeStoredProfileData() { return appConfig.mode === "supabase" || strictProductionModeEnabled(); } function minimalStorageAccount(record) { return Object.fromEntries( Object.entries(record).filter(([key, value]) => storageMinimalAccountKeys.has(key) && value !== undefined) ); } function storageSafeAccount(record, options = {}) { if (!record || typeof record !== "object") return record ?? null; const copy = { ...record }; delete copy.password; delete copy.passcode; delete copy.code; delete copy.access_token; delete copy.refresh_token; return options.minimizeProfileData ? minimalStorageAccount(copy) : copy; } function storageSafeAccounts(records = [], options = {}) { return Array.isArray(records) ? records.map((record) => storageSafeAccount(record, options)).filter(Boolean) : []; } function storageSafeVerification(verification, options = {}) { if (options.minimizeProfileData) return null; if (!verification?.verifiedAt) return null; const phone = verification.phone ?? verification.verifiedPhone ?? ""; if (!phone) return null; return { phone, phoneDigits: verification.phoneDigits ?? phoneDigits(phone), verifiedPhone: verification.verifiedPhone ?? phone, verifiedAt: verification.verifiedAt, userId: verification.userId ?? null, provider: verification.provider ?? "unknown" }; } function storageSafeSession(session, options = {}) { if (!session) return null; if (options.minimizeProfileData) { const userId = session.userId ?? null; return userId ? { userId, signedInAt: session.signedInAt ?? null } : null; } const safeSession = { phone: session.phone ?? "", email: session.email ?? "", userId: session.userId ?? null, signedInAt: session.signedInAt ?? null }; return safeSession.phone || safeSession.email || safeSession.userId ? safeSession : null; } function workspaceUiRoleConfig(role) { if (role === "passenger") { return { pageKey: "passengerPage", selectedPage: "trips", pages: persistedPassengerWorkspacePages }; } if (role === "rider") { return { pageKey: "riderPage", selectedPage: "requests", pages: persistedRiderWorkspacePages }; } return null; } function workspaceUiAccountKey(role, account = state?.[role], session = state?.sessions?.[role]) { const value = account?.supabaseUserId ?? account?.id ?? session?.userId ?? account?.email ?? session?.email ?? account?.phone ?? session?.phone ?? ""; return String(value ?? "").trim().toLowerCase(); } function normalizeWorkspaceUiMemoryEntry(role, entry = null) { const config = workspaceUiRoleConfig(role); if (!config || !entry || typeof entry !== "object") return null; const page = String(entry.page ?? "").trim().toLowerCase(); if (!config.pages.includes(page)) return null; const selectedRequestId = typeof entry.selectedRequestId === "string" && entry.selectedRequestId.trim() ? entry.selectedRequestId.trim() : null; const accountKey = String(entry.accountKey ?? "").trim().toLowerCase(); const updatedAt = String(entry.updatedAt ?? "").trim(); return { accountKey, page, selectedRequestId, updatedAt: updatedAt && !Number.isNaN(new Date(updatedAt).getTime()) ? updatedAt : null }; } function normalizeWorkspaceUiMemory(value = {}) { const source = value && typeof value === "object" ? value : {}; return { passenger: normalizeWorkspaceUiMemoryEntry("passenger", source.passenger), rider: normalizeWorkspaceUiMemoryEntry("rider", source.rider) }; } function selectedRequestIdForWorkspaceMemory(role, page, explicitValue) { if (explicitValue !== undefined) { return typeof explicitValue === "string" && explicitValue.trim() ? explicitValue.trim() : null; } const config = workspaceUiRoleConfig(role); if (!config || page !== config.selectedPage) return null; return typeof state.selectedRequestId === "string" && state.selectedRequestId.trim() ? state.selectedRequestId.trim() : null; } function rememberWorkspaceUiState(role, overrides = {}) { const config = workspaceUiRoleConfig(role); if (!config) return false; const page = String(overrides.page ?? state[config.pageKey] ?? "").trim().toLowerCase(); if (!config.pages.includes(page)) return false; state.workspaceUiMemory = normalizeWorkspaceUiMemory(state.workspaceUiMemory); state.workspaceUiMemory[role] = { accountKey: workspaceUiAccountKey(role), page, selectedRequestId: selectedRequestIdForWorkspaceMemory(role, page, overrides.selectedRequestId), updatedAt: new Date().toISOString() }; return true; } function rememberActiveWorkspaceUiState() { const role = state.activeTab; if (!["passenger", "rider"].includes(role)) return false; if (!state.sessions?.[role] || !state[role]) return false; return rememberWorkspaceUiState(role); } function workspaceUiMemoryForRole(role) { state.workspaceUiMemory = normalizeWorkspaceUiMemory(state.workspaceUiMemory); const entry = state.workspaceUiMemory[role]; if (!entry) return null; const accountKey = workspaceUiAccountKey(role); if (entry.accountKey && (!accountKey || entry.accountKey !== accountKey)) return null; return entry; } function availableWorkspacePagesForUiMemory(role) { if (role === "passenger" && typeof availablePassengerWorkspacePages === "function") { return availablePassengerWorkspacePages(); } if (role === "rider" && typeof availableRiderWorkspacePages === "function") { return availableRiderWorkspacePages(typeof currentRiderRecord === "function" ? currentRiderRecord() : undefined); } return workspaceUiRoleConfig(role)?.pages ?? []; } function restoreWorkspaceUiState(role, { replaceRoute = true, preferPathRoute = true } = {}) { const config = workspaceUiRoleConfig(role); const entry = workspaceUiMemoryForRole(role); if (!config || !entry) return false; const availablePages = availableWorkspacePagesForUiMemory(role); let page = entry.page; const selectedRequestId = entry.selectedRequestId; if (selectedRequestId) page = config.selectedPage; if (!availablePages.includes(page)) return false; state.activeTab = role; state.showRoleEntry = false; state[config.pageKey] = page; state.selectedRequestId = selectedRequestId; if (role === "passenger") { if (typeof passengerWorkspacePageSelectedInSession !== "undefined") passengerWorkspacePageSelectedInSession = true; if (typeof updatePassengerWorkspaceRoute === "function") { updatePassengerWorkspaceRoute(page, { replace: replaceRoute, requestId: selectedRequestId ?? "", preferPathRoute }); } } if (role === "rider" && typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute(page, { replace: replaceRoute, requestId: selectedRequestId ?? "" }); } return true; } function storageSafePendingProfileRecovery(recovery) { if (!recovery || typeof recovery !== "object") return null; const role = recovery.role === "rider" ? "rider" : recovery.role === "passenger" ? "passenger" : ""; const email = String(recovery.email ?? "").trim().toLowerCase(); if (!role || !email) return null; return { role, email, userId: recovery.userId ?? null, phone: recovery.phone ?? "", name: recovery.name ?? "", startedAt: recovery.startedAt ?? new Date().toISOString() }; } function storageSafePasswordReset(reset) { if (!reset || typeof reset !== "object") return { role: "", active: false, startedAt: null }; const role = reset.role === "rider" ? "rider" : reset.role === "passenger" ? "passenger" : ""; const active = role && reset.active === true; return { role: active ? role : "", active: Boolean(active), startedAt: active ? reset.startedAt || new Date().toISOString() : null, phoneOtpSentAt: active ? reset.phoneOtpSentAt || null : null, phoneOtpMaskedPhone: active ? String(reset.phoneOtpMaskedPhone || "") : "", phoneOtpVerified: false, phoneOtpVerifiedAt: null, recoverySessionUserId: active ? reset.recoverySessionUserId || null : null, recoverySessionActivatedAt: active ? reset.recoverySessionActivatedAt || null : null }; } function storageSafeAdminSession(session) { if (session?.source !== "demo" || !session.email || !demoAdminSignInAllowed()) return null; return { email: session.email, source: "demo", signedInAt: session.signedInAt ?? new Date().toISOString() }; } function storageSafeAdminPageKey(pageKey) { const normalized = String(pageKey ?? "").trim().toLowerCase(); if (normalized === "payments" || normalized === "payment" || normalized === "finance") return "accounting"; if (normalized === "support" || normalized === "support-inbox") return "safety"; if (normalized === "tax" || normalized === "checks") return "compliance"; return normalized; } function stateForStorage(options = {}) { const minimizeProfileData = options.minimizeProfileData ?? shouldMinimizeStoredProfileData(); const storageOptions = { minimizeProfileData }; const safeAdminSession = minimizeProfileData ? null : storageSafeAdminSession(state.adminSession); return { ...state, ...(minimizeProfileData ? minimizedPrivateStatePatch() : {}), verification: { passenger: storageSafeVerification(state.verification?.passenger, storageOptions), rider: storageSafeVerification(state.verification?.rider, storageOptions), passengerSignIn: null, riderSignIn: null }, sessions: { passenger: storageSafeSession(state.sessions?.passenger, storageOptions), rider: storageSafeSession(state.sessions?.rider, storageOptions) }, adminSession: safeAdminSession, adminDetail: safeAdminSession ? state.adminDetail : null, passenger: storageSafeAccount(state.passenger, storageOptions), rider: storageSafeAccount(state.rider, storageOptions), passengers: storageSafeAccounts(minimizeProfileData ? [state.passenger].filter(Boolean) : state.passengers, storageOptions), riders: storageSafeAccounts(minimizeProfileData ? [state.rider].filter(Boolean) : state.riders, storageOptions) }; } function minimizeRuntimeProfileState() { const storageOptions = { minimizeProfileData: true }; state.verification = { passenger: null, rider: null, passengerSignIn: null, riderSignIn: null }; state.sessions = { passenger: storageSafeSession(state.sessions?.passenger, storageOptions), rider: storageSafeSession(state.sessions?.rider, storageOptions) }; Object.assign(state, minimizedPrivateStatePatch()); state.passenger = storageSafeAccount(state.passenger, storageOptions); state.rider = storageSafeAccount(state.rider, storageOptions); state.passengers = storageSafeAccounts([state.passenger].filter(Boolean), storageOptions); state.riders = storageSafeAccounts([state.rider].filter(Boolean), storageOptions); clearStateLookupIndexes(); } function hardenStateForRuntime() { const safeAdminSession = storageSafeAdminSession(state.adminSession); let shouldRewriteStoredState = shouldMinimizeStoredProfileData(); if (shouldRewriteStoredState) minimizeRuntimeProfileState(); if (!safeAdminSession) { shouldRewriteStoredState ||= Boolean(state.adminSession || state.adminDetail); state.adminSession = null; state.adminDetail = null; if (typeof resetAdminData === "function") resetAdminData(); } else { state.adminSession = safeAdminSession; } if (shouldRewriteStoredState) saveState(); } function normalizeRiderMarketplaceDestinationFilter(value = {}) { const source = value && typeof value === "object" ? value : {}; const filter = { enabled: source.enabled === true, consent: source.consent === true, country: String(source.country ?? "").trim(), city: String(source.city ?? "").trim(), area: String(source.area ?? "").trim(), query: String(source.query ?? "").trim(), appliedAt: null }; if (filter.country && !countryCities[filter.country]) filter.country = ""; if (filter.country && filter.city && !countries[filter.country]?.[filter.city]) filter.city = ""; if (filter.country && filter.city && filter.area) { const areaExists = (countries[filter.country]?.[filter.city] ?? []).some((area) => area.name === filter.area); if (!areaExists) filter.area = ""; } const appliedAt = String(source.appliedAt ?? "").trim(); if (appliedAt && !Number.isNaN(new Date(appliedAt).getTime())) filter.appliedAt = appliedAt; if (!filter.consent || !(filter.country || filter.city || filter.area || filter.query)) filter.enabled = false; return filter; } function normalizeNotificationPreferenceSet(value = {}) { const source = value && typeof value === "object" ? value : {}; return Object.fromEntries( Object.entries(notificationPreferenceDefaults).map(([key, fallback]) => [key, source[key] !== false && fallback]) ); } function normalizeNotificationPreferences(value = {}) { const source = value && typeof value === "object" ? value : {}; return { passenger: normalizeNotificationPreferenceSet(source.passenger), rider: normalizeNotificationPreferenceSet(source.rider) }; } function normalizeMarketplaceFareChange(value = null) { const source = value && typeof value === "object" ? value : {}; const direction = source.direction === "down" ? "down" : source.direction === "up" ? "up" : ""; const amount = Number(source.amount); const previousFare = Number(source.previousFare ?? source.previous_fare); const currentFare = Number(source.currentFare ?? source.current_fare); const changedAt = String(source.changedAt ?? source.changed_at ?? "").trim(); if (!direction || !Number.isFinite(amount) || amount <= 0 || !Number.isFinite(previousFare) || !Number.isFinite(currentFare)) { return null; } return { direction, amount, previousFare, currentFare, changedAt: changedAt && !Number.isNaN(new Date(changedAt).getTime()) ? changedAt : new Date().toISOString() }; } function normalizeAdminInsuranceTelemetryFilters(value = {}) { const source = value && typeof value === "object" ? value : {}; return { view: ["daily", "segments"].includes(String(source.view ?? "").trim()) ? String(source.view).trim() : "segments", period: ["", "all", "p1_app_open", "p2_match_accepted", "p3_passenger_in_car"].includes(String(source.period ?? "").trim()) ? String(source.period ?? "").trim() : "", exportStatus: ["", "all", "pending", "exported", "excluded"].includes(String(source.exportStatus ?? "").trim()) ? String(source.exportStatus ?? "").trim() : "", startedFrom: /^\d{4}-\d{2}-\d{2}$/.test(String(source.startedFrom ?? "").trim()) ? String(source.startedFrom).trim() : "", startedTo: /^\d{4}-\d{2}-\d{2}$/.test(String(source.startedTo ?? "").trim()) ? String(source.startedTo).trim() : "" }; } function normalizeRiderDecisionQueue(value = []) { return (Array.isArray(value) ? value : []) .filter((item) => item && typeof item === "object" && item.requestId) .map((item) => { const createdAt = String(item.createdAt ?? new Date().toISOString()); return { id: String(item.id || ["rider-decision", item.riderId, item.requestId, item.eventType, createdAt].filter(Boolean).join(":")), riderId: String(item.riderId ?? ""), requestId: String(item.requestId), title: String(item.title ?? "Ride update"), body: String(item.body ?? ""), eventType: String(item.eventType ?? "ride_update"), fareOffer: Number.isFinite(Number(item.fareOffer)) ? Number(item.fareOffer) : null, createdAt }; }) .slice(-25); } function normalizeState(nextState) { nextState.language ||= "en"; nextState.showRoleEntry = nextState.showRoleEntry !== false; if (!["all", "car"].includes(nextState.filter)) nextState.filter = "all"; nextState.riderDestinationScope = nextState.riderDestinationScope === "all" ? "all" : "preferred"; nextState.riderMarketplaceDestinationFilter = normalizeRiderMarketplaceDestinationFilter(nextState.riderMarketplaceDestinationFilter); nextState.riderWorkloadMode = ["normal", "focus"].includes(nextState.riderWorkloadMode) ? nextState.riderWorkloadMode : "normal"; nextState.riderDecisionQueue = normalizeRiderDecisionQueue(nextState.riderDecisionQueue); nextState.riderNearbyRequestAlertedKeys = Array.isArray(nextState.riderNearbyRequestAlertedKeys) ? nextState.riderNearbyRequestAlertedKeys.filter(Boolean).slice(-200) : []; nextState.riderDismissedRequestKeys = Array.isArray(nextState.riderDismissedRequestKeys) ? nextState.riderDismissedRequestKeys.filter(Boolean).slice(-300) : []; nextState.riderClearedPrePickupCancellationKeys = Array.isArray(nextState.riderClearedPrePickupCancellationKeys) ? nextState.riderClearedPrePickupCancellationKeys.filter(Boolean).slice(-100) : []; nextState.riderNavigationPreferenceOverride = nextState.riderNavigationPreferenceOverride && typeof nextState.riderNavigationPreferenceOverride === "object" && ["google_maps", "waze"].includes(String(nextState.riderNavigationPreferenceOverride.value || "").trim().toLowerCase()) ? (() => { const riderId = String(nextState.riderNavigationPreferenceOverride.riderId || "").trim(); const riderIds = [ riderId, ...(Array.isArray(nextState.riderNavigationPreferenceOverride.riderIds) ? nextState.riderNavigationPreferenceOverride.riderIds : []) ] .map((value) => String(value ?? "").trim()) .filter(Boolean) .filter((value, index, values) => values.indexOf(value) === index) .slice(-8); return { riderId: riderIds[0] || riderId, riderIds, value: String(nextState.riderNavigationPreferenceOverride.value).trim().toLowerCase() === "waze" ? "waze" : "google_maps", updatedAt: nextState.riderNavigationPreferenceOverride.updatedAt || null }; })() : null; nextState.pendingProfileRecovery = storageSafePendingProfileRecovery(nextState.pendingProfileRecovery); nextState.notificationPreferences = normalizeNotificationPreferences(nextState.notificationPreferences); nextState.passengerPage = persistedPassengerWorkspacePages.includes(nextState.passengerPage) ? nextState.passengerPage : "request"; nextState.passengerFareMode = normalizePassengerFareMode(nextState.passengerFareMode); nextState.riderPage = persistedRiderWorkspacePages.includes(nextState.riderPage) ? nextState.riderPage : "overview"; nextState.selectedRequestId = typeof nextState.selectedRequestId === "string" && nextState.selectedRequestId.trim() ? nextState.selectedRequestId.trim() : null; nextState.workspaceUiMemory = normalizeWorkspaceUiMemory(nextState.workspaceUiMemory); nextState.riderAvailabilityActivated = nextState.riderAvailabilityActivated === true; nextState.accountMode ||= { passenger: "signin", rider: "signin" }; nextState.accountMode.passenger = ["signin", "create"].includes(nextState.accountMode.passenger) ? nextState.accountMode.passenger : "signin"; nextState.accountMode.rider = ["signin", "create"].includes(nextState.accountMode.rider) ? nextState.accountMode.rider : "signin"; nextState.passwordReset = storageSafePasswordReset(nextState.passwordReset); nextState.verification ||= { passenger: null, rider: null }; nextState.verification.passenger = storageSafeVerification(nextState.verification.passenger); nextState.verification.rider = storageSafeVerification(nextState.verification.rider); nextState.verification.passengerSignIn = null; nextState.verification.riderSignIn = null; nextState.sessions ||= { passenger: null, rider: null }; nextState.sessions.passenger = storageSafeSession(nextState.sessions.passenger); nextState.sessions.rider = storageSafeSession(nextState.sessions.rider); nextState.adminSession = storageSafeAdminSession(nextState.adminSession); nextState.adminDetail = nextState.adminSession ? nextState.adminDetail : null; const normalizedAdminPage = storageSafeAdminPageKey(nextState.adminPage); nextState.adminPage = nextState.adminSession && ["overview", "alerts", "geography", "activity", "reports", "controls", "messages", "accounting", "telemetry", "rewards", "compliance", "approvals", "passengers", "riders", "safety", "audit", "diagnostics", "account"].includes(normalizedAdminPage) ? normalizedAdminPage : "overview"; nextState.adminDirectorySearch ||= ""; nextState.adminDirectoryRegion ||= ""; nextState.adminDirectoryPages ||= { passengers: 0, riders: 0 }; nextState.adminDirectoryPages.passengers ||= 0; nextState.adminDirectoryPages.riders ||= 0; nextState.adminBoardPages ||= {}; Object.entries(nextState.adminBoardPages).forEach(([key, value]) => { nextState.adminBoardPages[key] = Math.max(0, Number(value) || 0); }); nextState.adminTransactionPage = Math.max(0, Number(nextState.adminTransactionPage) || 0); nextState.adminSystemEventPage = Math.max(0, Number(nextState.adminSystemEventPage) || 0); nextState.adminMessagesPage = Math.max(0, Number(nextState.adminMessagesPage) || 0); nextState.adminSupportInboxPage = Math.max(0, Number(nextState.adminSupportInboxPage) || 0); nextState.adminReferralRewardPage = Math.max(0, Number(nextState.adminReferralRewardPage) || 0); nextState.adminInsuranceTelemetryPage = Math.max(0, Number(nextState.adminInsuranceTelemetryPage) || 0); nextState.adminInsuranceTelemetryFilters = normalizeAdminInsuranceTelemetryFilters(nextState.adminInsuranceTelemetryFilters); nextState.passengerSelectedOfferId = typeof nextState.passengerSelectedOfferId === "string" ? nextState.passengerSelectedOfferId : null; nextState.passenger = storageSafeAccount(nextState.passenger); nextState.rider = storageSafeAccount(nextState.rider); nextState.passengers = storageSafeAccounts(nextState.passengers ?? []); if (nextState.passenger && !nextState.passengers.some((passenger) => passenger.id === nextState.passenger.id)) { nextState.passengers.unshift(nextState.passenger); } nextState.riders = storageSafeAccounts(nextState.riders ?? []); nextState.requests ||= []; nextState.riders = nextState.riders.map((rider) => ({ ...storageSafeAccount(rider), carBodyType: normalizeCarBodyType(rider.carBodyType ?? rider.car_body_type) })); nextState.requests = nextState.requests.map((request) => ({ ...request, businessAccountId: request.businessAccountId ?? request.business_account_id ?? null, carTypePreference: normalizeCarTypePreference(request.carTypePreference ?? request.car_type_preference), fareMode: normalizePassengerFareMode(request.fareMode ?? request.fare_mode), rideStops: normalizeRideStops(request.rideStops ?? request.ride_stops), rideStopPoints: normalizeRideStopPoints(request.rideStopPoints ?? request.ride_stop_points, request.rideStops ?? request.ride_stops), currentStopIndex: Math.max(0, Number(request.currentStopIndex ?? request.current_stop_index ?? 0) || 0), lastStopArrivedAt: request.lastStopArrivedAt ?? request.last_stop_arrived_at ?? null, estimatedDistanceMiles: request.estimatedDistanceMiles ?? request.estimated_distance_miles ?? null, estimatedTravelMinutes: request.estimatedTravelMinutes ?? request.estimated_travel_minutes ?? null, routeEstimateSource: request.routeEstimateSource ?? request.route_estimate_source ?? null, routeEstimateProvider: request.routeEstimateProvider ?? request.route_estimate_provider ?? null, routeEstimateCached: Boolean(request.routeEstimateCached ?? request.route_estimate_cached), routeEstimateKey: request.routeEstimateKey ?? request.route_estimate_key ?? null, routeEstimatePolyline: request.routeEstimatePolyline ?? request.route_estimate_polyline ?? null, routeEstimateDestinationFingerprint: request.routeEstimateDestinationFingerprint ?? request.route_estimate_destination_fingerprint ?? null, routeEstimateCreatedAt: request.routeEstimateCreatedAt ?? request.route_estimate_created_at ?? null, fareHistory: Array.isArray(request.fareHistory ?? request.fare_history) ? (request.fareHistory ?? request.fare_history) : [], marketplaceFareChange: normalizeMarketplaceFareChange(request.marketplaceFareChange ?? request.marketplace_fare_change), acceptedRouteChangeFare: Number(request.acceptedRouteChangeFare ?? request.accepted_route_change_fare ?? 0) || 0, arrivedAt: request.arrivedAt ?? request.arrived_at ?? null, startedAt: request.startedAt ?? request.started_at ?? null, completedAt: request.completedAt ?? request.completed_at ?? null, cancellationFeeAmount: request.cancellationFeeAmount ?? request.cancellation_fee_amount ?? 0, cancellationFeeCurrency: request.cancellationFeeCurrency ?? request.cancellation_fee_currency ?? moneyCurrencyForCountry(request.country), cancellationFeeStatus: request.cancellationFeeStatus ?? request.cancellation_fee_status ?? "not_applicable", cancellationFeeRiderId: request.cancellationFeeRiderId ?? request.cancellation_fee_rider_id ?? null, cancellationFeeElapsedMinutes: request.cancellationFeeElapsedMinutes ?? request.cancellation_fee_elapsed_minutes ?? null })); nextState.offers ||= []; nextState.demoSeeded = Boolean(nextState.demoSeeded); stripBundledDemoData(nextState); nextState.chats ||= []; nextState.notifications ||= []; nextState.notificationPopupIds = Array.isArray(nextState.notificationPopupIds) ? nextState.notificationPopupIds.filter(Boolean).slice(-100) : []; nextState.pushSubscriptions ||= []; nextState.paymentRequests ||= []; nextState.paymentAccounts ||= []; nextState.businessAccounts ||= []; nextState.businessSubscriptions ||= []; nextState.rideSettlements ||= []; nextState.rideTips ||= []; nextState.riderCompletedMileageSegments ||= []; nextState.financeAdjustments ||= []; nextState.riderDayPreferences ||= []; nextState.backgroundChecks ||= []; nextState.taxIdentityReferences ||= []; nextState.taxDocuments ||= []; nextState.rideRatings ||= []; nextState.riderRatingSummary = nextState.riderRatingSummary && typeof nextState.riderRatingSummary === "object" ? nextState.riderRatingSummary : null; nextState.routeChangeRequests = (nextState.routeChangeRequests ?? []).map((change) => ({ ...change, status: ["pending", "accepted", "declined"].includes(change.status) ? change.status : "pending", destinationArea: change.destinationArea ?? change.destination_area ?? null, rideStops: normalizeRideStops(change.rideStops ?? change.ride_stops), rideStopPoints: normalizeRideStopPoints(change.rideStopPoints ?? change.ride_stop_points, change.rideStops ?? change.ride_stops), additionalFare: Number(change.additionalFare ?? 0) || 0, totalFare: Number(change.totalFare ?? 0) || 0 })); nextState.routeChangePromptedIds = Array.isArray(nextState.routeChangePromptedIds) ? nextState.routeChangePromptedIds.filter(Boolean) : []; nextState.rejectedOfferIds = Array.isArray(nextState.rejectedOfferIds) ? nextState.rejectedOfferIds.filter(Boolean).slice(-500) : []; nextState.supportTickets ||= []; nextState.safetyReports ||= []; return nextState; } function saveState() { clearStateLookupIndexes(); rememberActiveWorkspaceUiState(); try { localStorage.setItem(storageKey, JSON.stringify(stateForStorage())); } catch (error) { if (!storageWriteWarningShown) { logClientWarning("Waka local state could not be saved. Continuing with in-memory state for this session.", error); storageWriteWarningShown = true; } } } function clearStateLookupIndexes() { stateLookupCache = null; } function stateLookupIndexes() { const requests = state.requests ?? []; const riders = state.riders ?? []; const offers = state.offers ?? []; if ( stateLookupCache && stateLookupCache.requests === requests && stateLookupCache.riders === riders && stateLookupCache.offers === offers && stateLookupCache.requestCount === requests.length && stateLookupCache.riderCount === riders.length && stateLookupCache.offerCount === offers.length ) { return stateLookupCache; } const requestMap = new Map(requests.map((request) => [request.id, request])); const riderMap = new Map(riders.map((rider) => [rider.id, rider])); const offerMap = new Map(offers.map((offer) => [offer.id, offer])); const offersByRequestId = new Map(); offers.forEach((offer) => { if (!offersByRequestId.has(offer.requestId)) offersByRequestId.set(offer.requestId, []); offersByRequestId.get(offer.requestId).push(offer); }); offersByRequestId.forEach((requestOffers) => { requestOffers.sort((a, b) => Number(a.fare) - Number(b.fare)); }); stateLookupCache = { requests, riders, offers, requestCount: requests.length, riderCount: riders.length, offerCount: offers.length, requestMap, riderMap, offerMap, offersByRequestId }; return stateLookupCache; } function isBundledDemoRider(record) { const email = String(record?.email ?? "").toLowerCase(); const phone = phoneDigits(record?.phone); const nationalId = String(record?.nationalId ?? record?.national_id_number ?? "").toUpperCase(); return bundledDemoRiderIds.has(record?.id) || bundledDemoRiderEmails.has(email) || bundledDemoRiderPhones.has(phone) || bundledDemoRiderNationalIds.has(nationalId); } function stripBundledDemoData(nextState) { if (isBundledDemoRider(nextState.rider)) { nextState.rider = null; if (nextState.sessions) nextState.sessions.rider = null; } nextState.riders = (nextState.riders ?? []).filter((rider) => !isBundledDemoRider(rider)); nextState.requests = (nextState.requests ?? []).filter((request) => !bundledDemoRequestIds.has(request.id)); nextState.offers = (nextState.offers ?? []).filter((offer) => !bundledDemoOfferIds.has(offer.id) && !bundledDemoRiderIds.has(offer.riderId)); nextState.taxIdentityReferences = (nextState.taxIdentityReferences ?? []).filter((reference) => !bundledDemoRiderIds.has(reference.riderId)); if (bundledDemoRequestIds.has(nextState.selectedRequestId)) nextState.selectedRequestId = null; return nextState; } function upsertById(items, item) { return [item, ...items.filter((existing) => existing.id !== item.id)]; } function makeId(prefix) { return crypto.randomUUID ? crypto.randomUUID() : `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`; } // Shared formatting, validation, DOM handles, routing, and UI utility helpers. function redactClientLogText(value) { return String(value ?? "") .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[redacted-jwt]") .replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, "[redacted-email]") .replace(/\+?\d[\d\s().-]{7,}\d/g, "[redacted-phone]") .replace(/\b(access_token|refresh_token|authorization|apikey|api[_-]?key|secret|password|client[_-]?secret|card|cvc|setup_intent)=([^&\s"'<>]+)/gi, "$1=[redacted]") .slice(0, 500); } function safeClientLogDetail(detail) { if (!detail) return detail; if (detail instanceof Error) { return { name: redactClientLogText(detail.name || "Error"), message: redactClientLogText(detail.message || ""), code: redactClientLogText(detail.code || detail.status || "") }; } if (typeof detail === "string" || typeof detail === "number" || typeof detail === "boolean") { return redactClientLogText(detail); } if (typeof detail === "object") { return { name: redactClientLogText(detail.name || detail.error || "ClientError"), message: redactClientLogText(detail.message || detail.msg || detail.error_description || ""), code: redactClientLogText(detail.code || detail.status || detail.statusCode || "") }; } return redactClientLogText(detail); } function clientLogShouldRedact() { return Boolean( (typeof strictProductionModeEnabled === "function" && strictProductionModeEnabled()) || (typeof appConfig === "object" && appConfig?.mode === "supabase") ); } function logClientWarning(message, ...details) { const safeMessage = redactClientLogText(message); const safeDetails = clientLogShouldRedact() ? details.map(safeClientLogDetail) : details; console.warn(safeMessage, ...safeDetails); } const clientRuntimeTelemetryStorageKey = "waka-client-runtime-events-v1"; const clientRuntimeTelemetryMaxStored = 20; const clientRuntimeTelemetryMaxFlush = 5; const clientRuntimeTelemetryFlushCooldownMs = 30 * 1000; const clientRuntimeTelemetryDedupeMs = 60 * 1000; let clientRuntimeTelemetryInstalled = false; let clientRuntimeTelemetryFlushInFlight = false; let clientRuntimeTelemetryLastFlushAt = 0; const clientRuntimeTelemetryRecent = new Map(); function clientEventIngestFunctionName() { return String(appConfig.clientEventIngestFunctionName || "client-event-ingest").trim(); } function clientRuntimeTelemetryEnabled() { return Boolean( typeof window !== "undefined" && appConfig.mode === "supabase" && configFlagEnabled(appConfig.clientErrorTelemetryEnabled ?? true) && hasSupabaseConfig() && clientEventIngestFunctionName() ); } function clientRuntimeTelemetryAuthToken() { return supabaseRestSession?.access_token || ""; } function clientRuntimeTelemetryId() { try { if (window.crypto?.randomUUID) return window.crypto.randomUUID(); } catch {} return `client-${Date.now()}-${Math.random().toString(36).slice(2)}`; } function clientRuntimeTelemetryPath() { try { return window.location?.pathname || "/"; } catch { return "/"; } } function clientRuntimeTelemetryScreen() { try { return `${window.screen?.width || 0}x${window.screen?.height || 0}`; } catch { return ""; } } function clientRuntimeTelemetryQueue() { try { const stored = JSON.parse(localStorage.getItem(clientRuntimeTelemetryStorageKey) || "[]"); return Array.isArray(stored) ? stored.slice(0, clientRuntimeTelemetryMaxStored) : []; } catch { return []; } } function saveClientRuntimeTelemetryQueue(events) { try { localStorage.setItem( clientRuntimeTelemetryStorageKey, JSON.stringify(events.slice(-clientRuntimeTelemetryMaxStored)) ); } catch (error) { logClientWarning("Client runtime telemetry queue could not be saved.", error); } } function forgetClientRuntimeTelemetryEvents(sentIds = []) { if (!sentIds.length) return; const sent = new Set(sentIds); saveClientRuntimeTelemetryQueue(clientRuntimeTelemetryQueue().filter((event) => !sent.has(event.id))); } function clientRuntimeTelemetryMessage(errorLike, fallback = "Client runtime event.") { if (errorLike instanceof Error) return errorLike.message || fallback; if (typeof errorLike === "string") return errorLike; if (typeof errorLike === "object" && errorLike) { return errorLike.message || errorLike.reason || errorLike.error || fallback; } return fallback; } function clientRuntimeTelemetryName(errorLike) { if (errorLike instanceof Error) return errorLike.name || "Error"; if (typeof errorLike === "object" && errorLike) return errorLike.name || errorLike.code || "ClientError"; return "ClientError"; } function clientRuntimeTelemetryRecord(eventType, errorLike, context = {}) { const message = redactClientLogText(clientRuntimeTelemetryMessage(errorLike)); const errorName = redactClientLogText(clientRuntimeTelemetryName(errorLike)); const source = redactClientLogText(context.source || ""); const line = Number.isFinite(Number(context.line)) ? Number(context.line) : null; const column = Number.isFinite(Number(context.column)) ? Number(context.column) : null; const dedupeKey = `${eventType}:${message}:${source}:${line}:${column}`; const now = Date.now(); const recentAt = clientRuntimeTelemetryRecent.get(dedupeKey) || 0; if (now - recentAt < clientRuntimeTelemetryDedupeMs) return null; clientRuntimeTelemetryRecent.set(dedupeKey, now); for (const [key, seenAt] of clientRuntimeTelemetryRecent) { if (now - seenAt > clientRuntimeTelemetryDedupeMs * 2) clientRuntimeTelemetryRecent.delete(key); } return { id: clientRuntimeTelemetryId(), occurredAt: new Date(now).toISOString(), eventType, severity: eventType === "client_warning" ? "warning" : "error", message, metadata: { runtimeRole, activeTab: state?.activeTab || "", path: clientRuntimeTelemetryPath(), visibilityState: document.visibilityState || "", online: navigator.onLine !== false, language: navigator.language || "", screen: clientRuntimeTelemetryScreen(), errorName, errorCode: redactClientLogText(errorLike?.code || errorLike?.status || ""), source, line, column } }; } function enqueueClientRuntimeTelemetry(event) { if (!event || !clientRuntimeTelemetryEnabled()) return; const queue = clientRuntimeTelemetryQueue(); queue.push(event); saveClientRuntimeTelemetryQueue(queue); void flushClientRuntimeTelemetry(); } async function flushClientRuntimeTelemetry({ force = false } = {}) { if (!clientRuntimeTelemetryEnabled()) return; const token = clientRuntimeTelemetryAuthToken(); if (!token) return; const now = Date.now(); if (!force && now - clientRuntimeTelemetryLastFlushAt < clientRuntimeTelemetryFlushCooldownMs) return; if (clientRuntimeTelemetryFlushInFlight) return; const events = clientRuntimeTelemetryQueue().slice(0, clientRuntimeTelemetryMaxFlush); if (!events.length) return; clientRuntimeTelemetryFlushInFlight = true; clientRuntimeTelemetryLastFlushAt = now; try { const response = await fetch(`${appConfig.supabaseUrl}/functions/v1/${clientEventIngestFunctionName()}`, { method: "POST", headers: { "content-type": "application/json", apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${token}` }, body: JSON.stringify({ events }), keepalive: true }); if (!response.ok) throw new Error(`Client event ingest failed with HTTP ${response.status}.`); forgetClientRuntimeTelemetryEvents(events.map((event) => event.id)); } catch (error) { logClientWarning("Client runtime telemetry flush was skipped.", error); } finally { clientRuntimeTelemetryFlushInFlight = false; } } function installClientRuntimeErrorReporting() { if (clientRuntimeTelemetryInstalled || typeof window === "undefined") return; clientRuntimeTelemetryInstalled = true; window.addEventListener("error", (event) => { enqueueClientRuntimeTelemetry(clientRuntimeTelemetryRecord("client_runtime_error", event.error || event.message, { source: event.filename, line: event.lineno, column: event.colno })); }, true); window.addEventListener("unhandledrejection", (event) => { enqueueClientRuntimeTelemetry(clientRuntimeTelemetryRecord("client_unhandled_rejection", event.reason || "Unhandled promise rejection.")); }); window.addEventListener("online", () => { void flushClientRuntimeTelemetry({ force: true }); }); document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") void flushClientRuntimeTelemetry({ force: true }); }); } const riderDocumentLabels = { driverLicense: "Driver's license", vehicleRegistration: "Registration document", insurance: "Insurance document", vehicleInspection: "Vehicle inspection document" }; function emptyRiderDocuments() { return { driverLicense: "", vehicleRegistration: "", insurance: "", vehicleInspection: "" }; } function requiredRiderDocuments() { return ["driverLicense", "vehicleRegistration", "insurance"]; } function parseRiderDocuments(value) { const documents = emptyRiderDocuments(); if (!value) return documents; if (typeof value === "object") { return { ...documents, ...value }; } const text = String(value).trim(); if (!text) return documents; try { const parsed = JSON.parse(text); if (parsed && typeof parsed === "object") return { ...documents, ...parsed }; } catch { return { ...documents, driverLicense: text }; } return { ...documents, driverLicense: text }; } function riderDocuments(rider) { const documents = { ...emptyRiderDocuments(), ...parseRiderDocuments(rider?.documentName), ...parseRiderDocuments(rider?.documents) }; if (rider?.driverLicenseDocumentName) documents.driverLicense = rider.driverLicenseDocumentName; if (rider?.vehicleRegistrationDocumentName) documents.vehicleRegistration = rider.vehicleRegistrationDocumentName; if (rider?.insuranceDocumentName) documents.insurance = rider.insuranceDocumentName; if (rider?.vehicleInspectionDocumentName) documents.vehicleInspection = rider.vehicleInspectionDocumentName; if (rider?.driverLicenseDocumentPath) documents.driverLicense = rider.driverLicenseDocumentPath; if (rider?.vehicleRegistrationDocumentPath) documents.vehicleRegistration = rider.vehicleRegistrationDocumentPath; if (rider?.insuranceDocumentPath) documents.insurance = rider.insuranceDocumentPath; if (rider?.vehicleInspectionDocumentPath) documents.vehicleInspection = rider.vehicleInspectionDocumentPath; return documents; } function selectedRiderDocumentFiles() { return { driverLicense: els.riderLicenseDocument.files[0] ?? null, vehicleRegistration: els.riderRegistrationDocument.files[0] ?? null, insurance: els.riderInsuranceDocument.files[0] ?? null, vehicleInspection: els.riderInspectionDocument.files[0] ?? null }; } function riderDocumentPayload(documents) { return JSON.stringify({ ...emptyRiderDocuments(), ...documents }); } function riderDocumentMetadata(rider) { const documents = riderDocuments(rider); return { vehicleDesignation: normalizeRiderVehicleDesignation(documents.vehicleDesignation ?? rider?.vehicleDesignation, rider?.carBodyType), navigationPreference: riderNavigationPreference(rider) }; } function riderApplicationDocumentPayload(documents, rider) { return riderDocumentPayload({ ...documents, vehicleDesignation: normalizeRiderVehicleDesignation(rider?.vehicleDesignation, rider?.carBodyType), navigationPreference: riderNavigationPreference(rider) }); } function missingRiderDocumentLabels(documents) { return requiredRiderDocuments() .filter((key) => !documents[key]) .map((key) => riderDocumentLabels[key]); } function riderDocumentSummary(rider) { const documents = riderDocuments(rider); const requiredSummary = requiredRiderDocuments() .map((key) => `${riderDocumentLabels[key]}: ${documents[key] || "missing"}`); const optionalSummary = documents.vehicleInspection ? [`${riderDocumentLabels.vehicleInspection}: ${documents.vehicleInspection}`] : ["Vehicle inspection: optional"]; return [...requiredSummary, ...optionalSummary].join(". "); } function inertElement(name = "pruned") { const classList = { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } }; const element = { name, value: "", checked: false, disabled: false, hidden: true, textContent: "", innerHTML: "", className: "", dataset: {}, style: {}, classList, children: [], files: [], options: [], selectedOptions: [], addEventListener() {}, removeEventListener() {}, append() {}, prepend() {}, replaceChildren() {}, reset() {}, focus() {}, click() {}, setAttribute() {}, removeAttribute() {}, getAttribute() { return null; }, matches() { return false; }, closest() { return null; }, querySelector() { return inertElement(`${name}:child`); }, querySelectorAll() { return []; } }; return element; } const els = { roleEntry: document.querySelector("#roleEntry"), workspace: document.querySelector("#workspace"), roleTabs: document.querySelector("#roleTabs"), connectionStatus: document.querySelector("#connectionStatus"), installApp: document.querySelector("#installApp"), deploymentUpdateNotice: document.querySelector("#deploymentUpdateNotice"), deploymentUpdateTitle: document.querySelector("#deploymentUpdateTitle"), deploymentUpdateMessage: document.querySelector("#deploymentUpdateMessage"), deploymentUpdateNow: document.querySelector("#deploymentUpdateNow"), languageSelect: document.querySelector("#languageSelect"), languageSelects: document.querySelectorAll("[data-language-select]"), passengerSignInForm: document.querySelector("#passengerSignInForm"), passengerSignInEmail: document.querySelector("#passengerSignInEmail"), passengerSignInPassword: document.querySelector("#passengerSignInPassword"), passengerSignInOtpPanel: document.querySelector("#passengerSignInOtpPanel"), passengerSignInPhone: document.querySelector("#passengerSignInPhone"), passengerSignInCode: document.querySelector("#passengerSignInCode"), sendPassengerSignInCode: document.querySelector("#sendPassengerSignInCode"), verifyPassengerSignIn: document.querySelector("#verifyPassengerSignIn"), forgotPassengerPassword: document.querySelector("#forgotPassengerPassword"), passengerPasswordResetPanel: document.querySelector("#passengerPasswordResetPanel"), passengerPasswordResetPhoneStep: document.querySelector("#passengerPasswordResetPhoneStep"), passengerPasswordResetPhoneHint: document.querySelector("#passengerPasswordResetPhoneHint"), passengerPasswordResetPhoneCode: document.querySelector("#passengerPasswordResetPhoneCode"), sendPassengerPasswordResetPhoneOtp: document.querySelector("#sendPassengerPasswordResetPhoneOtp"), verifyPassengerPasswordResetPhoneOtp: document.querySelector("#verifyPassengerPasswordResetPhoneOtp"), passengerPasswordResetPhoneStatus: document.querySelector("#passengerPasswordResetPhoneStatus"), passengerPasswordResetPasswordFields: document.querySelector("#passengerPasswordResetPasswordFields"), passengerResetPassword: document.querySelector("#passengerResetPassword"), passengerResetPasswordConfirm: document.querySelector("#passengerResetPasswordConfirm"), savePassengerResetPassword: document.querySelector("#savePassengerResetPassword"), passengerSignInStatus: document.querySelector("#passengerSignInStatus"), passengerAccountStage: document.querySelector("#passengerAccountStage"), passengerPanelHeading: document.querySelector("#passengerPanelHeading"), passengerWorkspaceHeader: document.querySelector("#passengerWorkspaceHeader"), passengerWorkspaceTitle: document.querySelector("#passengerWorkspaceTitle"), passengerWorkspaceMenu: document.querySelector("#passengerWorkspaceMenu"), passengerWorkspaceMenuToggle: document.querySelector("#passengerWorkspaceMenuToggle"), passengerSessionCard: document.querySelector("#passengerSessionCard"), passengerSessionTitle: document.querySelector("#passengerSessionTitle"), passengerSessionSummary: document.querySelector("#passengerSessionSummary"), passengerProfileAvatar: document.querySelector("#passengerProfileAvatar"), passengerProfilePhotoStatus: document.querySelector("#passengerProfilePhotoStatus"), passengerReferralPanel: document.querySelector("#passengerReferralPanel"), passengerReferralSummary: document.querySelector("#passengerReferralSummary"), passengerReferralCodeDisplay: document.querySelector("#passengerReferralCodeDisplay"), copyPassengerReferralCode: document.querySelector("#copyPassengerReferralCode"), sharePassengerReferralCode: document.querySelector("#sharePassengerReferralCode"), emailPassengerReferralCode: document.querySelector("#emailPassengerReferralCode"), textPassengerReferralCode: document.querySelector("#textPassengerReferralCode"), passengerReferralHowItWorks: document.querySelector("#passengerReferralHowItWorks"), passengerSignOut: document.querySelector("#passengerSignOut"), passengerMenuSignOut: document.querySelector("#passengerMenuSignOut"), passengerWorkspaceNav: document.querySelector("#passengerWorkspaceNav"), passengerPaymentForm: document.querySelector("#passengerPaymentForm"), passengerPaymentProvider: document.querySelector("#passengerPaymentProvider"), passengerBankName: document.querySelector("#passengerBankName"), passengerAccountHolder: document.querySelector("#passengerAccountHolder"), passengerAccountLast4: document.querySelector("#passengerAccountLast4"), passengerPaymentReference: document.querySelector("#passengerPaymentReference"), passengerPaymentStatus: document.querySelector("#passengerPaymentStatus"), startPassengerPaymentSetup: document.querySelector("#startPassengerPaymentSetup"), passengerLocationForm: document.querySelector("#passengerLocationForm"), passengerActiveCountry: document.querySelector("#passengerActiveCountry"), passengerActiveCity: document.querySelector("#passengerActiveCity"), passengerLocationStatus: document.querySelector("#passengerLocationStatus"), businessAccountForm: document.querySelector("#businessAccountForm"), businessName: document.querySelector("#businessName"), businessBillingEmail: document.querySelector("#businessBillingEmail"), businessCategory: document.querySelector("#businessCategory"), businessPlan: document.querySelector("#businessPlan"), businessAddress: document.querySelector("#businessAddress"), businessContactName: document.querySelector("#businessContactName"), businessContactPhone: document.querySelector("#businessContactPhone"), businessReferralCode: document.querySelector("#businessReferralCode"), businessReferralPanel: document.querySelector("#businessReferralPanel"), businessReferralSummary: document.querySelector("#businessReferralSummary"), businessReferralCodeDisplay: document.querySelector("#businessReferralCodeDisplay"), copyBusinessReferralCode: document.querySelector("#copyBusinessReferralCode"), shareBusinessReferralCode: document.querySelector("#shareBusinessReferralCode"), emailBusinessReferralCode: document.querySelector("#emailBusinessReferralCode"), textBusinessReferralCode: document.querySelector("#textBusinessReferralCode"), businessReferralHowItWorks: document.querySelector("#businessReferralHowItWorks"), businessAccountStatus: document.querySelector("#businessAccountStatus"), businessAccountList: document.querySelector("#businessAccountList"), passengerNoticePanel: document.querySelector("#passengerNoticePanel"), passengerNoticeList: document.querySelector("#passengerNoticeList"), passengerEnablePush: document.querySelector("#passengerEnablePush"), passengerPushStatus: document.querySelector("#passengerPushStatus"), passengerSupportForm: document.querySelector("#passengerSupportForm"), passengerSupportCategory: document.querySelector("#passengerSupportCategory"), passengerSupportSubject: document.querySelector("#passengerSupportSubject"), passengerSupportMessage: document.querySelector("#passengerSupportMessage"), passengerSupportStatus: document.querySelector("#passengerSupportStatus"), passengerAccountForm: document.querySelector("#passengerAccountForm"), passengerName: document.querySelector("#passengerName"), passengerEmail: document.querySelector("#passengerEmail"), passengerPassword: document.querySelector("#passengerPassword"), passengerPhoto: document.querySelector("#passengerPhoto"), passengerPhone: document.querySelector("#passengerPhone"), passengerVerificationCode: document.querySelector("#passengerVerificationCode"), sendPassengerCode: document.querySelector("#sendPassengerCode"), verifyPassengerPhone: document.querySelector("#verifyPassengerPhone"), passengerNationalId: document.querySelector("#passengerNationalId"), passengerDob: document.querySelector("#passengerDob"), passengerAccountUse: document.querySelector("#passengerAccountUse"), passengerReferralCode: document.querySelector("#passengerReferralCode"), passengerInitialBusinessFields: document.querySelector("#passengerInitialBusinessFields"), passengerInitialBusinessName: document.querySelector("#passengerInitialBusinessName"), passengerInitialBusinessBillingEmail: document.querySelector("#passengerInitialBusinessBillingEmail"), passengerInitialBusinessCategory: document.querySelector("#passengerInitialBusinessCategory"), passengerInitialBusinessPlan: document.querySelector("#passengerInitialBusinessPlan"), passengerInitialBusinessAddress: document.querySelector("#passengerInitialBusinessAddress"), passengerInitialBusinessReferralCode: document.querySelector("#passengerInitialBusinessReferralCode"), passengerCountry: document.querySelector("#passengerCountry"), passengerCity: document.querySelector("#passengerCity"), passengerStatus: document.querySelector("#passengerStatus"), passengerSaveButton: document.querySelector("#passengerSaveButton"), rideRequestForm: document.querySelector("#rideRequestForm"), passengerRideGate: document.querySelector("#passengerRideGate"), pickupArea: document.querySelector("#pickupArea"), pickupDescription: document.querySelector("#pickupDescription"), pickupUseCurrentLocation: document.querySelector("#pickupUseCurrentLocation"), pickupHistory: document.querySelector("#pickupHistory"), pickupSuggestions: document.querySelector("#pickupSuggestions"), pickupPlaceStatus: document.querySelector("#pickupPlaceStatus"), useCurrentPickup: document.querySelector("#useCurrentPickup"), capturePickupGps: document.querySelector("#capturePickupGps"), clearPickupGps: document.querySelector("#clearPickupGps"), pickupGpsStatus: document.querySelector("#pickupGpsStatus"), destinationArea: document.querySelector("#destinationArea"), destination: document.querySelector("#destination"), destinationHistory: document.querySelector("#destinationHistory"), destinationSuggestions: document.querySelector("#destinationSuggestions"), destinationPlaceStatus: document.querySelector("#destinationPlaceStatus"), rideRequestRoutePreview: document.querySelector("#rideRequestRoutePreview"), addRideStop: document.querySelector("#addRideStop"), clearRideStops: document.querySelector("#clearRideStops"), rideStopsPanel: document.querySelector("#rideStopsPanel"), rideStopsField: document.querySelector("#rideStopsField"), rideStops: document.querySelector("#rideStops"), rideStopSuggestions: document.querySelector("#rideStopSuggestions"), rideStopsStatus: document.querySelector("#rideStopsStatus"), rideBillingAccount: document.querySelector("#rideBillingAccount"), toggleRideTiming: document.querySelector("#toggleRideTiming"), rideTimingPanel: document.querySelector("#rideTimingPanel"), rideTiming: document.querySelector("#rideTiming"), scheduledAtField: document.querySelector("#scheduledAtField"), scheduledAt: document.querySelector("#scheduledAt"), passengerRiderAvailability: document.querySelector("#passengerRiderAvailability"), toggleVehiclePreference: document.querySelector("#toggleVehiclePreference"), vehiclePreferencePanel: document.querySelector("#vehiclePreferencePanel"), vehiclePreference: document.querySelector("#vehiclePreference"), toggleFareDetails: document.querySelector("#toggleFareDetails"), fareDetailsPanel: document.querySelector("#fareDetailsPanel"), passengerFareNegotiable: document.querySelector("#passengerFareNegotiable"), passengerFareMode: document.querySelector("#passengerFareMode"), passengerFareModeStatus: document.querySelector("#passengerFareModeStatus"), fareOffer: document.querySelector("#fareOffer"), fareGuidance: document.querySelector("#fareGuidance"), fareReviewPanel: document.querySelector("#fareReviewPanel"), paymentPreference: document.querySelector("#paymentPreference"), riderSignInForm: document.querySelector("#riderSignInForm"), riderSignInEmail: document.querySelector("#riderSignInEmail"), riderSignInPassword: document.querySelector("#riderSignInPassword"), riderSignInOtpPanel: document.querySelector("#riderSignInOtpPanel"), riderSignInPhone: document.querySelector("#riderSignInPhone"), riderSignInCode: document.querySelector("#riderSignInCode"), sendRiderSignInCode: document.querySelector("#sendRiderSignInCode"), verifyRiderSignIn: document.querySelector("#verifyRiderSignIn"), forgotRiderPassword: document.querySelector("#forgotRiderPassword"), riderPasswordResetPanel: document.querySelector("#riderPasswordResetPanel"), riderPasswordResetPhoneStep: document.querySelector("#riderPasswordResetPhoneStep"), riderPasswordResetPhoneHint: document.querySelector("#riderPasswordResetPhoneHint"), riderPasswordResetPhoneCode: document.querySelector("#riderPasswordResetPhoneCode"), sendRiderPasswordResetPhoneOtp: document.querySelector("#sendRiderPasswordResetPhoneOtp"), verifyRiderPasswordResetPhoneOtp: document.querySelector("#verifyRiderPasswordResetPhoneOtp"), riderPasswordResetPhoneStatus: document.querySelector("#riderPasswordResetPhoneStatus"), riderPasswordResetPasswordFields: document.querySelector("#riderPasswordResetPasswordFields"), riderResetPassword: document.querySelector("#riderResetPassword"), riderResetPasswordConfirm: document.querySelector("#riderResetPasswordConfirm"), saveRiderResetPassword: document.querySelector("#saveRiderResetPassword"), riderSignInStatus: document.querySelector("#riderSignInStatus"), riderAccountStage: document.querySelector("#riderAccountStage"), riderWorkspaceHeader: document.querySelector("#riderWorkspaceHeader"), riderWorkspaceTitle: document.querySelector("#riderWorkspaceTitle"), riderWorkspaceSummary: document.querySelector("#riderWorkspaceSummary"), riderWorkspaceMenu: document.querySelector("#riderWorkspaceMenu"), riderWorkspaceMenuToggle: document.querySelector("#riderWorkspaceMenuToggle"), riderWorkspaceNav: document.querySelector("#riderWorkspaceNav"), riderMenuSignOutTop: document.querySelector("#riderMenuSignOutTop"), riderOverviewGrid: document.querySelector("#riderOverviewGrid"), riderSessionCard: document.querySelector("#riderSessionCard"), riderProfileAvatar: document.querySelector("#riderProfileAvatar"), riderProfilePhotoStatus: document.querySelector("#riderProfilePhotoStatus"), riderProfileDetailList: document.querySelector("#riderProfileDetailList"), riderRatingsPanel: document.querySelector("#riderRatingsPanel"), riderNavigationPreference: document.querySelector("#riderNavigationPreference"), riderReferralPanel: document.querySelector("#riderReferralPanel"), riderReferralSummary: document.querySelector("#riderReferralSummary"), riderReferralCodeDisplay: document.querySelector("#riderReferralCodeDisplay"), copyRiderReferralCode: document.querySelector("#copyRiderReferralCode"), shareRiderReferralCode: document.querySelector("#shareRiderReferralCode"), emailRiderReferralCode: document.querySelector("#emailRiderReferralCode"), textRiderReferralCode: document.querySelector("#textRiderReferralCode"), riderReferralHowItWorks: document.querySelector("#riderReferralHowItWorks"), riderSessionTitle: document.querySelector("#riderSessionTitle"), riderSessionSummary: document.querySelector("#riderSessionSummary"), riderMenuSignOut: document.querySelector("#riderMenuSignOut"), riderPaymentForm: document.querySelector("#riderPaymentForm"), startRiderStripePayoutSetup: document.querySelector("#startRiderStripePayoutSetup"), riderPaymentProvider: document.querySelector("#riderPaymentProvider"), riderBankName: document.querySelector("#riderBankName"), riderAccountHolder: document.querySelector("#riderAccountHolder"), riderAccountLast4: document.querySelector("#riderAccountLast4"), riderPaymentReference: document.querySelector("#riderPaymentReference"), riderPaymentStatus: document.querySelector("#riderPaymentStatus"), riderLocationForm: document.querySelector("#riderLocationForm"), riderActiveCountry: document.querySelector("#riderActiveCountry"), riderActiveCity: document.querySelector("#riderActiveCity"), riderActiveArea: document.querySelector("#riderActiveArea"), riderDailyRegions: document.querySelector("#riderDailyRegions"), riderDestinationScope: document.querySelector("#riderDestinationScope"), captureRiderGps: document.querySelector("#captureRiderGps"), clearRiderGps: document.querySelector("#clearRiderGps"), riderGpsStatus: document.querySelector("#riderGpsStatus"), riderLocationStatus: document.querySelector("#riderLocationStatus"), riderDailyRegionStatus: document.querySelector("#riderDailyRegionStatus"), riderNoticePanel: document.querySelector("#riderNoticePanel"), riderNoticeList: document.querySelector("#riderNoticeList"), riderEnablePush: document.querySelector("#riderEnablePush"), riderPushStatus: document.querySelector("#riderPushStatus"), riderSupportForm: document.querySelector("#riderSupportForm"), riderSupportCategory: document.querySelector("#riderSupportCategory"), riderSupportSubject: document.querySelector("#riderSupportSubject"), riderSupportMessage: document.querySelector("#riderSupportMessage"), riderSupportStatus: document.querySelector("#riderSupportStatus"), riderFlowCard: document.querySelector("#riderFlowCard"), riderFlowTitle: document.querySelector("#riderFlowTitle"), riderFlowSummary: document.querySelector("#riderFlowSummary"), riderFlowSteps: document.querySelector("#riderFlowSteps"), riderFlowMeta: document.querySelector("#riderFlowMeta"), riderFlowActions: document.querySelector("#riderFlowActions"), riderSignOut: document.querySelector("#riderSignOut"), riderAccountForm: document.querySelector("#riderAccountForm"), riderName: document.querySelector("#riderName"), riderEmail: document.querySelector("#riderEmail"), riderPassword: document.querySelector("#riderPassword"), riderPhoto: document.querySelector("#riderPhoto"), riderPhone: document.querySelector("#riderPhone"), riderReferralCode: document.querySelector("#riderReferralCode"), riderVerificationCode: document.querySelector("#riderVerificationCode"), sendRiderCode: document.querySelector("#sendRiderCode"), verifyRiderPhone: document.querySelector("#verifyRiderPhone"), riderNationalId: document.querySelector("#riderNationalId"), riderDob: document.querySelector("#riderDob"), riderVehicle: document.querySelector("#riderVehicle"), riderCarMake: document.querySelector("#riderCarMake"), riderCarModel: document.querySelector("#riderCarModel"), riderCarBodyType: document.querySelector("#riderCarBodyType"), riderVehicleDesignation: document.querySelector("#riderVehicleDesignation"), riderCarYear: document.querySelector("#riderCarYear"), riderCarColor: document.querySelector("#riderCarColor"), riderCountry: document.querySelector("#riderCountry"), riderCity: document.querySelector("#riderCity"), riderArea: document.querySelector("#riderArea"), riderCredential: document.querySelector("#riderCredential"), riderLicenseExpiresOn: document.querySelector("#riderLicenseExpiresOn"), riderVehicleVin: document.querySelector("#riderVehicleVin"), riderRegistration: document.querySelector("#riderRegistration"), riderInsuranceProvider: document.querySelector("#riderInsuranceProvider"), riderInsuranceNumber: document.querySelector("#riderInsuranceNumber"), riderInsuranceExpiresOn: document.querySelector("#riderInsuranceExpiresOn"), riderBackgroundConsent: document.querySelector("#riderBackgroundConsent"), riderLicenseDocument: document.querySelector("#riderLicenseDocument"), riderRegistrationDocument: document.querySelector("#riderRegistrationDocument"), riderInsuranceDocument: document.querySelector("#riderInsuranceDocument"), riderInspectionDocument: document.querySelector("#riderInspectionDocument"), riderComplianceRenewalForm: document.querySelector("#riderComplianceRenewalForm"), riderRenewLicenseExpiresOn: document.querySelector("#riderRenewLicenseExpiresOn"), riderRenewLicenseDocument: document.querySelector("#riderRenewLicenseDocument"), riderRenewInsuranceExpiresOn: document.querySelector("#riderRenewInsuranceExpiresOn"), riderRenewInsuranceDocument: document.querySelector("#riderRenewInsuranceDocument"), riderSubmitComplianceRenewal: document.querySelector("#riderSubmitComplianceRenewal"), riderComplianceRenewalStatus: document.querySelector("#riderComplianceRenewalStatus"), riderStatus: document.querySelector("#riderStatus"), riderSubmitButton: document.querySelector("#riderSubmitButton"), riderBackgroundCheckPanel: document.querySelector("#riderBackgroundCheckPanel"), riderBackgroundCheckBadge: document.querySelector("#riderBackgroundCheckBadge"), riderBackgroundCheckSummary: document.querySelector("#riderBackgroundCheckSummary"), startRiderBackgroundCheck: document.querySelector("#startRiderBackgroundCheck"), riderBackgroundCheckStatus: document.querySelector("#riderBackgroundCheckStatus"), riderBackgroundCheckList: document.querySelector("#riderBackgroundCheckList"), riderTaxPanel: document.querySelector("#riderTaxPanel"), riderTaxOnboardingSummary: document.querySelector("#riderTaxOnboardingSummary"), startRiderTaxOnboarding: document.querySelector("#startRiderTaxOnboarding"), riderTaxOnboardingStatus: document.querySelector("#riderTaxOnboardingStatus"), riderTaxList: document.querySelector("#riderTaxList"), subscriptionText: document.querySelector("#subscriptionText"), subscriptionPlan: document.querySelector("#subscriptionPlan"), subscriptionRenewalMode: document.querySelector("#subscriptionRenewalMode"), subscriptionPaymentStatus: document.querySelector("#subscriptionPaymentStatus"), paySubscription: document.querySelector("#paySubscription"), riderEarningsPanel: document.querySelector("#riderEarningsPanel"), riderEarningsCount: document.querySelector("#riderEarningsCount"), riderEarningsSummary: document.querySelector("#riderEarningsSummary"), riderEarningsList: document.querySelector("#riderEarningsList"), offerForm: document.querySelector("#offerForm"), offerRequestContext: document.querySelector("#offerRequestContext"), counterFare: document.querySelector("#counterFare"), counterNote: document.querySelector("#counterNote"), acceptFare: document.querySelector("#acceptFare"), dropRiderNegotiation: document.querySelector("#dropRiderNegotiation"), selectedSummary: document.querySelector("#selectedSummary"), riderAppliedDestinationSummary: document.querySelector("#riderAppliedDestinationSummary"), marketPanel: document.querySelector("#marketPanel"), marketLocation: document.querySelector("#marketLocation"), openRiderDestinationFilter: document.querySelector("#openRiderDestinationFilter"), refreshMarket: document.querySelector("#refreshMarket"), marketFilters: document.querySelector("#marketFilters"), riderDestinationFilterPanel: document.querySelector("#riderDestinationFilterPanel"), riderDestinationFilterConsent: document.querySelector("#riderDestinationFilterConsent"), riderDestinationFilterCountry: document.querySelector("#riderDestinationFilterCountry"), riderDestinationFilterCity: document.querySelector("#riderDestinationFilterCity"), riderDestinationFilterArea: document.querySelector("#riderDestinationFilterArea"), riderDestinationFilterText: document.querySelector("#riderDestinationFilterText"), riderDestinationFilterApply: document.querySelector("#riderDestinationFilterApply"), riderDestinationFilterClear: document.querySelector("#riderDestinationFilterClear"), riderDestinationFilterStatus: document.querySelector("#riderDestinationFilterStatus"), cityMap: document.querySelector("#cityMap"), riderRequestDetailPanel: document.querySelector("#riderRequestDetailPanel"), riderRequestDetailTitle: document.querySelector("#riderRequestDetailTitle"), riderRequestDetailStatus: document.querySelector("#riderRequestDetailStatus"), riderMarketplaceBack: document.querySelector("#riderMarketplaceBack"), boardGrid: document.querySelector("#boardGrid"), requestsBoard: document.querySelector("#requestsBoard"), requestBoardTitle: document.querySelector("#requestBoardTitle"), requestList: document.querySelector("#requestList"), offersBoard: document.querySelector("#offersBoard"), offerBoardTitle: document.querySelector("#offerBoardTitle"), offerList: document.querySelector("#offerList"), adminIssuePagination: document.querySelector("#adminIssuePagination"), adminIssuePrev: document.querySelector("#adminIssuePrev"), adminIssuePage: document.querySelector("#adminIssuePage"), adminIssueNext: document.querySelector("#adminIssueNext"), requestCount: document.querySelector("#requestCount"), offerCount: document.querySelector("#offerCount"), gpsWriteMetric: document.querySelector("#gpsWriteMetric"), googleCallMetric: document.querySelector("#googleCallMetric"), routeCacheMetric: document.querySelector("#routeCacheMetric"), slowRpcMetric: document.querySelector("#slowRpcMetric"), dbStorageMetric: document.querySelector("#dbStorageMetric"), fileStorageMetric: document.querySelector("#fileStorageMetric"), noisyRowMetric: document.querySelector("#noisyRowMetric"), retentionRowMetric: document.querySelector("#retentionRowMetric"), adminPageTitle: document.querySelector("#adminPageTitle"), adminPageDescription: document.querySelector("#adminPageDescription"), adminPageStep: document.querySelector("#adminPageStep"), adminPrevPage: document.querySelector("#adminPrevPage"), adminNextPage: document.querySelector("#adminNextPage"), adminMetricGrid: document.querySelector("#adminMetricGrid"), adminPageDirectory: document.querySelector("#adminPageDirectory"), adminWorkspacePageList: document.querySelector("#adminWorkspacePageList"), adminPageDirectoryCount: document.querySelector("#adminPageDirectoryCount"), adminActivityPanel: document.querySelector("#adminActivityPanel"), adminActivityList: document.querySelector("#adminActivityList"), adminActivityCount: document.querySelector("#adminActivityCount"), adminActivityPagination: document.querySelector("#adminActivityPagination"), adminActivityPrev: document.querySelector("#adminActivityPrev"), adminActivityPage: document.querySelector("#adminActivityPage"), adminActivityNext: document.querySelector("#adminActivityNext"), adminDirectoryTools: document.querySelector("#adminDirectoryTools"), adminCompliancePagination: document.querySelector("#adminCompliancePagination"), adminCompliancePrev: document.querySelector("#adminCompliancePrev"), adminCompliancePage: document.querySelector("#adminCompliancePage"), adminComplianceNext: document.querySelector("#adminComplianceNext"), adminFinanceSummaryPagination: document.querySelector("#adminFinanceSummaryPagination"), adminFinanceSummaryPrev: document.querySelector("#adminFinanceSummaryPrev"), adminFinanceSummaryPage: document.querySelector("#adminFinanceSummaryPage"), adminFinanceSummaryNext: document.querySelector("#adminFinanceSummaryNext"), adminPaymentPagination: document.querySelector("#adminPaymentPagination"), adminPaymentPrev: document.querySelector("#adminPaymentPrev"), adminPaymentPage: document.querySelector("#adminPaymentPage"), adminPaymentNext: document.querySelector("#adminPaymentNext"), adminControlsPagination: document.querySelector("#adminControlsPagination"), adminControlsPrev: document.querySelector("#adminControlsPrev"), adminControlsPage: document.querySelector("#adminControlsPage"), adminControlsNext: document.querySelector("#adminControlsNext"), adminGeographyPagination: document.querySelector("#adminGeographyPagination"), adminGeographyPrev: document.querySelector("#adminGeographyPrev"), adminGeographyPage: document.querySelector("#adminGeographyPage"), adminGeographyNext: document.querySelector("#adminGeographyNext"), adminReportsPagination: document.querySelector("#adminReportsPagination"), adminReportsPrev: document.querySelector("#adminReportsPrev"), adminReportsPage: document.querySelector("#adminReportsPage"), adminReportsNext: document.querySelector("#adminReportsNext"), adminAuditPagination: document.querySelector("#adminAuditPagination"), adminAuditPrev: document.querySelector("#adminAuditPrev"), adminAuditPage: document.querySelector("#adminAuditPage"), adminAuditNext: document.querySelector("#adminAuditNext"), adminStatus: inertElement("adminStatus"), seedDemo: inertElement("seedDemo"), adminDirectorySearchButton: document.querySelector("#adminDirectorySearchButton"), adminDirectoryClearSearch: document.querySelector("#adminDirectoryClearSearch"), clearDemo: inertElement("clearDemo"), chatPanel: document.querySelector("#chatPanel"), chatStatus: document.querySelector("#chatStatus"), rideActionPanel: document.querySelector("#rideActionPanel"), chatThread: document.querySelector("#chatThread"), chatForm: document.querySelector("#chatForm"), chatInput: document.querySelector("#chatInput"), safetyReportForm: document.querySelector("#safetyReportForm"), safetyReportCategory: document.querySelector("#safetyReportCategory"), safetyReportSeverity: document.querySelector("#safetyReportSeverity"), safetyReportDetails: document.querySelector("#safetyReportDetails"), safetyReportStatus: document.querySelector("#safetyReportStatus"), rideRatingForm: document.querySelector("#rideRatingForm"), rideRatingScore: document.querySelector("#rideRatingScore"), rideRatingSafety: document.querySelector("#rideRatingSafety"), rideRatingPunctuality: document.querySelector("#rideRatingPunctuality"), rideRatingCommunication: document.querySelector("#rideRatingCommunication"), rideRatingVehicle: document.querySelector("#rideRatingVehicle"), rideRatingComment: document.querySelector("#rideRatingComment"), rideRatingStatus: document.querySelector("#rideRatingStatus"), requestTemplate: document.querySelector("#requestTemplate"), offerTemplate: document.querySelector("#offerTemplate"), reviewTemplate: document.querySelector("#reviewTemplate") }; function adminShellAvailable() { return false; } function availableWorkspaceTab(tab) { if (!workspaceTabs.includes(tab)) return null; if (!runtimeAllowsWorkspaceTab(tab)) return null; if (tab === "admin" && !adminShellAvailable()) return null; return tab; } function populateSelect(select, values, selectedValue) { if (!select) return; select.innerHTML = ""; values.forEach((value) => { const option = document.createElement("option"); option.value = value; option.textContent = value; option.selected = value === selectedValue; select.append(option); }); } function populateMultiSelect(select, values, selectedValues = []) { if (!select) return; const selected = new Set(selectedValues); select.innerHTML = ""; values.forEach((value) => { const option = document.createElement("option"); option.value = value; option.textContent = value; option.selected = selected.has(value); select.append(option); }); } function selectedMultiValues(select) { return [...(select?.selectedOptions ?? [])].map((option) => option.value).filter(Boolean); } function populateSelectOptions(select, options, selectedValue) { if (!select) return; select.innerHTML = ""; options.forEach((item) => { const option = document.createElement("option"); option.value = item.value; option.textContent = item.label; option.selected = item.value === selectedValue; select.append(option); }); } function daysFromNow(days) { const date = new Date(); date.setDate(date.getDate() + days); return date.toISOString(); } function daysAgo(days) { return daysFromNow(-days); } function defaultLaunchCountry() { return countryCities[appConfig.firstLaunchCountry] ? appConfig.firstLaunchCountry : "United States"; } function defaultLaunchCity(country = defaultLaunchCountry()) { return cityNames(country).includes(appConfig.firstLaunchCity) ? appConfig.firstLaunchCity : cityNames(country)[0]; } function moneyCurrencyForCountry(country = defaultLaunchCountry()) { return africanRidePaymentCountries.has(country) ? "XAF" : "USD"; } function minimumFareOffer(country = defaultLaunchCountry()) { return moneyCurrencyForCountry(country) === "USD" ? 1 : 100; } const passengerFareModeOverrideStorageKey = "waka-passenger-fare-mode-choice-v1"; const riderNavigationPreferenceChoiceStorageKey = "waka-rider-navigation-preference-choice-v1"; function readLocalJsonRecord(key) { try { const raw = localStorage.getItem(key); if (!raw) return null; const parsed = JSON.parse(raw); return parsed && typeof parsed === "object" ? parsed : null; } catch { return null; } } function writeLocalJsonRecord(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); } catch { // Ignore storage failures; in-memory state still carries the current choice. } } function normalizePassengerFareMode(value) { const normalized = String(value || "negotiable").trim().toLowerCase().replace(/[\s-]+/g, "_"); return normalized === "non_negotiable" ? "non_negotiable" : "negotiable"; } function rememberPassengerFareModeChoice(mode) { writeLocalJsonRecord(passengerFareModeOverrideStorageKey, { value: normalizePassengerFareMode(mode), updatedAt: new Date().toISOString() }); } function storedPassengerFareModeChoice() { const stored = readLocalJsonRecord(passengerFareModeOverrideStorageKey); return stored?.value ? normalizePassengerFareMode(stored.value) : null; } function syncPassengerFareModeInputs(mode, { userSelected = false } = {}) { const normalized = normalizePassengerFareMode(mode); if (els.passengerFareMode) { els.passengerFareMode.value = normalized; els.passengerFareMode.dataset.lastSyncedFareMode = normalized; if (userSelected) els.passengerFareMode.dataset.userSelectedFareMode = normalized; } if (els.passengerFareNegotiable instanceof HTMLInputElement) { els.passengerFareNegotiable.checked = normalized !== "non_negotiable"; els.passengerFareNegotiable.setAttribute("aria-checked", normalized !== "non_negotiable" ? "true" : "false"); els.passengerFareNegotiable.dataset.lastSyncedFareMode = normalized; if (userSelected) { els.passengerFareNegotiable.dataset.userSelectedFareMode = normalized; } } if (userSelected) rememberPassengerFareModeChoice(normalized); return normalized; } function passengerFareMode() { const selectedMode = els.passengerFareMode?.dataset?.userSelectedFareMode ?? els.passengerFareNegotiable?.dataset?.userSelectedFareMode; if (selectedMode) return normalizePassengerFareMode(selectedMode); if (els.passengerFareMode instanceof HTMLSelectElement) { const currentMode = normalizePassengerFareMode(els.passengerFareMode.value); const lastSyncedMode = els.passengerFareMode.dataset.lastSyncedFareMode; if (lastSyncedMode && currentMode !== normalizePassengerFareMode(lastSyncedMode)) { els.passengerFareMode.dataset.userSelectedFareMode = currentMode; state.passengerFareMode = currentMode; rememberPassengerFareModeChoice(currentMode); return currentMode; } } if (els.passengerFareNegotiable instanceof HTMLInputElement) { const currentMode = els.passengerFareNegotiable.checked ? "negotiable" : "non_negotiable"; const lastSyncedMode = els.passengerFareNegotiable.dataset.lastSyncedFareMode; if (lastSyncedMode && currentMode !== normalizePassengerFareMode(lastSyncedMode)) { els.passengerFareNegotiable.dataset.userSelectedFareMode = currentMode; state.passengerFareMode = currentMode; rememberPassengerFareModeChoice(currentMode); return currentMode; } } const storedMode = storedPassengerFareModeChoice(); if (storedMode) return storedMode; return normalizePassengerFareMode( state.passengerFareMode ?? els.passengerFareMode?.value ?? (els.passengerFareNegotiable?.checked === false ? "non_negotiable" : "negotiable") ); } function requestFareMode(request) { return normalizePassengerFareMode(request?.fareMode ?? request?.fare_mode); } function requestIsNonNegotiableFare(request) { return requestFareMode(request) === "non_negotiable"; } function requestIsNegotiableFare(request) { return !requestIsNonNegotiableFare(request); } function fareModeChipText(request) { return requestIsNonNegotiableFare(request) ? "Non-negotiable fare" : "Negotiable fare"; } function formatMoney(amount, country = defaultLaunchCountry()) { const value = Number(amount) || 0; const currency = moneyCurrencyForCountry(country); if (currency === "USD") { return new Intl.NumberFormat("en-US", { style: "currency", currency, maximumFractionDigits: 0 }).format(value); } return `${value.toLocaleString("en-US")} XAF`; } function normalizeCarBodyType(value) { const normalized = String(value ?? "").trim().toLowerCase(); const allowed = ["sedan", "suv", "hatchback", "minivan", "wagon", "pickup", "coupe", "convertible", "luxury"]; return allowed.includes(normalized) ? normalized : "sedan"; } function carBodyTypeLabel(value) { const normalized = normalizeCarBodyType(value); const labels = { sedan: "Sedan", suv: "SUV", hatchback: "Hatchback", minivan: "Minivan", wagon: "Wagon", pickup: "Pickup", coupe: "Coupe", convertible: "Convertible", luxury: "Luxury" }; return labels[normalized] ?? "Sedan"; } function carBodyTypeAllowsXlSpecial(value) { return ["suv", "minivan", "wagon", "pickup", "luxury"].includes(normalizeCarBodyType(value)); } function normalizeCarTypePreference(value) { const normalized = String(value ?? "").trim().toLowerCase(); if (["normal", "sedan"].includes(normalized)) return "sedan"; if (["xl_special", "xl/special", "xl-special", "suv"].includes(normalized)) return "suv"; return "sedan"; } function carTypePreferenceLabel(value) { const normalized = normalizeCarTypePreference(value); return normalized === "suv" ? "XL/Special" : "Normal"; } function passengerMinimumFareFromGuidance(guidance, vehicleDesignation = els.vehiclePreference?.value) { if (!guidance) return null; const minimum = normalizeCarTypePreference(vehicleDesignation) === "suv" ? Number(guidance.max) + 1 : Number(guidance.min); return Number.isFinite(minimum) && minimum > 0 ? Math.ceil(minimum) : null; } function normalizeRiderVehicleDesignation(value, bodyType = null) { const normalized = String(value ?? "").trim().toLowerCase().replace(/[/-]/g, "_"); const desired = ["normal", "xl_special", "both"].includes(normalized) ? normalized : "normal"; if (bodyType && !carBodyTypeAllowsXlSpecial(bodyType) && desired !== "normal") return "normal"; return desired; } function riderVehicleDesignationLabel(value) { const normalized = normalizeRiderVehicleDesignation(value); if (normalized === "xl_special") return "XL/Special"; if (normalized === "both") return "Normal and XL/Special"; return "Normal"; } function riderCanServeCarTypePreference(rider, preference) { const normalizedPreference = normalizeCarTypePreference(preference); const designation = normalizeRiderVehicleDesignation(rider?.vehicleDesignation, rider?.carBodyType); if (normalizedPreference === "sedan") return ["normal", "both"].includes(designation); if (normalizedPreference === "suv") return carBodyTypeAllowsXlSpecial(rider?.carBodyType) && ["xl_special", "both"].includes(designation); return true; } function normalizeRiderNavigationPreference(value) { const normalized = String(value || "google_maps") .trim() .toLowerCase() .replace(/[\s-]+/g, "_"); return normalized === "waze" ? "waze" : "google_maps"; } function riderNavigationPreferenceUiOverride() { if (!(els.riderNavigationPreference instanceof HTMLSelectElement)) return null; const currentPreference = normalizeRiderNavigationPreference(els.riderNavigationPreference.value); const selected = els.riderNavigationPreference.dataset.userSelectedNavigationPreference; if (selected) { const selectedPreference = normalizeRiderNavigationPreference(selected); if (selectedPreference === currentPreference) return selectedPreference; els.riderNavigationPreference.dataset.userSelectedNavigationPreference = currentPreference; rememberRiderNavigationPreferenceOverride(currentPreference); return currentPreference; } const lastSyncedPreference = els.riderNavigationPreference.dataset.lastSyncedNavigationPreference; if (lastSyncedPreference && currentPreference !== normalizeRiderNavigationPreference(lastSyncedPreference)) { els.riderNavigationPreference.dataset.userSelectedNavigationPreference = currentPreference; rememberRiderNavigationPreferenceOverride(currentPreference); return currentPreference; } return null; } function syncRiderNavigationPreferenceInput(preference, { userSelected = false } = {}) { const normalized = normalizeRiderNavigationPreference(preference); if (!els.riderNavigationPreference) return normalized; els.riderNavigationPreference.value = normalized; els.riderNavigationPreference.dataset.lastSyncedNavigationPreference = normalized; if (userSelected) { els.riderNavigationPreference.dataset.userSelectedNavigationPreference = normalized; rememberStoredRiderNavigationPreferenceChoice(normalized); } return normalized; } function riderIdentityAliasesForRecord(rider) { const aliases = []; [rider?.id, rider?.supabaseUserId, rider?.riderId, rider?.userId, rider?.accountId, rider?.profileId].forEach((value) => { const normalized = String(value ?? "").trim(); if (normalized && !aliases.includes(normalized)) aliases.push(normalized); }); return aliases; } function currentRiderIdentityAliases() { const aliases = []; const add = (value) => { const normalized = String(value ?? "").trim(); if (normalized && !aliases.includes(normalized)) aliases.push(normalized); }; riderIdentityAliasesForRecord(state.rider).forEach(add); add(state.sessions?.rider?.userId); const current = currentRiderRecord(); riderIdentityAliasesForRecord(current).forEach(add); return aliases; } function riderRecordMatchesIdentityAliases(rider, aliases = []) { const lookup = aliases .map((value) => String(value ?? "").trim()) .filter(Boolean); if (!lookup.length) return false; return riderIdentityAliasesForRecord(rider).some((id) => lookup.includes(id)); } function riderNavigationPreference(rider = currentRiderRecord()) { const explicitRider = arguments.length > 0; const uiOverride = riderNavigationPreferenceUiOverride(); if (uiOverride && (!explicitRider || !state.rider || rider === state.rider || riderRecordMatchesIdentityAliases(rider, currentRiderIdentityAliases()))) { return uiOverride; } const riderIds = riderIdentityAliasesForRecord(rider); let override = riderIds.length ? riderNavigationPreferenceOverrideForRider(riderIds) : null; if (!override && !explicitRider) override = riderNavigationPreferenceOverrideForRider(); if (!override && explicitRider && riderRecordMatchesIdentityAliases(rider, currentRiderIdentityAliases())) { override = riderNavigationPreferenceOverrideForRider(); } if (override) return override; const documents = riderDocuments(rider); return normalizeRiderNavigationPreference(documents.navigationPreference ?? rider?.navigationPreference); } function riderNavigationPreferenceOverrideForRider(riderIdOrAliases) { const storedOverride = storedRiderNavigationPreferenceChoice(); const explicitRider = arguments.length > 0; const lookupIds = (explicitRider ? (Array.isArray(riderIdOrAliases) ? riderIdOrAliases : [riderIdOrAliases]) : currentRiderIdentityAliases()) .map((value) => String(value ?? "").trim()) .filter(Boolean); return riderNavigationPreferenceOverrideValue(storedOverride, lookupIds) ?? riderNavigationPreferenceOverrideValue(state.riderNavigationPreferenceOverride, lookupIds); } function riderNavigationPreferenceOverrideValue(override, lookupIds = []) { if (!override?.value) return null; const overrideIds = [ override.riderId, ...(Array.isArray(override.riderIds) ? override.riderIds : []) ] .map((value) => String(value ?? "").trim()) .filter(Boolean); if (overrideIds.length && !lookupIds.some((id) => overrideIds.includes(id))) return null; return normalizeRiderNavigationPreference(override.value); } function storedRiderNavigationPreferenceChoice() { const stored = readLocalJsonRecord(riderNavigationPreferenceChoiceStorageKey); if (!stored?.value) return null; return { riderId: String(stored.riderId || "").trim(), riderIds: Array.isArray(stored.riderIds) ? stored.riderIds.map((value) => String(value ?? "").trim()).filter(Boolean) : [], value: normalizeRiderNavigationPreference(stored.value), updatedAt: stored.updatedAt || null }; } function rememberStoredRiderNavigationPreferenceChoice(preference, riderId = currentRiderRecord()?.id) { const riderIds = [ riderId, ...currentRiderIdentityAliases() ] .map((value) => String(value ?? "").trim()) .filter(Boolean) .filter((value, index, values) => values.indexOf(value) === index) .slice(-8); writeLocalJsonRecord(riderNavigationPreferenceChoiceStorageKey, { riderId: riderIds[0] || "", riderIds, value: normalizeRiderNavigationPreference(preference), updatedAt: new Date().toISOString() }); } function rememberRiderNavigationPreferenceOverride(preference, riderId = currentRiderRecord()?.id) { const riderIds = [ riderId, ...currentRiderIdentityAliases() ] .map((value) => String(value ?? "").trim()) .filter(Boolean) .filter((value, index, values) => values.indexOf(value) === index); state.riderNavigationPreferenceOverride = { riderId: riderIds[0] || "", riderIds, value: normalizeRiderNavigationPreference(preference), updatedAt: new Date().toISOString() }; rememberStoredRiderNavigationPreferenceChoice(preference, riderId); } function normalizeRideStops(value) { const rawStops = Array.isArray(value) ? value : String(value ?? "").split(/\r?\n|;/); return rawStops .map((stop) => String(stop ?? "").replace(/\s+/g, " ").trim()) .filter(Boolean) .slice(0, rideStopsMaxCount) .map((stop) => stop.slice(0, rideStopMaxLength)); } function rawRideStopPoints(value) { if (Array.isArray(value)) return value; if (typeof value !== "string") return []; try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed : []; } catch { return []; } } function normalizeRideStopPoint(point, fallbackLabel = "") { const label = String(point?.label ?? fallbackLabel ?? "").replace(/\s+/g, " ").trim().slice(0, rideStopMaxLength); const latitude = Number(point?.latitude ?? point?.lat); const longitude = Number(point?.longitude ?? point?.lng); if (typeof validGpsCoordinate !== "function" || !validGpsCoordinate(latitude, longitude)) return null; return { label, latitude, longitude }; } function rideStopPointKey(label) { return typeof stopPlaceKey === "function" ? stopPlaceKey(label) : String(label ?? "").replace(/\s+/g, " ").trim().toLowerCase(); } function normalizeRideStopPoints(value, stops = []) { const normalizedStops = normalizeRideStops(stops); const points = rawRideStopPoints(value); return normalizedStops.map((stop, index) => normalizeRideStopPoint(points[index], stop)); } function rideStopPointsForRoute(stops, existingPoints = []) { const normalizedStops = normalizeRideStops(stops); const existingByLabel = new Map(); rawRideStopPoints(existingPoints).forEach((point) => { const normalized = normalizeRideStopPoint(point); const key = normalized ? rideStopPointKey(normalized.label) : ""; if (key) existingByLabel.set(key, normalized); }); return normalizedStops.map((stop) => { const existing = existingByLabel.get(rideStopPointKey(stop)); if (existing) return { ...existing, label: stop }; const place = typeof stopPlaceForRoute === "function" ? stopPlaceForRoute(stop) : null; return normalizeRideStopPoint(place, stop); }); } function rideStopPointsComplete(stops, points) { const normalizedStops = normalizeRideStops(stops); if (!normalizedStops.length) return true; const normalizedPoints = normalizeRideStopPoints(points, normalizedStops); return normalizedPoints.length === normalizedStops.length && normalizedPoints.every(Boolean); } function rideStopPointAt(request, index = 0) { return normalizeRideStopPoints(request?.rideStopPoints, request?.rideStops)[index] ?? null; } function rideStopsInputEnabled() { return Boolean(els.rideStops && !els.rideStops.disabled && els.rideStops.dataset.enabled === "true"); } function rideStopsFormValue() { return rideStopsInputEnabled() ? els.rideStops.value : ""; } function rideStopsInputValue(stops) { return normalizeRideStops(stops).join("\n"); } function rideStopsSummary(stops) { const normalized = normalizeRideStops(stops); if (!normalized.length) return "No added stops"; return `${normalized.length} stop${normalized.length === 1 ? "" : "s"}: ${normalized.join("; ")}`; } function formatDate(value) { if (!value) return "Not set"; return new Intl.DateTimeFormat("en", { month: "short", day: "numeric", year: "numeric" }).format(new Date(value)); } function parseDateOnly(value) { const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(value ?? "").trim()); if (!match) return null; const year = Number(match[1]); const month = Number(match[2]); const day = Number(match[3]); const date = new Date(year, month - 1, day); if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) return null; return date; } function daysUntilDate(value) { const date = parseDateOnly(value); if (!date) return null; const today = new Date(); const todayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate()); return Math.ceil((date.getTime() - todayStart.getTime()) / (24 * 60 * 60 * 1000)); } function riderComplianceItems(rider) { return [ { key: "driverLicense", label: "Driver's license", expiresOn: rider?.driverLicenseExpiresOn ?? "" }, { key: "insurance", label: "Insurance", expiresOn: rider?.insuranceExpiresOn ?? "" } ]; } function riderExpiredComplianceItems(rider) { return riderComplianceItems(rider).filter((item) => { const days = daysUntilDate(item.expiresOn); return days === null || days < 0; }); } function riderUpcomingComplianceItems(rider, noticeDays = 30) { return riderComplianceItems(rider).filter((item) => { const days = daysUntilDate(item.expiresOn); return days !== null && days >= 0 && days <= noticeDays; }); } function riderComplianceReady(rider) { return riderExpiredComplianceItems(rider).length === 0; } function riderComplianceStatusText(rider) { const expired = riderExpiredComplianceItems(rider); if (expired.length) { return `Rider service is blocked until current ${expired.map((item) => item.label.toLowerCase()).join(" and ")} details are reviewed.`; } const upcoming = riderUpcomingComplianceItems(rider); if (upcoming.length) { return `Renewal reminder: ${upcoming.map((item) => `${item.label} expires ${formatDate(item.expiresOn)}`).join("; ")}.`; } return "License and insurance dates are current."; } function maskProviderReference(value) { const text = String(value ?? "").trim(); if (!text) return ""; if (text.length <= 10) return text; return `${text.slice(0, 7)}...${text.slice(-4)}`; } function formatDateOfBirthInput(value) { const digits = String(value ?? "").replace(/\D/g, "").slice(0, 8); const parts = [ digits.slice(0, 4), digits.slice(4, 6), digits.slice(6, 8) ].filter(Boolean); return parts.join("-"); } function validDateOfBirth(value) { const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value ?? "").trim()); if (!match) return false; const year = Number(match[1]); const month = Number(match[2]); const day = Number(match[3]); if (year < 1900) return false; const parsed = new Date(Date.UTC(year, month - 1, day)); const now = new Date(); const today = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())); return parsed.getUTCFullYear() === year && parsed.getUTCMonth() === month - 1 && parsed.getUTCDate() === day && parsed <= today; } function normalizeDateOfBirthInput(input) { if (!input) return ""; input.value = formatDateOfBirthInput(input.value); return input.value; } function formatYearMonthInput(value) { const digits = String(value ?? "").replace(/\D/g, "").slice(0, 6); const parts = [digits.slice(0, 4), digits.slice(4, 6)].filter(Boolean); return parts.join("-"); } function validYearMonthOfBirth(value) { const match = /^(\d{4})-(\d{2})$/.exec(String(value ?? "").trim()); if (!match) return false; const year = Number(match[1]); const month = Number(match[2]); if (year < 1900 || month < 1 || month > 12) return false; const now = new Date(); const parsed = new Date(Date.UTC(year, month - 1, 1)); const thisMonth = new Date(Date.UTC(now.getFullYear(), now.getMonth(), 1)); return parsed <= thisMonth; } function normalizeYearMonthOfBirthInput(input) { if (!input) return ""; input.value = formatYearMonthInput(input.value); return input.value; } function yearMonthToStoredDate(value) { return validYearMonthOfBirth(value) ? `${value}-01` : ""; } function storedDateToYearMonth(value) { return String(value ?? "").slice(0, 7); } function wireYearMonthInput(input) { if (!input) return; input.addEventListener("input", () => { const cursorWasAtEnd = input.selectionStart === input.value.length; input.value = formatYearMonthInput(input.value); if (cursorWasAtEnd && typeof input.setSelectionRange === "function") { input.setSelectionRange(input.value.length, input.value.length); } }); input.addEventListener("blur", () => { input.value = formatYearMonthInput(input.value); }); } function wireDateOfBirthInput(input) { if (!input) return; input.addEventListener("input", () => { const cursorWasAtEnd = input.selectionStart === input.value.length; input.value = formatDateOfBirthInput(input.value); if (cursorWasAtEnd && typeof input.setSelectionRange === "function") { input.setSelectionRange(input.value.length, input.value.length); } }); input.addEventListener("blur", () => { input.value = formatDateOfBirthInput(input.value); }); } function formFieldLabel(field) { const label = field.closest("label"); const explicitLabel = label?.querySelector("span")?.textContent || label?.textContent || ""; return explicitLabel .replace(/\s+/g, " ") .trim() .replace(/(Send code|Verify|Submit|Save).*$/i, "") .trim() || field.placeholder || field.id || "field"; } function invalidAccountFields(form) { return [...form.querySelectorAll("input, select, textarea")] .filter((field) => !field.disabled && field.willValidate && !field.checkValidity()); } function summarizeInvalidFields(fields) { const labels = fields.slice(0, 4).map(formFieldLabel); if (fields.length > labels.length) labels.push(`${fields.length - labels.length} more`); return labels.join(", "); } function validateAccountForm(form, statusNode) { const invalidFields = invalidAccountFields(form); if (!invalidFields.length) return true; setTranslatedStatus(statusNode, "accountMissingFields", { fields: summarizeInvalidFields(invalidFields) }); invalidFields[0].focus({ preventScroll: false }); return false; } function pendingProfileRecoveryForRole(type) { const recovery = storageSafePendingProfileRecovery(state.pendingProfileRecovery); return recovery?.role === type ? recovery : null; } function setPendingProfileRecovery(type, user, email) { const metadata = user?.user_metadata ?? {}; state.pendingProfileRecovery = storageSafePendingProfileRecovery({ role: type, userId: user?.id ?? null, email: email || authUserEmail(user), phone: user?.phone ?? metadata.phone ?? "", name: metadata.full_name ?? metadata.name ?? "", startedAt: new Date().toISOString() }); return state.pendingProfileRecovery; } function clearPendingProfileRecovery(type = null) { if (!state.pendingProfileRecovery) return; if (type && state.pendingProfileRecovery.role !== type) return; state.pendingProfileRecovery = null; } function updateAccountPhoneVerificationControls() { const relaxed = smsVerificationRelaxedForTesting(); [ els.passengerVerificationCode?.closest(".verification-row"), els.riderVerificationCode?.closest(".verification-row") ].filter(Boolean).forEach((row) => { row.hidden = relaxed; }); } function setButtonBusy(button, busy) { if (!button) return; button.disabled = busy; button.setAttribute("aria-busy", String(busy)); } function formatDateTime(value) { if (!value) return "Not scheduled"; return new Intl.DateTimeFormat("en", { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" }).format(new Date(value)); } function countryNames() { const first = defaultLaunchCountry(); return [ first, ...Object.keys(countryCities).filter((country) => country !== first) ]; } function cityNames(country = defaultLaunchCountry()) { return Object.keys(countries[country] ?? {}); } function locationSubdivisionLabel(country = defaultLaunchCountry()) { return country === "United States" ? "state" : "city"; } function areas(country = defaultLaunchCountry(), city = defaultLaunchCity(country)) { return countries[country]?.[city] ?? []; } function findArea(country, city, name) { return areas(country, city).find((area) => area.name === name) ?? areas(country, city)[0]; } function areaDistanceUnits(firstArea, secondArea) { if (!firstArea || !secondArea) return null; return Math.hypot(firstArea.x - secondArea.x, firstArea.y - secondArea.y); } function citySpanKm(country, city) { return cityDistanceSpanKm[country]?.[city] ?? defaultCitySpanKm; } function estimatedAreaDistanceKm(country, city, firstArea, secondArea) { const distance = areaDistanceUnits(firstArea, secondArea); if (distance == null) return null; return (distance / 100) * citySpanKm(country, city); } function formatDistanceKm(value) { if (value == null) return "distance not estimated"; if (value < 0.2) return "same pickup area"; if (value < 1) return `${Math.round(value * 1000)} m away`; return `${value.toFixed(value < 10 ? 1 : 0)} km away`; } function formatDistanceMiles(value) { if (value == null || !Number.isFinite(Number(value))) return "distance not estimated"; if (value < 0.2) return "same pickup area"; if (value < 1) return `${Math.round(value * 5280)} ft away`; return `${value.toFixed(value < 10 ? 1 : 0)} mi away`; } function pickupEtaMinutes(distanceKm, rider = currentRiderRecord()) { if (distanceKm == null || !Number.isFinite(Number(distanceKm))) return null; const speedKmh = riderPickupEtaSpeedKmh[rider?.vehicle] ?? riderPickupEtaSpeedKmh.car; return Math.max(2, Math.ceil((Number(distanceKm) * riderPickupEtaRoadFactor * 60) / speedKmh)); } function formatPickupEta(minutes) { if (minutes == null) return "pickup ETA not estimated"; if (minutes < 60) return `about ${minutes} min pickup`; const hours = Math.floor(minutes / 60); const remainder = minutes % 60; return remainder ? `about ${hours} hr ${remainder} min pickup` : `about ${hours} hr pickup`; } function populateLocationFields() { const countriesList = countryNames(); const passengerCountry = countriesList.includes(state.passenger?.country) ? state.passenger.country : defaultLaunchCountry(); populateSelect(els.passengerCountry, countriesList, passengerCountry); populateSelect(els.passengerActiveCountry, countriesList, passengerCountry); const passengerCity = state.passenger?.city ?? cityNames(passengerCountry)[0]; populateSelect(els.passengerCity, cityNames(passengerCountry), passengerCity); populateSelect(els.passengerActiveCity, cityNames(passengerCountry), passengerCity); populateSelect(els.pickupArea, areas(passengerCountry, passengerCity).map((area) => area.name), areas(passengerCountry, passengerCity)[0]?.name); populateSelect(els.destinationArea, areas(passengerCountry, passengerCity).map((area) => area.name), areas(passengerCountry, passengerCity)[1]?.name ?? areas(passengerCountry, passengerCity)[0]?.name); populateSelectOptions(els.vehiclePreference, carTypePreferenceOptions, normalizeCarTypePreference(els.vehiclePreference?.value)); updateRidePaymentOptions(passengerCountry); updateFareGuidance(); const riderCountry = countriesList.includes(state.rider?.country) ? state.rider.country : passengerCountry; const riderCity = state.rider?.city ?? cityNames(riderCountry)[0]; populateSelect(els.riderCountry, countriesList, riderCountry); populateSelect(els.riderCity, cityNames(riderCountry), riderCity); populateSelect(els.riderArea, areas(riderCountry, riderCity).map((area) => area.name), state.rider?.area ?? areas(riderCountry, riderCity)[0]?.name); populateSelect(els.riderActiveCountry, countriesList, riderCountry); populateSelect(els.riderActiveCity, cityNames(riderCountry), riderCity); populateSelect(els.riderActiveArea, areas(riderCountry, riderCity).map((area) => area.name), state.rider?.area ?? areas(riderCountry, riderCity)[0]?.name); populateRiderDailyRegionOptions(riderCountry, riderCity); populateVehicleCatalogFields(state.rider); } function hydrateForms() { if (els.languageSelect) els.languageSelect.value = state.language; els.languageSelects?.forEach((select) => { select.value = state.language; }); updateAccountPhoneVerificationControls(); updatePassengerInitialBusinessFields(); const invitedReferralCode = new URLSearchParams(window.location.search).get("ref") || new URLSearchParams(String(window.location.hash).split("?")[1] || "").get("ref") || ""; if (invitedReferralCode && !state.passenger && els.passengerReferralCode && !els.passengerReferralCode.value) { els.passengerReferralCode.value = invitedReferralCode; } if (invitedReferralCode && !state.rider && els.riderReferralCode && !els.riderReferralCode.value) { els.riderReferralCode.value = invitedReferralCode; } if (invitedReferralCode && els.businessReferralCode && !els.businessReferralCode.value) { els.businessReferralCode.value = invitedReferralCode; } if (invitedReferralCode && els.passengerInitialBusinessReferralCode && !els.passengerInitialBusinessReferralCode.value) { els.passengerInitialBusinessReferralCode.value = invitedReferralCode; } if (state.sessions.passenger) { els.passengerSignInEmail.value = state.sessions.passenger.email ?? ""; els.passengerSignInPhone.value = state.sessions.passenger.phone ?? ""; setTranslatedStatus(els.passengerSignInStatus, "signedInAs", { identity: sessionDisplayIdentity("passenger") }); } if (state.passenger) { els.passengerName.value = state.passenger.name; els.passengerEmail.value = state.passenger.email ?? ""; els.passengerPhone.value = state.passenger.phone; els.passengerNationalId.value = state.passenger.nationalId ?? ""; els.passengerDob.value = state.passenger.dateOfBirth ?? ""; els.passengerCountry.value = state.passenger.country; els.passengerCity.value = state.passenger.city; els.passengerActiveCountry.value = state.passenger.country; els.passengerActiveCity.value = state.passenger.city; const passengerSubdivision = locationSubdivisionLabel(state.passenger.country); const passengerPhoneStatus = smsVerificationRelaxedForTesting() ? "Phone verification is relaxed for this staging pilot." : "Phone verified."; els.passengerStatus.textContent = `${state.passenger.name} is ready to request rides in ${state.passenger.city}. ${passengerPhoneStatus}`; els.passengerLocationStatus.textContent = `Ride requests publish in ${state.passenger.city} ${passengerSubdivision}, ${state.passenger.country}.`; const passengerPayment = paymentAccountFor("passenger", state.passenger.id); if (passengerPayment) { els.passengerPaymentProvider.value = passengerPayment.provider; els.passengerBankName.value = passengerPayment.institutionName ?? ""; els.passengerAccountHolder.value = passengerPayment.accountHolder ?? ""; els.passengerAccountLast4.value = passengerPayment.accountLast4 ?? ""; els.passengerPaymentReference.value = passengerPayment.reference ?? ""; els.passengerPaymentStatus.textContent = paymentAccountSummary("passenger", state.passenger); } } const passengerRecovery = pendingProfileRecoveryForRole("passenger"); if (!state.passenger && passengerRecovery) { if (passengerRecovery.name && !els.passengerName.value) els.passengerName.value = passengerRecovery.name; if (passengerRecovery.email) els.passengerEmail.value = passengerRecovery.email; if (passengerRecovery.phone && !els.passengerPhone.value) els.passengerPhone.value = passengerRecovery.phone; setTranslatedStatus(els.passengerStatus, "supabaseProfileMissing"); } if (typeof updatePassengerFareModeControls === "function") updatePassengerFareModeControls(); if (state.sessions.rider) { els.riderSignInEmail.value = state.sessions.rider.email ?? ""; els.riderSignInPhone.value = state.sessions.rider.phone ?? ""; setTranslatedStatus(els.riderSignInStatus, "signedInAs", { identity: sessionDisplayIdentity("rider") }); } if (state.rider) { els.riderName.value = state.rider.name; els.riderEmail.value = state.rider.email ?? ""; els.riderPhone.value = state.rider.phone; els.riderNationalId.value = state.rider.nationalId ?? ""; els.riderDob.value = storedDateToYearMonth(state.rider.dateOfBirth) || state.rider.dateOfBirth || ""; els.riderVehicle.value = "car"; populateVehicleCatalogFields(state.rider); els.riderCarMake.value = state.rider.carMake ?? els.riderCarMake.value; populateSelect(els.riderCarModel, carMakeCatalog[els.riderCarMake.value] ?? carMakeCatalog.Other, state.rider.carModel); els.riderCarBodyType.value = normalizeCarBodyType(state.rider.carBodyType); if (els.riderVehicleDesignation) { populateRiderVehicleDesignationOptions(state.rider); els.riderVehicleDesignation.value = normalizeRiderVehicleDesignation(state.rider.vehicleDesignation, state.rider.carBodyType); } syncRiderNavigationPreferenceInput(riderNavigationPreference(state.rider)); els.riderCarYear.value = String(state.rider.carYear ?? els.riderCarYear.value); els.riderCarColor.value = state.rider.carColor ?? ""; els.riderCountry.value = state.rider.country; els.riderCity.value = state.rider.city; updateRiderAreas(); els.riderArea.value = state.rider.area; els.riderActiveCountry.value = state.rider.country; els.riderActiveCity.value = state.rider.city; updateRiderActiveAreas(); els.riderActiveArea.value = state.rider.area; if (els.riderCredential) els.riderCredential.value = state.rider.credential; if (els.riderLicenseExpiresOn) els.riderLicenseExpiresOn.value = state.rider.driverLicenseExpiresOn ?? ""; els.riderVehicleVin.value = state.rider.vehicleVin ?? ""; els.riderRegistration.value = state.rider.registration; els.riderInsuranceProvider.value = state.rider.insuranceProvider ?? ""; els.riderInsuranceNumber.value = state.rider.insuranceNumber ?? ""; if (els.riderInsuranceExpiresOn) els.riderInsuranceExpiresOn.value = state.rider.insuranceExpiresOn ?? ""; if (els.riderBackgroundConsent) els.riderBackgroundConsent.checked = Boolean(state.rider.backgroundCheckConsentAt); els.riderLocationStatus.textContent = riderServiceAreaSummary(state.rider); if (typeof renderRiderAvailabilityControls === "function") { renderRiderAvailabilityControls(state.rider); } else { els.riderGpsStatus.textContent = riderCurrentFreshGps(state.rider) ? "Online and available." : "Offline. Activate when ready."; } const riderPayment = paymentAccountFor("rider", state.rider.id); if (riderPayment) { if (els.riderPaymentProvider) els.riderPaymentProvider.value = riderPayment.provider; els.riderBankName.value = riderPayment.institutionName ?? ""; els.riderAccountHolder.value = riderPayment.accountHolder ?? ""; els.riderAccountLast4.value = riderPayment.accountLast4 ?? ""; if (els.riderPaymentReference) els.riderPaymentReference.value = riderPayment.reference ?? ""; els.riderPaymentStatus.textContent = paymentAccountSummary("rider", state.rider); } populateRiderDailyRegionOptions(state.rider.country, state.rider.city); renderRiderDailyRegionStatus(state.rider); } const riderRecovery = pendingProfileRecoveryForRole("rider"); if (!state.rider && riderRecovery) { if (riderRecovery.name && !els.riderName.value) els.riderName.value = riderRecovery.name; if (riderRecovery.email) els.riderEmail.value = riderRecovery.email; if (riderRecovery.phone && !els.riderPhone.value) els.riderPhone.value = riderRecovery.phone; setTranslatedStatus(els.riderStatus, "supabaseProfileMissing"); } } function sessionDisplayIdentity(type) { const session = state.sessions?.[type] ?? {}; const account = type === "rider" ? state.rider : state.passenger; return session.email ?? account?.email ?? session.phone ?? account?.phone ?? "this Waka account"; } function updateConnectionStatus() { const statusPill = els.connectionStatus?.closest(".status-pill"); if (statusPill) statusPill.hidden = true; if (appConfig.mode === "supabase") { if (supabaseClient) { setTranslatedStatus(els.connectionStatus, "supabaseReady"); return; } if (!appConfig.supabaseUrl || !appConfig.supabaseAnonKey) { setTranslatedStatus(els.connectionStatus, "supabaseConfigNeeded"); return; } if (!window.supabase?.createClient) { els.connectionStatus.textContent = "Supabase auth ready"; return; } setTranslatedStatus(els.connectionStatus, window.supabase?.createClient ? "supabaseConnecting" : "supabaseSdkUnavailable"); return; } setTranslatedStatus(els.connectionStatus, navigator.onLine ? "onlineDemo" : "offlineReady"); } function updateInstallButton() { const standalone = window.matchMedia("(display-mode: standalone)").matches || navigator.standalone; els.installApp.hidden = true; els.installApp.disabled = standalone; els.installApp.textContent = standalone ? translatedValue("installed") : translatedValue("installApp"); } function makeVerificationCode() { return String(Math.floor(100000 + Math.random() * 900000)); } function phoneOtpErrorMessage(error) { if (/unsupported phone provider/i.test(error.message)) { return "Phone OTP is not enabled in Supabase. Configure an SMS provider in Auth > Providers > Phone, or use manual pilot verification before public launch."; } if (error.status === 429 || /rate limit|too many/i.test(error.message)) { return translatedMessage("phoneOtpRateLimited"); } return error.message; } function profileAvailabilityErrorMessage(error) { const message = String(error?.message || error || ""); if (error?.status === 429 || /too many account checks|rate limit|too many/i.test(message)) { return "Too many account checks. Wait a few minutes before trying again."; } return ""; } function phoneOtpCooldownKey(type, phone) { return `${type}:${phone}`; } function phoneOtpCooldownSeconds(type, phone) { const availableAt = phoneOtpCooldowns.get(phoneOtpCooldownKey(type, phone)) ?? 0; return Math.max(0, Math.ceil((availableAt - Date.now()) / 1000)); } function startPhoneOtpCooldown(type, phone) { phoneOtpCooldowns.set(phoneOtpCooldownKey(type, phone), Date.now() + phoneOtpCooldownMs); } function clearPhoneOtpCooldown(type, phone) { phoneOtpCooldowns.delete(phoneOtpCooldownKey(type, phone)); } function phoneDigits(value) { return String(value ?? "").replace(/\D/g, ""); } function phoneMatches(first, second) { const firstDigits = phoneDigits(first); const secondDigits = phoneDigits(second); if (!firstDigits || !secondDigits) return false; if (firstDigits === secondDigits) return true; if (firstDigits.length < 8 || secondDigits.length < 8) return false; return firstDigits.endsWith(secondDigits) || secondDigits.endsWith(firstDigits); } async function updatePassengerFareOffer(event, requestId) { event.preventDefault(); const form = event.currentTarget; const status = form.querySelector(".fare-boost-status"); const input = form.querySelector(".fare-boost-input"); const request = state.requests.find((item) => item.id === requestId); if (!canBoostPassengerFare(request)) { status.textContent = "Only open requests from this passenger can be updated."; return; } const rawFareDraft = String(input?.value ?? passengerFareBoostDrafts.get(requestId) ?? ""); passengerFareBoostDrafts.set(requestId, rawFareDraft); const nextFare = Number(rawFareDraft.replace(/[^\d]/g, "")); if (!nextFare || nextFare <= request.fareOffer) { status.textContent = `Enter a fare higher than ${formatMoney(request.fareOffer)}.`; return; } try { status.textContent = "Updating fare..."; await updateRideRequestFareInSupabase(request.id, nextFare); state.requests = state.requests.map((item) => item.id === request.id ? { ...item, fareOffer: nextFare } : item); passengerFareBoostDrafts.delete(request.id); passengerFareBoostLastFocus = null; if (typeof passengerFareBoostOpenRequestId !== "undefined") passengerFareBoostOpenRequestId = null; if (input instanceof HTMLInputElement) input.blur(); pushSystemChat(request.id, `Passenger increased the fare offer to ${formatMoney(nextFare)}.`); saveState(); renderAll(); void refreshMarketplace({ silent: true }); } catch (error) { status.textContent = error.message; } } // Supabase runtime configuration, authentication, profile, storage, and row mapping helpers. let supabaseClient = null; let supabaseSdkPromise = null; let supabaseRestSession = null; let passwordResetRoleCapturedBeforeSupabaseInit = ""; let riderProfileHydrationInFlight = null; const riderProfileHydrationRefreshMs = 15000; let riderProfileHydrationRefreshAt = 0; let insuranceTelemetryRpcUnavailable = false; let lastInsuranceTelemetrySource = "not used"; function mapRideRequestFromDatabase(request, profileMap = new Map(), offerMap = new Map()) { const passenger = profileMap.get(request.passenger_id); const selectedOffer = offerMap.get(request.selected_offer_id); const selectedRider = selectedOffer ? profileMap.get(selectedOffer.riderId) : null; const pickupGps = gpsPointFromDatabaseLocation( request.pickup_location, request.pickup_gps_accuracy_meters, request.pickup_gps_captured_at ) ?? normalizeGpsPoint({ latitude: request.pickup_latitude ?? request.pickup_lat, longitude: request.pickup_longitude ?? request.pickup_lng ?? request.pickup_lon, accuracyMeters: request.pickup_gps_accuracy_meters, capturedAt: request.pickup_gps_captured_at }); const pickupLocationShared = Boolean(request.pickup_location || pickupGps); return { id: request.id, passengerId: request.passenger_id, passengerName: passenger?.full_name ?? state.passenger?.name ?? "Passenger", passengerPhone: "Hidden by Waka relay", businessAccountId: request.business_account_id ?? null, country: request.country, city: request.city, pickupArea: request.pickup_area, pickupDescription: request.pickup_description, destinationArea: request.destination_area ?? null, destination: request.destination, destinationPlaceId: request.destination_place_id ?? null, destinationFormattedAddress: request.destination_formatted_address ?? null, destinationLatitude: request.destination_lat ?? null, destinationLongitude: request.destination_lng ?? null, vehicle: request.vehicle_preference, carTypePreference: normalizeCarTypePreference(request.car_type_preference), rideStops: normalizeRideStops(request.ride_stops), rideStopPoints: normalizeRideStopPoints(request.ride_stop_points, request.ride_stops), currentStopIndex: Math.max(0, Number(request.current_stop_index ?? 0) || 0), lastStopArrivedAt: request.last_stop_arrived_at ?? null, estimatedDistanceMiles: request.estimated_distance_miles ?? null, estimatedTravelMinutes: request.estimated_travel_minutes ?? null, acceptedRouteChangeFare: request.accepted_route_change_fare ?? 0, routeEstimateSource: request.route_estimate_source ?? null, routeEstimateProvider: request.route_estimate_provider ?? null, routeEstimateCached: Boolean(request.route_estimate_cached), routeEstimateKey: request.route_estimate_key ?? null, routeEstimatePolyline: request.route_estimate_polyline ?? null, routeEstimateCreatedAt: request.route_estimate_created_at ?? null, routeEstimateDestinationFingerprint: request.route_estimate_destination_fingerprint ?? null, fareOffer: request.fare_offer_xaf, fareMode: normalizePassengerFareMode(request.fare_mode), fareHistory: Array.isArray(request.fare_history) ? request.fare_history : [], paymentPreference: paymentFromDatabase(request.payment_preference), rideTiming: request.scheduled_at ? "scheduled" : "now", scheduledAt: request.scheduled_at ?? null, riderConfirmationStatus: request.rider_confirmation_status ?? null, riderConfirmationRequestedAt: request.rider_confirmation_requested_at ?? null, riderConfirmedAt: request.rider_confirmed_at ?? null, releasedAt: request.released_at ?? null, status: request.status, selectedOfferId: request.selected_offer_id, agreedFare: selectedOffer?.fare ?? null, selectedRiderId: request.selected_rider_id ?? selectedOffer?.riderId ?? null, selectedRiderName: selectedRider?.full_name ?? null, cancellationFeeAmount: request.cancellation_fee_amount ?? 0, cancellationFeeCurrency: request.cancellation_fee_currency ?? moneyCurrencyForCountry(request.country), cancellationFeeStatus: request.cancellation_fee_status ?? "not_applicable", cancellationFeeRiderId: request.cancellation_fee_rider_id ?? null, cancellationFeeElapsedMinutes: request.cancellation_fee_elapsed_minutes ?? null, createdAt: request.created_at, matchedAt: request.matched_at, arrivedAt: request.arrived_at ?? null, startedAt: request.started_at ?? null, completedAt: request.completed_at ?? null, gpsDistanceMeters: request.gps_distance_meters ?? null, matchSource: request.match_source ?? null, pickupLocationShared, pickupGps, pickupLatitude: pickupGps?.latitude ?? null, pickupLongitude: pickupGps?.longitude ?? null, pickupGpsAccuracyMeters: request.pickup_gps_accuracy_meters ?? null, pickupGpsCapturedAt: request.pickup_gps_captured_at ?? null, cancelledBy: request.cancelled_by ?? null, cancelledAt: request.cancelled_at ?? null, cancelReason: request.cancel_reason ?? null, cancellationFeeAmount: request.cancellation_fee_amount ?? 0, cancellationFeeCurrency: request.cancellation_fee_currency ?? moneyCurrencyForCountry(request.country), cancellationFeeStatus: request.cancellation_fee_status ?? "not_applicable", cancellationFeeRiderId: request.cancellation_fee_rider_id ?? null, cancellationFeeElapsedMinutes: request.cancellation_fee_elapsed_minutes ?? null }; } function mapOfferFromDatabase(offer) { return { id: offer.id, requestId: offer.ride_request_id, riderId: offer.rider_id, fare: offer.fare_xaf, type: offer.type, note: offer.public_note ?? "", pickupDistanceMeters: offer.pickup_distance_meters ?? null, distanceSource: offer.distance_source ?? null, fareHistory: Array.isArray(offer.fare_history) ? offer.fare_history : [], createdAt: offer.created_at, updatedAt: offer.updated_at ?? offer.created_at }; } function mapPassengerApproachFromDatabase(row) { const riderApproachGps = normalizeGpsPoint({ latitude: row.rider_lat, longitude: row.rider_lng, accuracyMeters: row.accuracy_meters, capturedAt: row.captured_at }); return { requestId: row.request_id, selectedRiderId: row.rider_id, selectedRiderName: firstNameOnly(row.rider_name, "Rider"), riderApproachDistanceMeters: row.pickup_distance_meters ?? null, riderApproachSource: row.distance_source ?? null, riderApproachGps, riderApproachLatitude: riderApproachGps?.latitude ?? null, riderApproachLongitude: riderApproachGps?.longitude ?? null, riderApproachAccuracyMeters: row.accuracy_meters ?? null, riderApproachCapturedAt: row.captured_at ?? null, riderApproachIsLive: Boolean(row.is_live) }; } function mapActiveRideContactFromDatabase(row) { return { requestId: row.request_id, contactUserId: row.counterparty_id, contactName: firstNameOnly(row.counterparty_name, "Matched contact"), contactProfilePhotoPath: row.counterparty_profile_photo_path ?? "", contactPhone: "", contactRelayPhone: row.relay_phone ?? "", contactRelayStatus: row.relay_status ?? "relay_not_configured", contactProviderSessionId: row.provider_session_id ?? "" }; } function mapRiderCompletedMileageSegmentFromDatabase(row = {}) { return { id: row.id, riderId: row.rider_id, requestId: row.ride_request_id, period: row.insurance_period, status: row.status, distanceMiles: Number(row.distance_miles ?? 0), startedAt: row.started_at, endedAt: row.ended_at, eventCount: Number(row.event_count ?? 0), source: row.source ?? "" }; } function mapChatFromDatabase(message) { const parsed = parseRouteChangeEventText(message.body); const systemLike = parsed || /^\[System\]/i.test(String(message.body ?? "")); const senderRole = ["passenger", "rider"].includes(message.sender_role) ? message.sender_role : ""; return { id: message.id, requestId: message.ride_request_id, senderId: message.sender_id, sender: systemLike ? "system" : senderRole || (message.sender_id === state.rider?.id ? "rider" : message.sender_id === state.passenger?.id ? "passenger" : "system"), text: parsed?.message ?? String(message.body ?? "").replace(/^\[System\]\s*/i, ""), routeChangeEvent: parsed?.event ?? null, deliveryStatus: "sent", createdAt: message.created_at }; } function mapRideRouteChangeFromDatabase(row = {}) { const payload = row.change_payload && typeof row.change_payload === "object" ? row.change_payload : {}; const stops = normalizeRideStops(payload.rideStops ?? payload.ride_stops ?? []); const stopPoints = normalizeRideStopPoints(payload.rideStopPoints ?? payload.ride_stop_points ?? [], stops); return { id: payload.id ?? row.change_id, requestId: payload.requestId ?? row.ride_request_id, type: payload.type ?? row.change_type, country: payload.country ?? null, destinationArea: payload.destinationArea ?? payload.destination_area ?? null, destination: payload.destination ?? null, destinationPlaceId: payload.destinationPlaceId ?? null, destinationFormattedAddress: payload.destinationFormattedAddress ?? null, destinationLatitude: payload.destinationLatitude ?? null, destinationLongitude: payload.destinationLongitude ?? null, rideStops: stops, rideStopPoints: stopPoints, routeEstimate: payload.routeEstimate ?? null, routeDelta: payload.routeDelta ?? null, additionalFare: Number(payload.additionalFare ?? row.additional_fare ?? 0) || 0, totalFare: Number(payload.totalFare ?? row.total_fare ?? 0) || 0, acceptedRouteChangeFare: Number(payload.acceptedRouteChangeFare ?? 0) || 0, requestedAt: payload.requestedAt ?? row.created_at, decidedAt: payload.decidedAt ?? (row.status === "pending" ? null : row.decided_at), status: ["pending", "accepted", "declined"].includes(row.status) ? row.status : payload.status ?? "pending", passengerId: payload.passengerId ?? row.passenger_id ?? null, riderId: payload.riderId ?? row.rider_id ?? null }; } function profileToPassenger(profile) { return { id: profile.id, supabaseUserId: profile.id, name: profile.full_name, email: profile.email, phone: profile.phone, phoneVerified: Boolean(profile.phone_verified_at), phoneVerifiedAt: profile.phone_verified_at, nationalId: profile.national_id_number, dateOfBirth: profile.date_of_birth, preferredLanguage: profile.preferred_language, country: profile.country, city: profile.city, profilePhotoPath: profile.profile_photo_path, accountStatus: profile.account_status ?? "active", accountStatusReason: profile.account_status_reason ?? "", accountStatusChangedAt: profile.account_status_changed_at ?? null, accountStatusChangedBy: profile.account_status_changed_by ?? null, accountClosedAt: profile.account_closed_at ?? null, createdAt: profile.created_at }; } function mapAdminNotificationFromDatabase(notification) { const deliveryChannels = normalizeNotificationDeliveryChannels(notification.delivery_channels); return { id: notification.id, recipientId: notification.recipient_id, recipientRole: notification.recipient_role, title: notification.title ?? "Waka notice", body: notification.body, createdBy: notification.created_by, createdByRole: notification.created_by_role ?? null, requestId: notification.request_id ?? null, actionUrl: notification.action_url ?? "", eventType: notification.event_type ?? "", deliveryChannels, deliveryStatus: notification.delivery_status ?? {}, createdAt: notification.created_at, readAt: notification.read_at ?? null }; } function normalizeNotificationDeliveryChannels(value) { const raw = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : []; const channels = raw .map((item) => String(item ?? "").trim().toLowerCase()) .filter((item) => ["in_app", "push", "email", "sms"].includes(item)); if (!channels.includes("in_app")) channels.unshift("in_app"); return [...new Set(channels)]; } function notificationDeliveryLabel(channels = []) { const normalized = normalizeNotificationDeliveryChannels(channels); const labels = { in_app: "in-app", push: "phone push", email: "email", sms: "SMS" }; return normalized.map((channel) => labels[channel] ?? channel).join(", "); } function mapSupportTicketFromDatabase(ticket, profileMap = new Map()) { const account = profileMap.get(ticket.account_id); const reviewer = profileMap.get(ticket.reviewed_by); return { id: ticket.id, accountId: ticket.account_id, accountRole: ticket.account_role, accountName: account?.full_name ?? account?.email ?? "Account", category: ticket.category, subject: ticket.subject ?? "Support request", message: ticket.message, priority: ticket.priority ?? "medium", status: ticket.status ?? "open", reviewedBy: ticket.reviewed_by ?? null, reviewedByName: reviewer?.full_name ?? reviewer?.email ?? "", reviewedAt: ticket.reviewed_at ?? null, createdAt: ticket.created_at, updatedAt: ticket.updated_at ?? ticket.created_at }; } function profileAccountStatus(profile) { return profile?.account_status ?? profile?.accountStatus ?? "active"; } function profileAccountIsBlocked(profile) { return ["suspended", "closed"].includes(profileAccountStatus(profile)); } function profileAccountBlockedMessage(profile) { const status = profileAccountStatus(profile); const reason = profile?.account_status_reason ?? profile?.accountStatusReason ?? ""; const base = status === "closed" ? "This Waka account has been closed by admin." : "This Waka account activity is suspended by admin."; return reason ? `${base} Reason: ${reason}` : `${base} Contact Waka support for review.`; } function directoryRowToPassenger(row) { return profileToPassenger({ id: row.id, full_name: row.full_name, email: row.email, phone: row.phone, phone_verified_at: row.phone_verified_at, national_id_number: row.national_id_number, date_of_birth: row.date_of_birth, preferred_language: row.preferred_language, country: row.country, city: row.city, profile_photo_path: row.profile_photo_path, account_status: row.account_status, account_status_reason: row.account_status_reason, account_status_changed_at: row.account_status_changed_at, account_status_changed_by: row.account_status_changed_by, account_closed_at: row.account_closed_at, created_at: row.created_at }); } function directoryRowToRider(row) { const documents = parseRiderDocuments(row.document_path); const vehicleDesignation = normalizeRiderVehicleDesignation(row.vehicle_designation ?? documents.vehicleDesignation, row.car_body_type); return { ...directoryRowToPassenger(row), area: row.operating_area ?? "No application", vehicle: row.vehicle ?? "not set", credential: row.credential_number ?? "No application", registration: row.vehicle_registration ?? "No application", carMake: row.car_make ?? "", carModel: row.car_model ?? "", carBodyType: normalizeCarBodyType(row.car_body_type), vehicleDesignation, navigationPreference: normalizeRiderNavigationPreference(documents.navigationPreference), carYear: row.car_year ?? "", carColor: row.car_color ?? "", vehicleVin: row.vehicle_vin ?? "", insuranceProvider: row.insurance_provider ?? "", insuranceNumber: row.insurance_number ?? "", driverLicenseExpiresOn: row.driver_license_expires_on ?? "", insuranceExpiresOn: row.insurance_expires_on ?? "", complianceSuspendedAt: row.compliance_suspended_at ?? null, complianceSuspensionReason: row.compliance_suspension_reason ?? "", backgroundCheckConsentAt: row.background_check_consent_at ?? null, backgroundCheckProvider: row.background_check_consent_provider ?? row.background_check_provider ?? "", backgroundCheckConsentVersion: row.background_check_consent_version ?? "", backgroundCheckStatus: row.background_check_status ?? "not requested", backgroundCheckDecision: row.background_check_decision ?? "pending", documentName: row.document_path ?? "", documents, driverLicenseDocumentPath: documents.driverLicense, vehicleRegistrationDocumentPath: documents.vehicleRegistration, insuranceDocumentPath: documents.insurance, vehicleInspectionDocumentPath: documents.vehicleInspection, status: row.application_status ?? "profile only", approvedAt: row.reviewed_at ?? null, trialEndsAt: row.trial_ends_at ?? null, subscriptionPaidUntil: row.paid_until ?? null, rating: "new" }; } function riderTrialEndsFromApproval(application, subscription) { if (subscription?.trial_ends_at) return subscription.trial_ends_at; if (application?.status !== "approved") return null; const anchor = application?.reviewed_at || application?.updated_at || application?.created_at; if (!anchor) return null; const trialEnd = new Date(anchor); if (Number.isNaN(trialEnd.getTime())) return null; trialEnd.setDate(trialEnd.getDate() + trialDays); return trialEnd.toISOString(); } function riderApplicationWorkspaceRank(application) { const rank = { approved: 70, background_pending: 60, needs_correction: 50, pending: 40, declined: 30, suspended: 20 }; return rank[application?.status] ?? 0; } function riderApplicationWorkspaceTime(application) { const timestamp = application?.reviewed_at || application?.updated_at || application?.created_at; const value = timestamp ? new Date(timestamp).getTime() : 0; return Number.isFinite(value) ? value : 0; } function chooseRiderApplicationForWorkspace(rows = []) { const applications = (Array.isArray(rows) ? rows : [rows]).filter(Boolean); if (!applications.length) return null; return [...applications].sort((left, right) => { const rankDifference = riderApplicationWorkspaceRank(right) - riderApplicationWorkspaceRank(left); if (rankDifference) return rankDifference; return riderApplicationWorkspaceTime(right) - riderApplicationWorkspaceTime(left); })[0] ?? null; } function applySignedInProfile(type, profile, user) { state.sessions[type] = { phone: profile.phone, email: profile.email, userId: user.id, signedInAt: new Date().toISOString() }; if (type === "passenger") { state.passenger = profileToPassenger(profile); state.passengers = upsertById(state.passengers, state.passenger); } if (type === "rider") { state.rider = { ...(state.rider ?? {}), ...profileToPassenger(profile), area: state.rider?.area ?? "", vehicle: state.rider?.vehicle ?? "car", credential: state.rider?.credential ?? "", registration: state.rider?.registration ?? "", carMake: state.rider?.carMake ?? "", carModel: state.rider?.carModel ?? "", carBodyType: normalizeCarBodyType(state.rider?.carBodyType), vehicleDesignation: normalizeRiderVehicleDesignation(state.rider?.vehicleDesignation, state.rider?.carBodyType), navigationPreference: riderNavigationPreference(state.rider), carYear: state.rider?.carYear ?? "", carColor: state.rider?.carColor ?? "", vehicleVin: state.rider?.vehicleVin ?? "", insuranceProvider: state.rider?.insuranceProvider ?? "", insuranceNumber: state.rider?.insuranceNumber ?? "", backgroundCheckConsentAt: state.rider?.backgroundCheckConsentAt ?? null, backgroundCheckProvider: state.rider?.backgroundCheckProvider ?? "", backgroundCheckConsentVersion: state.rider?.backgroundCheckConsentVersion ?? "", backgroundCheckStatus: state.rider?.backgroundCheckStatus ?? "not requested", backgroundCheckDecision: state.rider?.backgroundCheckDecision ?? "pending", documentName: state.rider?.documentName ?? "", documents: riderDocuments(state.rider), needsApplication: Boolean(profile.needsApplication), accountStatus: profile.account_status ?? "active", accountStatusReason: profile.account_status_reason ?? "", accountStatusChangedAt: profile.account_status_changed_at ?? null, accountStatusChangedBy: profile.account_status_changed_by ?? null, accountClosedAt: profile.account_closed_at ?? null, status: profile.needsApplication ? "profile only" : state.rider?.status ?? profile.status ?? "pending", approvedAt: state.rider?.approvedAt ?? null, trialEndsAt: state.rider?.trialEndsAt ?? null, subscriptionPaidUntil: state.rider?.subscriptionPaidUntil ?? null, rating: state.rider?.rating ?? "new" }; state.riders = upsertById(state.riders, state.rider); } } function applyRuntimeConfig(localConfig, source) { appConfig = { ...appConfig, ...localConfig, buckets: { ...appConfig.buckets, ...(localConfig.buckets ?? {}) } }; runtimeConfigSource = source; window.WAKA_CONFIG = appConfig; } function readCachedRuntimeConfig() { try { return JSON.parse(localStorage.getItem(runtimeConfigStorageKey)); } catch { return null; } } function cacheRuntimeConfig(localConfig) { try { localStorage.setItem(runtimeConfigStorageKey, JSON.stringify(localConfig)); } catch { // Local storage can be unavailable in private contexts; the live config still works. } } function isLocalDevelopmentHost(hostname = window.location.hostname) { return ["127.0.0.1", "localhost", "::1", ""].includes(hostname); } function isSecureRuntimeContext() { return window.location.protocol === "https:" || isLocalDevelopmentHost(); } function runtimeConfigFileName() { const configured = String(appConfig.runtimeConfigFile ?? "").trim(); if (configured) return configured; return isLocalDevelopmentHost() ? "config.local.json" : "config.runtime.json"; } async function fetchRuntimeConfig() { const configFile = runtimeConfigFileName(); const configUrl = new URL(configFile, window.location.href); const cacheBustUrl = new URL(configUrl.href); cacheBustUrl.searchParams.set("t", Date.now().toString()); const urls = [...new Set([configFile, configUrl.href, cacheBustUrl.href])]; const attempts = urls.flatMap((url) => [ fetchRuntimeConfigUrl(url), fetchRuntimeConfigWithXhr(url) ]); return firstRuntimeConfig(attempts); } function firstRuntimeConfig(attempts) { return new Promise((resolve) => { let settled = false; let pending = attempts.length; const timer = window.setTimeout(() => finish(null), runtimeConfigTimeoutMs + 500); function finish(config) { if (settled) return; settled = true; window.clearTimeout(timer); resolve(config); } attempts.forEach((attempt) => { attempt .then((config) => { if (config) { finish(config); return; } pending -= 1; if (pending === 0) finish(null); }) .catch(() => { pending -= 1; if (pending === 0) finish(null); }); }); }); } async function fetchRuntimeConfigUrl(url) { let timeoutId; const controller = new AbortController(); const timeout = new Promise((_, reject) => { timeoutId = window.setTimeout(() => { controller.abort(); reject(new Error("Runtime config load timed out.")); }, runtimeConfigTimeoutMs); }); try { const response = await Promise.race([ fetch(url, { cache: "no-store", credentials: "same-origin", signal: controller.signal }), timeout ]); if (!response.ok) return null; return response.json(); } finally { window.clearTimeout(timeoutId); } } function fetchRuntimeConfigWithXhr(url) { return new Promise((resolve, reject) => { const request = new XMLHttpRequest(); request.open("GET", url, true); request.responseType = "text"; request.timeout = runtimeConfigTimeoutMs; request.onload = () => { if (request.status < 200 || request.status >= 300) { resolve(null); return; } try { resolve(JSON.parse(request.responseText)); } catch (error) { reject(error); } }; request.onerror = () => reject(new Error("Runtime config XHR failed.")); request.ontimeout = () => reject(new Error("Runtime config XHR timed out.")); request.send(); }); } async function loadRuntimeConfig() { const cachedConfig = readCachedRuntimeConfig(); const configFile = runtimeConfigFileName(); try { const localConfig = await fetchRuntimeConfig(); if (!localConfig) { if (cachedConfig) applyRuntimeConfig(cachedConfig, `cached ${configFile}`); return; } applyRuntimeConfig(localConfig, configFile); cacheRuntimeConfig(localConfig); } catch { if (cachedConfig) applyRuntimeConfig(cachedConfig, `cached ${configFile}`); } } function loadSupabaseSdk() { if (window.supabase?.createClient) return Promise.resolve(true); if (supabaseSdkPromise) return supabaseSdkPromise; supabaseSdkPromise = new Promise((resolve) => { const script = document.createElement("script"); let settled = false; const timer = window.setTimeout(() => finish(false), 8000); function finish(loaded) { if (settled) return; settled = true; window.clearTimeout(timer); if (!loaded) supabaseSdkPromise = null; resolve(loaded); } script.src = supabaseSdkUrl; script.async = true; script.dataset.wakaSupabaseSdk = "true"; script.onload = () => finish(Boolean(window.supabase?.createClient)); script.onerror = () => finish(false); document.head.appendChild(script); }); return supabaseSdkPromise; } async function initSupabaseClient() { if (appConfig.mode !== "supabase") return; if (!appConfig.supabaseUrl || !appConfig.supabaseAnonKey) { updateConnectionStatus(); return; } passwordResetRoleCapturedBeforeSupabaseInit ||= passwordResetRoleFromLocation(); const passwordResetReturnPending = Boolean(passwordResetRoleCapturedBeforeSupabaseInit); const sdkReady = await loadSupabaseSdk(); if (!sdkReady) { updateConnectionStatus(); return; } supabaseClient = window.supabase.createClient(appConfig.supabaseUrl, appConfig.supabaseAnonKey, { auth: { persistSession: true, autoRefreshToken: true, detectSessionInUrl: !passwordResetReturnPending } }); updateConnectionStatus(); } function usesManualPhoneVerification() { return appConfig.phoneVerificationMode === "manual"; } function smsVerificationRelaxedForTesting() { return configFlagEnabled(appConfig.relaxSmsVerificationForTesting); } function markManualPhoneVerified(type, phone, status) { state.verification[type] = { phone, phoneDigits: phoneDigits(phone), verifiedPhone: phone, verifiedAt: new Date().toISOString(), provider: "manual-pilot" }; saveState(); setTranslatedStatus(status, "manualPhoneVerified"); return true; } function markSmsRelaxedPhoneVerified(type, phone, status) { state.verification[type] = { phone, phoneDigits: phoneDigits(phone), verifiedPhone: phone, verifiedAt: new Date().toISOString(), provider: "email-test-bypass" }; saveState(); setTranslatedStatus(status, "smsVerificationRelaxedForTesting"); return true; } function isSupabaseMode() { return appConfig.mode === "supabase" && Boolean(supabaseClient); } function hasSupabaseConfig() { return appConfig.mode === "supabase" && Boolean(appConfig.supabaseUrl && appConfig.supabaseAnonKey); } function hasSupabaseRuntime() { return isSupabaseMode() || Boolean(supabaseRestSession); } function configFlagEnabled(value) { return value === true || String(value ?? "").toLowerCase() === "true"; } function strictProductionModeEnabled() { return configFlagEnabled(appConfig.strictProductionMode); } function normalizedPublicRole(value) { const role = String(value || "").trim().toLowerCase(); return ["passenger", "rider"].includes(role) ? role : ""; } function authIsolationMode() { const mode = String(appConfig.authIsolationMode || "strict-single-tenant").trim().toLowerCase(); return ["strict-single-tenant", "separate-tenants", "shared"].includes(mode) ? mode : "strict-single-tenant"; } function strictRoleAuthIsolationEnabled() { return authIsolationMode() !== "shared"; } function separateRoleAuthTenantsRequested() { return authIsolationMode() === "separate-tenants"; } function roleAuthTenantConfig(role) { const normalizedRole = normalizedPublicRole(role); const configured = appConfig.roleAuthTenants?.[normalizedRole] || {}; return { supabaseUrl: String(configured.supabaseUrl || "").trim(), supabaseAnonKey: String(configured.supabaseAnonKey || "").trim() }; } function roleAuthTenantConfigured(role) { const tenant = roleAuthTenantConfig(role); return Boolean(tenant.supabaseUrl && tenant.supabaseAnonKey); } function signedInProfileHasPrimaryRole(profile, role) { return Boolean(profile?.id && normalizedPublicRole(profile.role) === normalizedPublicRole(role)); } async function signedInProfileCanUseWorkspaceRole(profile, role) { if (!profile?.id || !normalizedPublicRole(role)) return false; if (strictRoleAuthIsolationEnabled()) return signedInProfileHasPrimaryRole(profile, role); return signedInProfileHasRole(profile, role); } const platformFeatureFlagDefinitions = Object.freeze([ { key: "ride_publishing_enabled", label: "Ride publishing", description: "Allow passengers to publish new ride requests.", defaultEnabled: true }, { key: "rider_activation_enabled", label: "Rider activation", description: "Allow approved riders to activate availability and enter the marketplace.", defaultEnabled: true }, { key: "notification_delivery_enabled", label: "External notification delivery", description: "Allow queued push, SMS, and email delivery workers to process notifications.", defaultEnabled: true }, { key: "route_estimates_enabled", label: "Route estimates", description: "Allow paid route estimate Edge Function calls.", defaultEnabled: true }, { key: "marketplace_realtime_enabled", label: "Marketplace realtime", description: "Allow browser Supabase Realtime subscriptions for ride, offer, chat, and notice updates.", defaultEnabled: true }, { key: "client_error_reporting_enabled", label: "Client error reporting", description: "Allow signed-in browser runtime errors to be sanitized and recorded in system events.", defaultEnabled: true } ]); const platformFeatureFlagsCacheMs = 30 * 1000; let platformFeatureFlags = defaultPlatformFeatureFlags(); let platformFeatureFlagsLoadedAt = 0; let platformFeatureFlagsPromise = null; let platformFeatureFlagsWarningShown = false; function platformFeatureDefinitions() { return platformFeatureFlagDefinitions.map((definition) => ({ ...definition })); } function defaultPlatformFeatureFlags() { return Object.fromEntries(platformFeatureFlagDefinitions.map((definition) => [ definition.key, definition.defaultEnabled !== false ])); } function platformFeatureEnabled(key) { return platformFeatureFlags[String(key)] !== false; } function normalizePlatformFeatureFlagRow(row = {}) { return { key: row.flag_key ?? row.key, label: row.label ?? "", description: row.description ?? "", enabled: row.enabled !== false, reason: row.reason ?? "", updatedAt: row.updated_at ?? row.updatedAt ?? null, updatedBy: row.updated_by ?? row.updatedBy ?? null }; } async function loadPlatformFeatureFlagsFromSupabase({ force = false } = {}) { if (!hasSupabaseRuntime()) return platformFeatureFlags; const now = Date.now(); if (!force && platformFeatureFlagsLoadedAt && now - platformFeatureFlagsLoadedAt < platformFeatureFlagsCacheMs) { return platformFeatureFlags; } if (platformFeatureFlagsPromise) return platformFeatureFlagsPromise; platformFeatureFlagsPromise = (async () => { try { let rows; if (supabaseClient?.from) { const { data, error } = await withSupabaseTimeout( supabaseClient .from("platform_feature_flags") .select("flag_key,enabled,updated_at") .order("flag_key", { ascending: true }), "Loading platform feature flags", optionalSupabaseRequestTimeoutMs ); if (error) throw error; rows = data; } else { rows = await supabaseRestRequest("/rest/v1/platform_feature_flags?select=flag_key,enabled,updated_at&order=flag_key.asc"); } const next = defaultPlatformFeatureFlags(); (rows ?? []).forEach((row) => { const key = String(row?.flag_key ?? "").trim(); if (key) next[key] = row.enabled !== false; }); platformFeatureFlags = next; platformFeatureFlagsLoadedAt = Date.now(); return platformFeatureFlags; } catch (error) { if (!platformFeatureFlagsWarningShown) { platformFeatureFlagsWarningShown = true; logClientWarning("Platform feature flags could not be loaded; using enabled defaults.", error); } platformFeatureFlagsLoadedAt = Date.now(); return platformFeatureFlags; } finally { platformFeatureFlagsPromise = null; } })(); return platformFeatureFlagsPromise; } async function assertPlatformFeatureEnabled(key, label = "This feature") { await loadPlatformFeatureFlagsFromSupabase(); if (!platformFeatureEnabled(key)) { throw new Error(`${label} is temporarily paused by Waka operations.`); } } function demoToolsAllowed() { return appConfig.mode === "demo" && isLocalDevelopmentHost() && !strictProductionModeEnabled(); } function phoneOtpSignInEnabled() { return configFlagEnabled(appConfig.enablePhoneOtpSignIn); } function shouldBlockClientFallbackWrites() { return hasSupabaseRuntime() || strictProductionModeEnabled(); } function assertClientFallbackAllowed(feature, sqlFile) { if (!shouldBlockClientFallbackWrites()) return; const runtimeMode = hasSupabaseRuntime() ? "Supabase mode" : "strict production mode"; throw new Error(`${feature} requires ${sqlFile} in ${runtimeMode}. Install the SQL/RPC from docs/SUPABASE-LAUNCH-RUNBOOK.md, then retry.`); } async function withSupabaseTimeout(promise, label, timeoutMs = supabaseRequestTimeoutMs) { let timeoutId; const timeout = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject(new Error(`${label} is taking too long. Check your internet connection, Supabase project, and Auth settings, then try again.`)); }, timeoutMs); }); try { return await Promise.race([promise, timeout]); } finally { clearTimeout(timeoutId); } } async function supabaseRestRequest(path, { method = "GET", body = null, accessToken = supabaseRestSession?.access_token, headers = {}, returnResponse = false } = {}) { if (!hasSupabaseConfig()) throw new Error("Supabase config is missing."); const requestHeaders = { apikey: appConfig.supabaseAnonKey, Authorization: `Bearer ${accessToken || appConfig.supabaseAnonKey}`, ...headers }; if (body !== null) requestHeaders["Content-Type"] = "application/json"; const response = await fetch(`${appConfig.supabaseUrl}${path}`, { method, headers: requestHeaders, body: body === null ? null : JSON.stringify(body) }); const text = await response.text(); let payload = null; if (text) { try { payload = JSON.parse(text); } catch { payload = text; } } if (!response.ok) { throw new Error(payload?.msg || payload?.message || text || `Supabase request failed with HTTP ${response.status}.`); } return returnResponse ? { data: payload, headers: response.headers, status: response.status } : payload; } async function callSupabaseRpc(functionName, body, label, timeoutMs = optionalSupabaseRequestTimeoutMs) { if (!hasSupabaseRuntime()) return null; if (!supabaseClient) { return withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rpc/${functionName}`, { method: "POST", body, headers: { Prefer: "return=minimal" } }), label, timeoutMs ); } const { data, error } = await withSupabaseTimeout( supabaseClient.rpc(functionName, body), label, timeoutMs ); if (error) throw error; return data; } async function signInWithSupabasePasswordRest(email, password) { const session = await withSupabaseTimeout( supabaseRestRequest("/auth/v1/token?grant_type=password", { method: "POST", accessToken: appConfig.supabaseAnonKey, body: { email, password } }), "Signing in with Supabase Auth" ); supabaseRestSession = session; updateConnectionStatus(); return session; } async function signInAndLoadProfileForRole(email, password, role, onStage = null) { let user = null; let profile = null; reportSupabaseStep(onStage, `Signing in to the ${role} login...`); if (supabaseClient) { const { data, error } = await withSupabaseTimeout( supabaseClient.auth.signInWithPassword({ email, password }), `Signing in as ${role}`, optionalSupabaseRequestTimeoutMs ); if (error) throw error; user = data?.user ?? null; if (!user?.id) throw new Error("Supabase accepted the sign-in but did not return the account id."); reportSupabaseStep(onStage, `Loading the Waka ${role} profile...`); const { data: profileData, error: profileError } = await withSupabaseTimeout( supabaseClient .from("profiles") .select("*") .eq("id", user.id) .maybeSingle(), `Loading the ${role} profile`, supabaseProfileSaveTimeoutMs ); if (profileError) throw profileError; profile = profileData; } else { const session = await signInWithSupabasePasswordRest(email, password); user = session.user; reportSupabaseStep(onStage, `Loading the Waka ${role} profile...`); profile = await selectProfileRest(user.id, "*", session.access_token); } if (!profile) throw new Error(`Supabase sign-in worked, but the Waka ${role} profile was not found.`); if (!(await signedInProfileCanUseWorkspaceRole(profile, role))) { throw new Error(`This Waka login does not have a separate ${role} profile. Use the ${role} account or create one before signing in here.`); } return { user, profile: profileForWorkspaceRole(profile, { role }) }; } async function selectProfileRest(userId, select = "*", accessToken = supabaseRestSession?.access_token) { const params = new URLSearchParams(); params.set("id", `eq.${userId}`); params.set("select", select); const rows = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/profiles?${params.toString()}`, { accessToken }), "Loading the Supabase profile", supabaseProfileSaveTimeoutMs ); return Array.isArray(rows) ? rows[0] ?? null : rows; } async function selectRiderApplicationRest(riderId, accessToken = supabaseRestSession?.access_token) { const params = new URLSearchParams(); params.set("rider_id", `eq.${riderId}`); params.set("select", "*"); params.set("order", "created_at.desc"); params.set("limit", "10"); const rows = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rider_applications?${params.toString()}`, { accessToken }), "Loading the rider application", supabaseProfileSaveTimeoutMs ); return Array.isArray(rows) ? chooseRiderApplicationForWorkspace(rows) : rows; } async function selectRiderSubscriptionRest(riderId, accessToken = supabaseRestSession?.access_token) { const params = new URLSearchParams(); params.set("rider_id", `eq.${riderId}`); params.set("select", "*"); params.set("limit", "1"); const rows = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rider_subscriptions?${params.toString()}`, { accessToken }), "Loading the rider subscription", supabaseProfileSaveTimeoutMs ); return Array.isArray(rows) ? rows[0] ?? null : rows; } async function updateProfileLocationInSupabase(profileId, country, city) { if (!hasSupabaseRuntime() || !profileId) return; const payload = { country, city }; if (supabaseClient) { const { error } = await withSupabaseTimeout( supabaseClient.from("profiles").update(payload).eq("id", profileId), "Updating profile location", supabaseProfileSaveTimeoutMs ); if (error) throw error; return; } await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/profiles?id=eq.${profileId}`, { method: "PATCH", body: payload, headers: { Prefer: "return=minimal" } }), "Updating profile location", supabaseProfileSaveTimeoutMs ); } async function updateRiderApplicationLocationInSupabase(riderId, area) { if (!hasSupabaseRuntime() || !riderId) return; const payload = { operating_area: area }; if (supabaseClient) { const { error } = await withSupabaseTimeout( supabaseClient.from("rider_applications").update(payload).eq("rider_id", riderId), "Updating rider operating area", supabaseProfileSaveTimeoutMs ); if (error) throw error; return; } await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rider_applications?rider_id=eq.${riderId}`, { method: "PATCH", body: payload, headers: { Prefer: "return=minimal" } }), "Updating rider operating area", supabaseProfileSaveTimeoutMs ); } async function updateRiderApplicationDocumentsInSupabase(riderId, documents) { if (!hasSupabaseRuntime() || !riderId) return; const payload = { document_path: riderDocumentPayload(documents) }; if (supabaseClient) { const { error } = await withSupabaseTimeout( supabaseClient.from("rider_applications").update(payload).eq("rider_id", riderId), "Updating rider application documents", supabaseProfileSaveTimeoutMs ); if (error) throw error; return; } await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rider_applications?rider_id=eq.${riderId}`, { method: "PATCH", body: payload, headers: { Prefer: "return=minimal" } }), "Updating rider application documents", supabaseProfileSaveTimeoutMs ); } async function updatePassengerCurrentCityInSupabase(profileId, country, city) { if (!hasSupabaseRuntime() || !profileId) return; if (!locationUpdateRpcUnavailable.passenger) { try { await callSupabaseRpc( "passenger_update_current_city", { p_country: country, p_city: city }, "Updating passenger city", supabaseProfileSaveTimeoutMs ); lastLocationUpdateSource = "location update RPC"; return; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; locationUpdateRpcUnavailable.passenger = true; logClientWarning("Passenger location RPC is not installed yet. Falling back to direct profile update.", error); } } assertClientFallbackAllowed("Passenger location update", "supabase-location-update-rpc.sql"); lastLocationUpdateSource = "direct location update fallback"; await updateProfileLocationInSupabase(profileId, country, city); } async function updateRiderCurrentAreaInSupabase(riderId, country, city, area) { if (!hasSupabaseRuntime() || !riderId) return; if (!locationUpdateRpcUnavailable.rider) { try { await callSupabaseRpc( "rider_update_current_area", { p_country: country, p_city: city, p_area: area }, "Updating rider area", supabaseProfileSaveTimeoutMs ); lastLocationUpdateSource = "location update RPC"; return; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; locationUpdateRpcUnavailable.rider = true; logClientWarning("Rider location RPC is not installed yet. Falling back to direct profile and rider location updates.", error); } } assertClientFallbackAllowed("Rider location update", "supabase-location-update-rpc.sql"); lastLocationUpdateSource = "direct location update fallback"; await updateProfileLocationInSupabase(riderId, country, city); await updateRiderApplicationLocationInSupabase(riderId, area); } async function updateRiderLiveGpsWithRpc(rider) { const currentGps = riderCurrentGps(rider); if (!currentGps) return false; await callSupabaseRpc( "rider_update_live_gps", { p_lat: currentGps.latitude, p_lng: currentGps.longitude, p_accuracy_meters: currentGps.accuracyMeters ?? null, p_captured_at: currentGps.capturedAt ?? null }, "Updating rider live GPS", optionalSupabaseRequestTimeoutMs ); lastLocationUpdateSource = "location update RPC"; void recordInsuranceTelemetryPointInSupabase(rider, currentGps, "rider_live_gps"); return true; } async function clearRiderLiveGpsWithRpc() { await callSupabaseRpc( "rider_clear_live_gps", {}, "Stopping rider live GPS", optionalSupabaseRequestTimeoutMs ); lastLocationUpdateSource = "location update RPC"; void closeInsuranceTelemetrySegmentInSupabase("rider_clear_live_gps"); return true; } function insuranceTelemetryEnabled() { return configFlagEnabled(appConfig.insuranceTelemetryEnabled ?? true); } async function recordInsuranceTelemetryPointInSupabase(rider, gpsPoint, eventType = "rider_live_gps") { if (!hasSupabaseRuntime() || !insuranceTelemetryEnabled() || insuranceTelemetryRpcUnavailable) return null; const point = normalizeGpsPoint(gpsPoint); if (!rider?.id || !point) return null; try { const segment = await callSupabaseRpcResult( "record_insurance_telemetry_point", { p_ride_request_id: null, p_period: null, p_lat: point.latitude, p_lng: point.longitude, p_accuracy_meters: point.accuracyMeters ?? null, p_captured_at: point.capturedAt ?? null, p_event_type: eventType }, "Recording insurance telemetry", optionalSupabaseRequestTimeoutMs ); lastInsuranceTelemetrySource = "insurance telemetry RPC"; return Array.isArray(segment) ? segment[0] ?? null : segment; } catch (error) { if (adminDirectoryRpcMissing(error)) { insuranceTelemetryRpcUnavailable = true; lastInsuranceTelemetrySource = "insurance telemetry RPC missing"; logClientWarning("Insurance telemetry RPC is not installed yet. Rider GPS still synced, but active-mile reporting is not production ready.", error); return null; } lastInsuranceTelemetrySource = "insurance telemetry warning"; logClientWarning("Insurance telemetry point could not be recorded.", error); return null; } } async function closeInsuranceTelemetrySegmentInSupabase(eventType = "manual_close") { if (!hasSupabaseRuntime() || !insuranceTelemetryEnabled() || insuranceTelemetryRpcUnavailable) return null; try { const result = await callSupabaseRpcResult( "close_insurance_telemetry_segment", { p_event_type: eventType }, "Closing insurance telemetry", optionalSupabaseRequestTimeoutMs ); lastInsuranceTelemetrySource = "insurance telemetry RPC"; return result ?? true; } catch (error) { if (adminDirectoryRpcMissing(error)) { insuranceTelemetryRpcUnavailable = true; lastInsuranceTelemetrySource = "insurance telemetry RPC missing"; logClientWarning("Insurance telemetry close RPC is not installed yet.", error); return null; } lastInsuranceTelemetrySource = "insurance telemetry warning"; logClientWarning("Insurance telemetry segment could not be closed.", error); return null; } } async function recordInsuranceTelemetryTransitionInSupabase(request, actionName, gpsPoint = null) { if (!hasSupabaseRuntime() || !insuranceTelemetryEnabled() || insuranceTelemetryRpcUnavailable) return null; if (!request?.id || !actionName) return null; const point = normalizeGpsPoint(gpsPoint); try { const segment = await callSupabaseRpcResult( "record_insurance_telemetry_transition", { p_request_id: request.id, p_action_name: actionName, p_lat: point?.latitude ?? null, p_lng: point?.longitude ?? null, p_accuracy_meters: point?.accuracyMeters ?? null, p_captured_at: point?.capturedAt ?? null }, "Recording insurance lifecycle telemetry", optionalSupabaseRequestTimeoutMs ); lastInsuranceTelemetrySource = "insurance telemetry RPC"; return Array.isArray(segment) ? segment[0] ?? null : segment; } catch (error) { if (adminDirectoryRpcMissing(error)) { insuranceTelemetryRpcUnavailable = true; lastInsuranceTelemetrySource = "insurance telemetry RPC missing"; logClientWarning("Insurance lifecycle telemetry RPC is not installed yet.", error); return null; } lastInsuranceTelemetrySource = "insurance telemetry warning"; logClientWarning("Insurance lifecycle telemetry could not be recorded.", error); return null; } } async function clearRiderLiveGpsInSupabase(rider) { if (!hasSupabaseRuntime() || !rider?.id) return; if (!locationUpdateRpcUnavailable.clearLiveGps) { try { const didClear = await clearRiderLiveGpsWithRpc(); if (didClear) return; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; locationUpdateRpcUnavailable.clearLiveGps = true; logClientWarning("Rider clear live GPS RPC is not installed yet. Falling back to direct rider location upsert.", error); } } assertClientFallbackAllowed("Rider live GPS clearing", "supabase-location-update-rpc.sql"); await updateRiderLocationPresenceInSupabase(clearRiderLiveGpsFields(rider)); } async function expireRiderLiveGpsIfNeeded() { const rider = currentRiderRecord(); if (!riderLiveGpsNeedsClearing(rider)) return false; const ageMinutes = typeof riderLiveGpsAgeMinutes === "function" ? riderLiveGpsAgeMinutes(rider) : null; const inactivityLimit = Number(typeof riderAvailabilityInactivityTimeoutMinutes !== "undefined" ? riderAvailabilityInactivityTimeoutMinutes : 8 * 60); const longInactive = ageMinutes != null && Number.isFinite(ageMinutes) && ageMinutes >= inactivityLimit; if (state.riderAvailabilityActivated === true && !longInactive) { if (activeRole() === "rider") { els.riderGpsStatus.textContent = "Activated. Waka is waiting for a fresh location update before nearby ride requests appear."; } return false; } const clearedRider = clearRiderLiveGpsFields(rider); if (longInactive && state.riderAvailabilityActivated === true) { state.riderAvailabilityActivated = false; } saveCurrentRiderRecord(clearedRider); await clearRiderLiveGpsInSupabase(clearedRider); if (activeRole() === "rider") { els.riderGpsStatus.textContent = longInactive ? "Offline after a long period without a fresh location. Activate when you are ready to receive requests again." : "Live GPS expired or became inaccurate; activate again before receiving requests."; } return true; } async function updateRiderApplicationReviewInSupabase(riderId, status, reviewedAt, reviewNote = null) { const payload = { status, reviewed_by: state.adminSession.userId, reviewed_at: reviewedAt, review_note: status === "needs_correction" ? reviewNote : null }; if (supabaseClient) { const { error } = await supabaseClient .from("rider_applications") .update(payload) .eq("rider_id", riderId); if (error) throw error; return; } await supabaseRestRequest(`/rest/v1/rider_applications?rider_id=eq.${riderId}`, { method: "PATCH", body: payload, headers: { Prefer: "return=minimal" } }); } async function updateRiderLocationPresenceInSupabase(rider) { if (!hasSupabaseRuntime() || !rider?.id) return; const currentGps = riderCurrentGps(rider); if (currentGps && !locationUpdateRpcUnavailable.liveGps) { try { const didUpdate = await updateRiderLiveGpsWithRpc(rider); if (didUpdate) return; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; locationUpdateRpcUnavailable.liveGps = true; logClientWarning("Rider live GPS RPC is not installed yet. Falling back to direct rider location upsert.", error); } } const location = gpsPointToDatabase(currentGps); const payload = { rider_id: rider.id, city: rider.city, area_label: rider.area, is_online: Boolean(currentGps && rider.status === "approved" && isSubscriptionActive(rider)), updated_at: new Date().toISOString() }; payload.location = location; payload.accuracy_meters = currentGps?.accuracyMeters ?? null; payload.captured_at = currentGps?.capturedAt ?? null; try { assertClientFallbackAllowed("Rider live GPS update", "supabase-location-update-rpc.sql"); lastLocationUpdateSource = "direct rider location upsert fallback"; if (supabaseClient) { await withSupabaseTimeout( supabaseClient.from("rider_locations").upsert(payload, { onConflict: "rider_id" }), "Updating rider live location", optionalSupabaseRequestTimeoutMs ); void recordInsuranceTelemetryPointInSupabase(rider, currentGps, "rider_live_gps_fallback"); return; } await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rider_locations?on_conflict=rider_id", { method: "POST", body: payload, headers: { Prefer: "resolution=merge-duplicates,return=minimal" } }), "Updating rider live location", optionalSupabaseRequestTimeoutMs ); void recordInsuranceTelemetryPointInSupabase(rider, currentGps, "rider_live_gps_fallback"); } catch (error) { logClientWarning("Rider live location was not updated.", error); } } function reportSupabaseStep(onStage, message) { if (typeof onStage === "function") onStage(message); } function pause(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function isMissingAuthSessionError(error) { return Boolean(error?.message && /auth session missing|session.*missing/i.test(error.message)); } function isMissingJwtUserError(error) { return Boolean(error?.message && /user from sub claim in jwt does not exist/i.test(error.message)); } function isInvalidLoginCredentialsError(error) { return Boolean(error?.message && /invalid login credentials/i.test(error.message)); } function isAlreadyRegisteredError(error) { return Boolean(error?.message && /already|registered|exists/i.test(error.message)); } function authUserEmail(user) { return user?.email?.toLowerCase?.() ?? ""; } function authUserMatchesVerifiedPhone(user, profile) { return Boolean(user?.phone && profile?.phone && phoneMatches(user.phone, profile.phone)); } async function attachEmailPasswordToVerifiedPhoneUser(user, profile, onStage) { if (!authUserMatchesVerifiedPhone(user, profile)) return user; const updates = {}; if (profile.email && authUserEmail(user) !== profile.email) updates.email = profile.email; if (profile.password) updates.password = profile.password; if (!Object.keys(updates).length || !supabaseClient) return user; reportSupabaseStep(onStage, "Linking email and password to the verified phone account..."); const { data, error } = await withSupabaseTimeout( supabaseClient.auth.updateUser(updates), "Linking email/password to the verified phone account", supabaseProfileSaveTimeoutMs ); if (error) { logClientWarning("Phone was verified, but email/password setup still needs attention.", error); reportSupabaseStep(onStage, `Phone verified. Email/password sign-in still needs attention: ${error.message}`); return { ...user, emailSetupPending: true, emailSetupError: error.message }; } const updatedUser = data?.user ?? user; return { ...updatedUser, emailSetupPending: Boolean(updates.email && authUserEmail(updatedUser) !== profile.email) }; } function clearStoredSupabaseAuthSession() { try { Object.keys(localStorage) .filter((key) => /^sb-.+-auth-token$/.test(key)) .forEach((key) => localStorage.removeItem(key)); } catch (error) { logClientWarning("Stored Supabase session could not be cleared.", error); } } async function clearStaleSupabaseSession() { clearStoredSupabaseAuthSession(); try { await withSupabaseTimeout( supabaseClient.auth.signOut({ scope: "local" }), "Clearing the stale Supabase session", optionalSupabaseRequestTimeoutMs ); } catch (error) { logClientWarning("Stale Supabase session could not be signed out.", error); } } async function getSupabaseUser() { if (supabaseRestSession?.user && !supabaseClient) return supabaseRestSession.user; if (!isSupabaseMode()) return null; if (supabaseClient.auth.getSession) { const { data: sessionData, error: sessionError } = await withSupabaseTimeout( supabaseClient.auth.getSession(), "Reading the saved Supabase session", optionalSupabaseRequestTimeoutMs ); if (sessionError && !isMissingAuthSessionError(sessionError)) throw sessionError; if (sessionData?.session?.user) return sessionData.session.user; } const { data, error } = await withSupabaseTimeout( supabaseClient.auth.getUser(), "Checking the current Supabase session" ); if (isMissingAuthSessionError(error)) return null; if (isMissingJwtUserError(error)) { await clearStaleSupabaseSession(); return null; } if (error) throw error; return data?.user ?? null; } async function loadSupabaseProfileForUser(user, select = "*", timeoutMessage = "Loading the Waka profile") { if (!user?.id) return null; if (supabaseClient) { const { data, error } = await withSupabaseTimeout( supabaseClient .from("profiles") .select(select) .eq("id", user.id) .maybeSingle(), timeoutMessage, supabaseProfileSaveTimeoutMs ); if (error) throw error; return data; } return selectProfileRest(user.id, select, supabaseRestSession?.access_token); } async function savePhoneVerificationEvent(userId, phone, provider = "supabase-otp") { if (!isSupabaseMode() || !userId) return; try { const { error } = await withSupabaseTimeout( supabaseClient.from("phone_verification_events").insert({ user_id: userId, phone, provider }), "Saving the phone verification audit event", optionalSupabaseRequestTimeoutMs ); if (error) logClientWarning("Phone verification audit event was not saved.", error); } catch (error) { logClientWarning("Phone verification audit event was skipped.", error); } } function profileOnboardingRpcBody(profile, profilePhotoPath = null) { return { p_role: profile.role, p_full_name: profile.name, p_email: profile.email, p_phone: profile.phone, p_phone_verified_at: profile.phoneVerifiedAt, p_national_id_number: profile.nationalId, p_date_of_birth: profile.dateOfBirth, p_preferred_language: profile.preferredLanguage, p_country: profile.country, p_city: profile.city, p_profile_photo_path: profilePhotoPath, p_phone_verification_provider: profile.phoneVerificationProvider ?? "supabase-otp" }; } async function upsertProfileWithOnboardingRpc(profile, user, onStage, didRefreshSession = false) { reportSupabaseStep(onStage, didRefreshSession ? "Retrying profile onboarding RPC..." : "Syncing profile details through onboarding RPC..."); try { await callSupabaseRpc( "upsert_own_profile", profileOnboardingRpcBody(profile, profile.profilePhotoPath ?? null), "Saving the Waka profile", supabaseProfileSaveTimeoutMs ); lastProfileOnboardingSource = "profile onboarding RPC"; return user; } catch (error) { if (isMissingJwtUserError(error) && !didRefreshSession) { reportSupabaseStep(onStage, "Supabase session mismatch found. Refreshing sign-in..."); const refreshedUser = await refreshSupabaseSignIn(profile, onStage); await pause(800); return upsertProfileWithOnboardingRpc(profile, refreshedUser, onStage, true); } throw error; } } async function saveProfilePhotoPathWithRpc(profilePhotoPath) { await callSupabaseRpc( "save_profile_photo_path", { p_profile_photo_path: profilePhotoPath }, "Saving the profile photo path", supabaseProfileSaveTimeoutMs ); lastProfileOnboardingSource = "profile onboarding RPC"; } async function submitRiderApplicationWithRpc(rider, documentPath) { await callSupabaseRpc( "submit_rider_application", { p_vehicle: rider.vehicle, p_operating_area: rider.area, p_credential_number: rider.credential, p_vehicle_registration: rider.registration, p_car_make: rider.carMake, p_car_model: rider.carModel, p_car_body_type: normalizeCarBodyType(rider.carBodyType), p_vehicle_designation: normalizeRiderVehicleDesignation(rider.vehicleDesignation, rider.carBodyType), p_car_year: rider.carYear ? Number(rider.carYear) : null, p_car_color: rider.carColor, p_vehicle_vin: rider.vehicleVin, p_insurance_provider: rider.insuranceProvider, p_insurance_number: rider.insuranceNumber, p_driver_license_expires_on: rider.driverLicenseExpiresOn, p_insurance_expires_on: rider.insuranceExpiresOn, p_background_check_consent: Boolean(rider.backgroundCheckConsentAt), p_background_check_provider: rider.backgroundCheckProvider || appConfig.backgroundCheckProvider || "checkr", p_background_check_consent_version: rider.backgroundCheckConsentVersion || "maryland-2026-05", p_document_path: documentPath }, "Submitting the rider application", supabaseProfileSaveTimeoutMs ); lastProfileOnboardingSource = "profile onboarding RPC"; } function authRedirectPathForRole(role) { if (role === "rider") return "/rider"; if (role === "passenger") return "/passenger"; return "/"; } function authRedirectUrlForRole(role) { const path = authRedirectPathForRole(role); try { return new URL(path, window.location.origin).toString(); } catch { return path; } } function authSignupOptionsForProfile(profile) { return { emailRedirectTo: authRedirectUrlForRole(profile?.role), data: { role: profile?.role || "passenger", waka_onboarding: `${profile?.role || "passenger"}_account`, full_name: profile?.name || "" } }; } async function requestSupabaseSignupConfirmation(email, role = null, onStage = null) { if (!supabaseClient?.auth?.resend || !email) return false; try { reportSupabaseStep(onStage, "Requesting the Supabase confirmation email..."); const { error } = await withSupabaseTimeout( supabaseClient.auth.resend({ type: "signup", email, options: { emailRedirectTo: authRedirectUrlForRole(role) } }), "Requesting the Supabase confirmation email", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return true; } catch (error) { logClientWarning("Supabase confirmation email could not be requested.", error); return false; } } function emailConfirmationNeededMessage(role, confirmationRequested) { const accountLabel = role === "rider" ? "rider" : "passenger"; const confirmationText = confirmationRequested ? "I asked Supabase to send the confirmation email; check the inbox and spam folder." : "Supabase did not confirm that a confirmation email was sent. Check the Supabase Auth email settings or temporarily disable email confirmation during the pilot."; const riderApplicationText = role === "rider" ? " Waka cannot submit the rider application for admin review until Supabase lets this browser sign in." : ""; return `Supabase created the ${accountLabel} login, but email confirmation is still required before sign-in.${riderApplicationText} ${confirmationText}`; } async function ensureSupabaseAuthUser(profile, onStage, options = {}) { reportSupabaseStep(onStage, "Checking current Supabase session..."); const existingUser = await getSupabaseUser(); if (authUserMatchesVerifiedPhone(existingUser, profile)) { if (options.preventExistingAccount) { throw new Error("An account already exists with this phone number. Sign in with the existing account instead of creating a duplicate."); } return attachEmailPasswordToVerifiedPhoneUser(existingUser, profile, onStage); } if (authUserEmail(existingUser) === profile.email) { if (options.preventExistingAccount) { throw new Error("An account already exists with this email address. Sign in with the existing account instead of creating a duplicate."); } return existingUser; } if (existingUser) { reportSupabaseStep(onStage, "Switching Supabase user..."); await clearStaleSupabaseSession(); } if (!profile.email || !profile.password) { throw new Error("Enter an email and password to create the Supabase account."); } const credentials = { email: profile.email, password: profile.password }; reportSupabaseStep(onStage, "Checking for existing Supabase account..."); const signInFirst = await withSupabaseTimeout( supabaseClient.auth.signInWithPassword(credentials), "Checking for an existing Supabase account" ); if (!signInFirst.error && signInFirst.data?.user) { if (options.preventExistingAccount) { await clearStaleSupabaseSession(); throw new Error("An account already exists with this email address. Sign in with the existing account instead of creating a duplicate."); } return signInFirst.data.user; } if (signInFirst.error && !isInvalidLoginCredentialsError(signInFirst.error)) { throw signInFirst.error; } reportSupabaseStep(onStage, "Creating Supabase auth account..."); const signUpResult = await withSupabaseTimeout( supabaseClient.auth.signUp({ ...credentials, options: authSignupOptionsForProfile(profile) }), "Creating the Supabase auth account" ); if (isAlreadyRegisteredError(signUpResult.error) && options.preventExistingAccount) { throw new Error("An account already exists with this email address. Sign in with the existing account instead of creating a duplicate."); } if (signUpResult.error && !isAlreadyRegisteredError(signUpResult.error)) { throw signUpResult.error; } if (signUpResult.data?.session?.user) return signUpResult.data.session.user; reportSupabaseStep(onStage, "Signing in to Supabase..."); const signInResult = await withSupabaseTimeout( supabaseClient.auth.signInWithPassword(credentials), "Signing in to Supabase" ); if (signInResult.error) { if (isInvalidLoginCredentialsError(signInResult.error) && isAlreadyRegisteredError(signUpResult.error)) { const confirmationRequested = await requestSupabaseSignupConfirmation(profile.email, profile.role, onStage); throw new Error(`This email already has a Supabase login, but Supabase did not accept the sign-in yet. Waka will not create a duplicate account. ${confirmationRequested ? "I asked Supabase to resend the confirmation email; check the inbox and spam folder." : "Supabase did not confirm that a confirmation email was sent. Check the Supabase Auth email settings or temporarily disable email confirmation during the pilot."} After confirmation, sign in here and submit the rider application again.`); } if (isInvalidLoginCredentialsError(signInResult.error)) { if (signUpResult.data?.user && !signUpResult.data?.session) { const confirmationRequested = await requestSupabaseSignupConfirmation(profile.email, profile.role, onStage); throw new Error(emailConfirmationNeededMessage(profile.role, confirmationRequested)); } throw new Error("This email already has a Supabase login. Sign in with the existing password first; Waka will open the correct profile or application form."); } throw new Error(`${signInResult.error.message}. If email confirmation is enabled, confirm the email first or disable email confirmation during the pilot.`); } return signInResult.data.user; } async function refreshSupabaseSignIn(profile, onStage) { if (!profile.email || !profile.password) { throw new Error("Supabase session is stale. Enter the email and password again, then save."); } reportSupabaseStep(onStage, "Refreshing Supabase sign-in..."); await clearStaleSupabaseSession(); const { data, error } = await withSupabaseTimeout( supabaseClient.auth.signInWithPassword({ email: profile.email, password: profile.password }), "Refreshing Supabase sign-in" ); if (error) throw error; if (!data?.user) throw new Error("Supabase sign-in refreshed, but no user was returned."); return data.user; } async function upsertProfilePayload(payload, profile, user, onStage, didRefreshSession = false) { reportSupabaseStep(onStage, didRefreshSession ? "Retrying profile sync..." : "Syncing profile details in background..."); const { error } = await withSupabaseTimeout( supabaseClient.from("profiles").upsert(payload, { onConflict: "id" }), "Saving the profile row", supabaseProfileSaveTimeoutMs ); if (error && isMissingJwtUserError(error) && !didRefreshSession) { reportSupabaseStep(onStage, "Supabase session mismatch found. Refreshing sign-in..."); const refreshedUser = await refreshSupabaseSignIn(profile, onStage); await pause(800); return upsertProfilePayload({ ...payload, id: refreshedUser.id }, profile, refreshedUser, onStage, true); } if (error) throw error; lastProfileOnboardingSource = "direct profile upsert fallback"; return user; } async function syncProfileDetailsToSupabase(profile, user, onStage) { const payload = { id: user.id, role: profile.role, full_name: profile.name, email: profile.email, phone: profile.phone, phone_verified_at: profile.phoneVerifiedAt, national_id_number: profile.nationalId, date_of_birth: profile.dateOfBirth, preferred_language: profile.preferredLanguage, country: profile.country, city: profile.city }; if (profile.profilePhotoPath) { payload.profile_photo_path = profile.profilePhotoPath; } let syncedUser = null; if (!profileOnboardingRpcUnavailable.profile) { try { syncedUser = await upsertProfileWithOnboardingRpc(profile, user, onStage); } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; profileOnboardingRpcUnavailable.profile = true; logClientWarning("Profile onboarding RPC is not installed yet. Falling back to direct profile upsert.", error); } } if (!syncedUser) { reportSupabaseStep(onStage, "Profile onboarding RPC unavailable; using policy-protected profile save..."); syncedUser = await upsertProfilePayload(payload, profile, user, onStage); savePhoneVerificationEvent(syncedUser.id, profile.phone, profile.phoneVerificationProvider ?? "supabase-otp"); } queueProfilePhotoUpload(syncedUser.id, profile.role, profilePhotoInput(profile.role)?.files[0] ?? null); return syncedUser; } function profileRecoveryDraftFromLocalAccount(type, user) { if (!["passenger", "rider"].includes(type)) return null; const account = state[type]; const email = authUserEmail(user); if (!account || (email && account.email && String(account.email).toLowerCase() !== email)) return null; const country = account.country || defaultLaunchCountry(); const city = account.city || defaultLaunchCity(country); const draft = { ...account, role: type, name: account.name || email.split("@")[0] || (type === "rider" ? "Rider" : "Passenger"), email: account.email || email, phone: account.phone || user?.phone || "", phoneVerifiedAt: account.phoneVerifiedAt || user?.phone_confirmed_at || (account.phoneVerified ? new Date().toISOString() : null), nationalId: account.nationalId || account.credential || "", dateOfBirth: account.dateOfBirth || null, preferredLanguage: account.preferredLanguage || state.language, country, city, profilePhotoPath: account.profilePhotoPath ?? null, phoneVerificationProvider: account.phoneVerificationProvider ?? "supabase-auth-recovery" }; if (!draft.name || !draft.email || !draft.phone || !draft.phoneVerifiedAt || !draft.country || !draft.city) return null; if (type === "rider" && (!draft.nationalId || !draft.dateOfBirth)) return null; return draft; } async function recoverMissingSupabaseProfileFromLocalAccount(type, user, onStage = null) { const draft = profileRecoveryDraftFromLocalAccount(type, user); if (!draft) return null; reportSupabaseStep(onStage, "Supabase sign-in worked. Re-syncing the missing Waka profile from this device..."); const syncedUser = await syncProfileDetailsToSupabase(draft, user, onStage); return loadSupabaseProfileForUser(syncedUser, "*", `Reloading the repaired ${type} profile`); } async function saveProfileToSupabase(profile, onStage, options = {}) { if (!isSupabaseMode()) return null; let user = await ensureSupabaseAuthUser(profile, onStage, options); if (!user) throw new Error("Supabase account could not be created or signed in."); reportSupabaseStep(onStage, options.waitForProfile ? `Creating Waka ${profile.role} profile before reporting success...` : "Account created. Finishing setup in the background..."); const profileSyncPromise = syncProfileDetailsToSupabase(profile, user, onStage) .then((syncedUser) => { reportSupabaseStep(onStage, `${profile.role === "rider" ? "Rider" : "Passenger"} account created and profile synced.`); return syncedUser; }) .catch((error) => { reportSupabaseStep(onStage, `${profile.role === "rider" ? "Rider" : "Passenger"} account created. Profile sync still needs attention: ${error.message}`); logClientWarning("Profile sync was not completed.", error); if (options.waitForProfile) throw error; return user; }); if (options.waitForProfile) { user = await profileSyncPromise; } return { ...user, profilePhotoPath: profile.profilePhotoPath ?? null, profileSyncPromise }; } function queueProfilePhotoUpload(userId, type, file) { if (!isSupabaseMode() || !file) return; uploadProfilePhoto(userId, type, file) .then(async (profilePhotoPath) => { if (!profilePhotoPath) return; if (type === "rider" && state.rider?.id === userId) { state.rider = { ...state.rider, profilePhotoPath }; state.riders = upsertById(state.riders, state.rider); saveState(); renderAll(); } if (type === "passenger" && state.passenger?.id === userId) { state.passenger = { ...state.passenger, profilePhotoPath }; state.passengers = upsertById(state.passengers, state.passenger); saveState(); renderAll(); } if (!profileOnboardingRpcUnavailable.photo) { try { await saveProfilePhotoPathWithRpc(profilePhotoPath); return; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; profileOnboardingRpcUnavailable.photo = true; logClientWarning("Profile photo RPC is not installed yet. Falling back to direct profile update.", error); } } assertClientFallbackAllowed("Profile photo path save", "supabase-profile-onboarding-rpc.sql"); lastProfileOnboardingSource = "direct profile photo update fallback"; const { error } = await withSupabaseTimeout( supabaseClient.from("profiles").update({ profile_photo_path: profilePhotoPath }).eq("id", userId), "Saving the profile photo path" ); if (error) throw error; }) .catch((error) => { logClientWarning("Profile photo upload was skipped.", error); }); } function profilePhotoInput(type) { return type === "rider" ? els.riderPhoto : els.passengerPhoto; } async function uploadProfilePhoto(userId, type, file) { if (!isSupabaseMode() || !file) return null; const safeName = file.name.replace(/[^a-z0-9._-]/gi, "-").toLowerCase(); const path = `${userId}/${type}-${Date.now()}-${safeName}`; const { error } = await withSupabaseTimeout( supabaseClient.storage .from(appConfig.buckets.profilePhotos) .upload(path, file, { upsert: false }), "Uploading the profile photo" ); if (error) throw error; return path; } async function uploadRiderDocument(userId, documentType, file) { if (!isSupabaseMode() || !file) return null; const safeName = file.name.replace(/[^a-z0-9._-]/gi, "-").toLowerCase(); const path = `${userId}/${documentType}-${Date.now()}-${safeName}`; const { error } = await withSupabaseTimeout( supabaseClient.storage .from(appConfig.buckets.riderDocuments) .upload(path, file, { upsert: false }), `Uploading the ${riderDocumentLabels[documentType]}` ); if (error) throw error; return path; } async function uploadRiderDocuments(userId) { const files = selectedRiderDocumentFiles(); const entries = await Promise.all(Object.entries(files).map(async ([documentType, file]) => { return [documentType, await uploadRiderDocument(userId, documentType, file)]; })); return Object.fromEntries(entries); } async function submitRiderComplianceRenewalToSupabase(rider, renewal) { const riderId = rider?.id ?? state.sessions?.rider?.userId; if (!riderId) throw new Error("Rider sign-in is required before uploading renewed documents."); const driverLicenseDocumentPath = renewal.driverLicenseFile ? await uploadRiderDocument(riderId, "driverLicense", renewal.driverLicenseFile) : null; const insuranceDocumentPath = renewal.insuranceFile ? await uploadRiderDocument(riderId, "insurance", renewal.insuranceFile) : null; const documents = { ...riderDocuments(rider), ...(driverLicenseDocumentPath ? { driverLicense: driverLicenseDocumentPath } : {}), ...(insuranceDocumentPath ? { insurance: insuranceDocumentPath } : {}) }; if (!hasSupabaseRuntime()) { return { application: { status: rider?.status === "suspended" ? "pending" : rider?.status, review_note: rider?.status === "suspended" ? "Renewed compliance documents submitted for admin review." : rider?.reviewNote ?? "", driver_license_expires_on: renewal.driverLicenseExpiresOn || rider?.driverLicenseExpiresOn || null, insurance_expires_on: renewal.insuranceExpiresOn || rider?.insuranceExpiresOn || null, document_path: riderDocumentPayload(documents) }, documents }; } if (profileOnboardingRpcUnavailable.riderComplianceRenewal) { throw new Error("Rider compliance renewal backend is not installed yet. Run supabase-rider-compliance-expirations.sql."); } try { const data = await callSupabaseRpc( "submit_rider_compliance_renewal", { p_driver_license_expires_on: renewal.driverLicenseExpiresOn || null, p_driver_license_document_path: driverLicenseDocumentPath, p_insurance_expires_on: renewal.insuranceExpiresOn || null, p_insurance_document_path: insuranceDocumentPath }, "Submitting renewed rider documents", supabaseProfileSaveTimeoutMs + optionalSupabaseRequestTimeoutMs ); const application = Array.isArray(data) ? data[0] : data; return { application, documents }; } catch (error) { if (adminDirectoryRpcMissing(error)) { profileOnboardingRpcUnavailable.riderComplianceRenewal = true; throw new Error("Rider compliance renewal backend is not installed yet. Run supabase-rider-compliance-expirations.sql."); } throw error; } } function riderOnboardingSubmitFunctionName() { return String(appConfig.riderOnboardingSubmitFunctionName || "rider-onboarding-submit").trim() || "rider-onboarding-submit"; } function passengerOnboardingSubmitFunctionName() { return String(appConfig.passengerOnboardingSubmitFunctionName || "passenger-onboarding-submit").trim() || "passenger-onboarding-submit"; } function passwordResetRequestFunctionName() { return String(appConfig.passwordResetRequestFunctionName || "password-reset-request").trim() || "password-reset-request"; } function passwordResetPhoneOtpFunctionName() { return String(appConfig.passwordResetPhoneOtpFunctionName || "password-reset-phone-otp").trim() || "password-reset-phone-otp"; } function passwordResetCompleteFunctionName() { return String(appConfig.passwordResetCompleteFunctionName || "password-reset-complete").trim() || "password-reset-complete"; } function runtimeAllowsTestingRelaxation() { const projectName = String(appConfig.projectName || "").toLowerCase(); const hostname = String(window.location?.hostname || "").toLowerCase(); return /\b(staging|stage|pilot|test|preview|local)\b/.test(projectName) || hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "staging.wakagood.com" || hostname.endsWith(".pages.dev"); } function passwordResetPhoneOtpRelaxedForTesting() { return configFlagEnabled(appConfig.relaxPasswordResetPhoneOtpForTesting) && runtimeAllowsTestingRelaxation(); } function passwordResetPhoneOtpRequired() { return configFlagEnabled(appConfig.passwordResetPhoneOtpRequired ?? true) && !passwordResetPhoneOtpRelaxedForTesting(); } function passengerOnboardingPayload(passenger) { return { role: "passenger", name: passenger.name, email: passenger.email, password: passenger.password, phone: passenger.phone, phoneVerifiedAt: passenger.phoneVerifiedAt, phoneVerificationProvider: passenger.phoneVerificationProvider, accountUse: passenger.accountUse, nationalId: passenger.nationalId, dateOfBirth: passenger.dateOfBirth, preferredLanguage: passenger.preferredLanguage, country: passenger.country, city: passenger.city }; } async function submitNewPassengerOnboardingToSupabase(passenger, onStage = null) { if (!hasSupabaseRuntime()) return null; reportSupabaseStep(onStage, "Submitting passenger account through secure onboarding..."); const form = new FormData(); form.append("payload", JSON.stringify(passengerOnboardingPayload(passenger))); const profilePhoto = profilePhotoInput("passenger")?.files?.[0] ?? null; if (profilePhoto) form.append("profilePhoto", profilePhoto, profilePhoto.name); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${passengerOnboardingSubmitFunctionName()}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, Authorization: `Bearer ${appConfig.supabaseAnonKey}` }, body: form }), "Submitting the passenger onboarding account", supabaseProfileSaveTimeoutMs + optionalSupabaseRequestTimeoutMs ); const text = await response.text(); let payload = null; if (text) { try { payload = JSON.parse(text); } catch { payload = { error: text }; } } if (!response.ok) { throw new Error(payload?.error || payload?.message || text || `Passenger onboarding failed with HTTP ${response.status}.`); } if (!payload?.userId) throw new Error("Passenger onboarding completed without a passenger account id."); return payload; } function riderOnboardingPayload(rider) { return { role: "rider", name: rider.name, email: rider.email, password: rider.password, phone: rider.phone, phoneVerifiedAt: rider.phoneVerifiedAt, phoneVerificationProvider: rider.phoneVerificationProvider, nationalId: rider.nationalId, dateOfBirth: rider.dateOfBirth, preferredLanguage: rider.preferredLanguage, country: rider.country, city: rider.city, area: rider.area, credential: rider.credential, registration: rider.registration, carMake: rider.carMake, carModel: rider.carModel, carBodyType: normalizeCarBodyType(rider.carBodyType), vehicleDesignation: normalizeRiderVehicleDesignation(rider.vehicleDesignation, rider.carBodyType), navigationPreference: riderNavigationPreference(rider), carYear: rider.carYear ? Number(rider.carYear) : null, carColor: rider.carColor, vehicleVin: rider.vehicleVin, insuranceProvider: rider.insuranceProvider, insuranceNumber: rider.insuranceNumber, driverLicenseExpiresOn: rider.driverLicenseExpiresOn, insuranceExpiresOn: rider.insuranceExpiresOn, backgroundCheckConsent: Boolean(rider.backgroundCheckConsentAt), backgroundCheckProvider: rider.backgroundCheckProvider || appConfig.backgroundCheckProvider || "checkr", backgroundCheckConsentVersion: rider.backgroundCheckConsentVersion || "maryland-2026-05" }; } function appendRiderOnboardingFile(form, key, file) { if (file) form.append(key, file, file.name); } async function submitNewRiderOnboardingToSupabase(rider, onStage = null) { if (!hasSupabaseRuntime()) return null; reportSupabaseStep(onStage, "Submitting rider application through secure onboarding..."); const form = new FormData(); form.append("payload", JSON.stringify(riderOnboardingPayload(rider))); const files = selectedRiderDocumentFiles(); Object.entries(files).forEach(([key, file]) => appendRiderOnboardingFile(form, key, file)); appendRiderOnboardingFile(form, "profilePhoto", profilePhotoInput("rider")?.files?.[0] ?? null); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${riderOnboardingSubmitFunctionName()}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, Authorization: `Bearer ${appConfig.supabaseAnonKey}` }, body: form }), "Submitting the rider onboarding application", supabaseProfileSaveTimeoutMs + optionalSupabaseRequestTimeoutMs ); const text = await response.text(); let payload = null; if (text) { try { payload = JSON.parse(text); } catch { payload = { error: text }; } } if (!response.ok) { throw new Error(payload?.error || payload?.message || text || `Rider onboarding failed with HTTP ${response.status}.`); } if (!payload?.userId) throw new Error("Rider onboarding completed without a rider account id."); return payload; } function storagePathCanBeSigned(path) { return Boolean(path && typeof path === "string" && path.includes("/")); } function storageReviewButton(label, bucket, path) { if (!storagePathCanBeSigned(path)) return ""; return ``; } async function openSignedStorageFile(bucket, path, label) { const signedInUser = state.adminSession || state.sessions.rider || state.sessions.passenger; if (!signedInUser) { if (els.adminStatus) els.adminStatus.textContent = "Sign in before opening stored files."; return; } if (!isSupabaseMode()) { els.adminStatus.textContent = "Secure file viewing is available after Supabase sign-in."; return; } if (!storagePathCanBeSigned(path)) { els.adminStatus.textContent = `${label} is stored as a file name only. New Supabase uploads can be opened securely from here.`; return; } let pendingWindow = null; try { els.adminStatus.textContent = `Creating secure link for ${label}...`; pendingWindow = window.open("", "_blank", "noopener,noreferrer"); if (pendingWindow) { pendingWindow.document.title = `Opening ${label}`; pendingWindow.document.body.textContent = `Opening secure Waka ${label} link...`; } const { data, error } = await withSupabaseTimeout( supabaseClient.storage.from(bucket).createSignedUrl(path, 300), `Creating secure link for ${label}`, optionalSupabaseRequestTimeoutMs ); if (error) throw error; if (pendingWindow) pendingWindow.location.href = data.signedUrl; const opened = pendingWindow || window.open(data.signedUrl, "_blank", "noopener,noreferrer"); els.adminStatus.textContent = opened ? `Opened secure 5-minute link for ${label}.` : `Secure link created for ${label}, but the browser blocked the new tab.`; if (state.adminSession) { void logAdminAudit("admin_open_storage_file", "profiles", state.adminDetail?.id ?? null, { bucket, storage_path: path, file_label: label }); } } catch (error) { try { if (pendingWindow && !pendingWindow.closed) pendingWindow.close(); } catch {} if (els.adminStatus) els.adminStatus.textContent = `Could not open ${label}: ${error.message}`; } } async function saveRiderApplicationToSupabase(rider, userId) { if (!isSupabaseMode()) return; const uploadedDocuments = await uploadRiderDocuments(userId); const documents = { ...riderDocuments(rider), ...Object.fromEntries(Object.entries(uploadedDocuments).filter(([, value]) => Boolean(value))) }; documents.vehicleDesignation = normalizeRiderVehicleDesignation(rider.vehicleDesignation, rider.carBodyType); documents.navigationPreference = riderNavigationPreference(rider); const applicationPayload = { vehicle: rider.vehicle, operating_area: rider.area, credential_number: rider.credential, vehicle_registration: rider.registration, car_make: rider.carMake, car_model: rider.carModel, car_body_type: normalizeCarBodyType(rider.carBodyType), vehicle_designation: normalizeRiderVehicleDesignation(rider.vehicleDesignation, rider.carBodyType), car_year: rider.carYear ? Number(rider.carYear) : null, car_color: rider.carColor, vehicle_vin: rider.vehicleVin, insurance_provider: rider.insuranceProvider, insurance_number: rider.insuranceNumber, driver_license_expires_on: rider.driverLicenseExpiresOn, insurance_expires_on: rider.insuranceExpiresOn, background_check_consent_at: rider.backgroundCheckConsentAt, background_check_consent_provider: rider.backgroundCheckProvider || appConfig.backgroundCheckProvider || "checkr", background_check_consent_version: rider.backgroundCheckConsentVersion || "maryland-2026-05", document_path: riderDocumentPayload(documents) }; if (!profileOnboardingRpcUnavailable.riderApplication) { try { await submitRiderApplicationWithRpc(rider, applicationPayload.document_path); return documents; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; profileOnboardingRpcUnavailable.riderApplication = true; logClientWarning("Rider application RPC is not installed yet. Falling back to direct application write.", error); } } assertClientFallbackAllowed("Rider application submission", "supabase-profile-onboarding-rpc.sql"); const { data: existingApplication, error: lookupError } = await withSupabaseTimeout( supabaseClient .from("rider_applications") .select("id,status") .eq("rider_id", userId) .order("created_at", { ascending: false }) .limit(1) .maybeSingle(), "Checking for an existing rider application", supabaseProfileSaveTimeoutMs ); if (lookupError) throw lookupError; if (existingApplication?.id) { if (!["pending", "needs_correction", "declined"].includes(existingApplication.status)) { throw new Error("Approved or suspended rider applications can only be changed by admin support."); } lastProfileOnboardingSource = "direct rider application update fallback"; const { error: updateError } = await withSupabaseTimeout( supabaseClient.from("rider_applications").update({ ...applicationPayload, status: "pending", review_note: null, reviewed_by: null, reviewed_at: null }).eq("id", existingApplication.id), "Updating the existing rider application", supabaseProfileSaveTimeoutMs ); if (updateError) throw updateError; return documents; } lastProfileOnboardingSource = "direct rider application insert fallback"; const { error } = await withSupabaseTimeout( supabaseClient.from("rider_applications").insert({ rider_id: userId, ...applicationPayload }), "Saving the rider application", supabaseProfileSaveTimeoutMs ); if (error) throw error; return documents; } async function callSupabaseRpcResult(functionName, body, label, timeoutMs = optionalSupabaseRequestTimeoutMs) { if (!hasSupabaseRuntime()) return null; if (!supabaseClient) { return withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rpc/${functionName}`, { method: "POST", body }), label, timeoutMs ); } const { data, error } = await withSupabaseTimeout( supabaseClient.rpc(functionName, body), label, timeoutMs ); if (error) throw error; return data; } async function profileContactAvailability(email, phone, excludeUserId = null, role = null) { if (!hasSupabaseRuntime()) return { emailAvailable: true, phoneAvailable: true }; const lookupRole = strictRoleAuthIsolationEnabled() && normalizedPublicRole(role) ? null : role; const rows = await callSupabaseRpcResult( "profile_contact_available", { p_email: email, p_phone: phone, p_exclude_user_id: excludeUserId, p_role: lookupRole }, "Checking whether this email and phone are available", optionalSupabaseRequestTimeoutMs ); const result = Array.isArray(rows) ? rows[0] : rows; return { emailAvailable: result?.email_available !== false, phoneAvailable: result?.phone_available !== false }; } async function existingProfileRoleSetup(email, password, phone, role, availability = {}, onStage = null) { if (!hasSupabaseRuntime() || !role) return { action: "new" }; const normalizedEmail = String(email || "").trim().toLowerCase(); const duplicateEmail = availability.emailAvailable === false; const duplicatePhone = availability.phoneAvailable === false; if (!duplicateEmail && !duplicatePhone) return { action: "new" }; if (strictRoleAuthIsolationEnabled() && normalizedPublicRole(role)) { return { action: "blocked", reason: duplicateEmail ? "email" : duplicatePhone ? "phone" : "contact" }; } let user = await getSupabaseUser().catch((error) => { logClientWarning("Current Supabase user could not be checked before role setup.", error); return null; }); const signedInOwnsEmail = authUserEmail(user) === normalizedEmail; const signedInOwnsPhone = authUserMatchesVerifiedPhone(user, { phone }); if (!signedInOwnsEmail && !signedInOwnsPhone) { if (!normalizedEmail || !password || !supabaseClient?.auth?.signInWithPassword) { return { action: "blocked", reason: duplicatePhone ? "phone" : "contact" }; } reportSupabaseStep(onStage, "Signing in to the existing Waka login so passenger access can be added..."); const { data, error } = await withSupabaseTimeout( supabaseClient.auth.signInWithPassword({ email: normalizedEmail, password }), "Signing in to the existing Waka login", optionalSupabaseRequestTimeoutMs ); if (error) { if (isInvalidLoginCredentialsError(error)) return { action: "blocked", reason: "credentials" }; throw error; } user = data?.user ?? null; } if (!user) return { action: "blocked", reason: "contact" }; const profile = await loadSupabaseProfileForUser(user, "*", "Loading existing Waka profile for role setup").catch((error) => { logClientWarning("Existing Waka profile could not be loaded before role setup.", error); return null; }); if (duplicatePhone && profile?.phone && !phoneMatches(profile.phone, phone)) { return { action: "blocked", reason: "phone" }; } if (profile && await signedInProfileHasRole(profile, role)) { return { action: "existing_role", user, profile }; } return { action: "can_add_role", user, profile }; } function roleSetupBlockedMessage(role, reason = "contact") { const label = role === "rider" ? "rider" : "passenger"; const splitGuidance = `In this single Auth deployment, passenger and rider accounts need unique contact details until the passenger/rider Auth split is completed. Use a different email and phone for the ${label} account, or finish the two-project Auth setup before reusing this contact for both roles.`; if (reason === "credentials") { return `This email is already attached to a Waka login. ${splitGuidance}`; } if (reason === "phone") { return `This phone number is already attached to another Waka login. ${splitGuidance}`; } return `This email or phone is already attached to a Waka login. ${splitGuidance}`; } function rememberRoleSetupPhoneVerification(type, phone, profile) { if (!profile?.phone_verified_at || !phoneMatches(profile.phone, phone)) return false; state.verification[type] = { phone, phoneDigits: phoneDigits(phone), verifiedPhone: phone, verifiedAt: profile.phone_verified_at, provider: "existing-waka-profile" }; return true; } async function profileAvailabilityExcludeUserId(email, fallbackUserId = null) { if (!hasSupabaseRuntime()) return fallbackUserId; try { const user = await getSupabaseUser(); return authUserEmail(user) === String(email || "").trim().toLowerCase() ? user.id : fallbackUserId; } catch (error) { logClientWarning("Signed-in profile identity could not be checked before contact availability.", error); return fallbackUserId; } } function supabaseBooleanResult(value) { if (Array.isArray(value)) return supabaseBooleanResult(value[0]); if (typeof value === "boolean") return value; if (value && typeof value === "object") { const firstValue = Object.values(value)[0]; return firstValue === true || firstValue === "true"; } return value === true || value === "true"; } async function signedInProfileHasRole(profile, role) { if (!profile?.id || !role) return false; if (profile.role === role) return true; if (!hasSupabaseRuntime()) return false; try { const result = await callSupabaseRpcResult( "profile_has_role", { p_user_id: profile.id, p_role: role }, `Checking whether this account has ${role} access`, optionalSupabaseRequestTimeoutMs ); return supabaseBooleanResult(result); } catch (error) { logClientWarning("Profile role membership check was skipped.", error); return false; } } async function signedInProfileHasRiderApplication(profile) { if (!profile?.id || !hasSupabaseRuntime()) return false; try { if (supabaseClient) { const { data, error } = await withSupabaseTimeout( supabaseClient .from("rider_applications") .select("id,rider_id,status") .eq("rider_id", profile.id) .order("created_at", { ascending: false }) .limit(1), "Checking whether this account has an existing rider application", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return Array.isArray(data) && data.length > 0; } const params = new URLSearchParams(); params.set("rider_id", `eq.${profile.id}`); params.set("select", "id,rider_id,status"); params.set("order", "created_at.desc"); params.set("limit", "1"); const rows = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rider_applications?${params.toString()}`, { accessToken: supabaseRestSession?.access_token }), "Checking whether this account has an existing rider application", optionalSupabaseRequestTimeoutMs ); return Array.isArray(rows) && rows.length > 0; } catch (error) { logClientWarning("Existing rider application check was skipped.", error); return false; } } function uniqueWorkspaceRoleCandidates(candidates = []) { const roles = []; candidates.forEach((candidate) => { if (!["passenger", "rider"].includes(candidate)) return; if (!availableWorkspaceTab(candidate)) return; if (!roles.includes(candidate)) roles.push(candidate); }); return roles; } function roleRequestedByCurrentShell() { if (typeof publicHomePathIsActive === "function" && publicHomePathIsActive()) return null; if (typeof requestedTabFromLocation === "function") { const requested = requestedTabFromLocation(); if (["passenger", "rider"].includes(requested)) return requested; } if (["passenger", "rider"].includes(state.activeTab)) return state.activeTab; return null; } function keepPublicHomeVisibleForSessionRestore() { if (typeof publicHomePathIsActive !== "function" || !publicHomePathIsActive()) return false; if (typeof requestedTabFromLocation !== "function") return true; return !requestedTabFromLocation(); } function locallyRememberedWorkspaceRole(profile, role) { if (!profile?.id || !["passenger", "rider"].includes(role)) return false; const session = state.sessions?.[role]; const account = state[role]; return session?.userId === profile.id || account?.id === profile.id || account?.supabaseUserId === profile.id; } function workspaceRoleHasRestoreIntent(profile, role) { if (!profile?.id || !["passenger", "rider"].includes(role)) return false; return profile.role === role || locallyRememberedWorkspaceRole(profile, role); } function clearWorkspaceRoleSession(role) { if (!["passenger", "rider"].includes(role)) return; state.sessions[role] = null; state[role] = null; state.accountMode[role] = "signin"; if (role === "passenger") { state.passengerPage = "request"; state.passengerSelectedOfferId = null; } if (role === "rider") { state.riderPage = "overview"; state.riderAvailabilityActivated = false; } } async function signedInProfileCanRestoreWorkspaceRole(profile, role) { if (!profile?.id || !["passenger", "rider"].includes(role)) return false; if (!workspaceRoleHasRestoreIntent(profile, role)) return false; if (signedInProfileHasPrimaryRole(profile, role)) return true; if (strictRoleAuthIsolationEnabled()) return false; if (await signedInProfileHasRole(profile, role)) return true; if (role === "rider" && await signedInProfileHasRiderApplication(profile)) return true; return false; } async function pruneUnauthorizedWorkspaceSessionsForProfile(profile) { if (!profile?.id) return; for (const role of ["passenger", "rider"]) { if (!locallyRememberedWorkspaceRole(profile, role)) continue; if (await signedInProfileCanRestoreWorkspaceRole(profile, role)) continue; clearWorkspaceRoleSession(role); } } async function resolveSupabaseSessionWorkspaceRole(profile) { if (!profile?.id) return null; const storedRiderSession = state.sessions?.rider?.userId === profile.id ? "rider" : null; const storedPassengerSession = state.sessions?.passenger?.userId === profile.id ? "passenger" : null; const candidates = uniqueWorkspaceRoleCandidates([ roleRequestedByCurrentShell(), storedRiderSession, storedPassengerSession, profile.role ]); for (const role of candidates) { if (await signedInProfileCanRestoreWorkspaceRole(profile, role)) { return { role, needsApplication: false }; } } return null; } function profileForWorkspaceRole(profile, roleResolution) { if (!profile || !roleResolution?.role) return profile; const { role, needsApplication = false } = roleResolution; if (profile.role === role && !needsApplication) return profile; return { ...profile, primaryRole: profile.role, role, needsApplication, status: needsApplication ? "profile only" : profile.status }; } async function signedInProfileHasActiveAdminAssignment(profile) { if (!profile?.id || !hasSupabaseRuntime()) return false; try { if (!supabaseClient) { const params = new URLSearchParams(); params.set("admin_id", `eq.${profile.id}`); params.set("revoked_at", "is.null"); params.set("select", "admin_id"); params.set("limit", "1"); const rows = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/admin_role_assignments?${params.toString()}`), "Checking admin role assignments", optionalSupabaseRequestTimeoutMs ); return Array.isArray(rows) && rows.length > 0; } const { data, error } = await withSupabaseTimeout( supabaseClient .from("admin_role_assignments") .select("admin_id") .eq("admin_id", profile.id) .is("revoked_at", null) .limit(1), "Checking admin role assignments", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return Array.isArray(data) && data.length > 0; } catch (error) { logClientWarning("Admin role assignment check was skipped.", error); return false; } } async function signedInProfileIsAdminIdentity(profile) { if (!profile?.id) return false; if (profile.role === "admin") return true; if (await signedInProfileHasRole(profile, "admin")) return true; return signedInProfileHasActiveAdminAssignment(profile); } function mapSafetyReportFromDatabase(report, profileMap = new Map(), requestMap = new Map()) { const reporter = profileMap.get(report.reporter_id); const reportedUser = profileMap.get(report.reported_user_id); const request = requestMap.get(report.ride_request_id); return { id: report.id, requestId: report.ride_request_id, reporterId: report.reporter_id, reporterName: reporter?.full_name ?? reporter?.email ?? "Reporter", reporterRole: report.reporter_role, reportedUserId: report.reported_user_id, reportedUserName: reportedUser?.full_name ?? reportedUser?.email ?? "Unknown account", category: report.category, severity: report.severity, details: report.details, status: report.status, reviewedBy: report.reviewed_by, reviewedAt: report.reviewed_at, routeSummary: request ? `${request.pickupArea} to ${requestDestinationText(request)}` : `Ride ${report.ride_request_id}`, createdAt: report.created_at }; } function mapTaxDocumentFromDatabase(row, profileMap = new Map()) { const rider = profileMap.get(row.rider_id); return { id: row.id, riderId: row.rider_id, riderName: rider?.full_name ?? "Rider", taxYear: row.tax_year, documentType: row.document_type, provider: row.provider, storagePath: row.storage_path, providerDocumentId: row.provider_document_id ?? "", providerAccountId: row.provider_account_id ?? "", providerDocumentUrl: row.provider_document_url ?? "", deliveryMethod: row.delivery_method ?? (row.storage_path ? "private_storage" : "provider_portal"), filingStatus: row.filing_status ?? "", status: row.status, availableAt: row.available_at ?? null, filedAt: row.filed_at ?? null, issuedAt: row.issued_at ?? null, metadata: row.metadata ?? {}, createdAt: row.created_at, updatedAt: row.updated_at ?? row.created_at }; } function mapTaxIdentityReferenceFromDatabase(row, profileMap = new Map()) { const rider = profileMap.get(row.rider_id); return { id: row.id, riderId: row.rider_id, riderName: rider?.full_name ?? "Rider", provider: row.provider, providerSubjectId: maskProviderReference(row.provider_subject_id), status: row.tax_profile_status, tinLast4: row.tin_last4 ?? "", legalName: row.legal_name ?? "", businessName: row.business_name ?? "", taxClassification: row.tax_classification ?? "", lastVerifiedAt: row.last_verified_at ?? null, createdAt: row.created_at, updatedAt: row.updated_at }; } function mapRiderBackgroundCheckFromDatabase(row, profileMap = new Map()) { const rider = profileMap.get(row.rider_id); return { id: row.id, riderId: row.rider_id, riderName: rider?.full_name ?? "Rider", provider: row.provider, providerReference: row.provider_reference, status: row.status, decision: row.decision, summary: row.summary ?? "", completedAt: row.completed_at ?? null, createdAt: row.created_at }; } function mapRideRatingFromDatabase(row, profileMap = new Map()) { const rated = profileMap.get(row.rated_user_id); const reviewer = profileMap.get(row.reviewer_id); return { id: row.id, requestId: row.ride_request_id, reviewerId: row.reviewer_id, reviewerRole: row.reviewer_role, reviewerName: reviewer?.full_name ?? "Reviewer", ratedUserId: row.rated_user_id, ratedUserName: rated?.full_name ?? "Rated account", score: row.score, safetyScore: row.safety_score ?? row.score, punctualityScore: row.punctuality_score ?? row.score, communicationScore: row.communication_score ?? row.score, vehicleScore: row.vehicle_score ?? row.score, comment: row.comment ?? "", createdAt: row.created_at }; } function mapRiderRatingSummaryFromDatabase(row = {}) { const numberOrNull = (value) => { const numeric = Number(value); return Number.isFinite(numeric) ? numeric : null; }; return { riderId: row.rider_id ?? state.rider?.id ?? null, ratingCount: Number(row.rating_count ?? 0) || 0, overallPercent: numberOrNull(row.overall_percent), safetyPercent: numberOrNull(row.safety_percent), punctualityPercent: numberOrNull(row.punctuality_percent), communicationPercent: numberOrNull(row.communication_percent), vehiclePercent: numberOrNull(row.vehicle_percent), latestRatingAt: row.latest_rating_at ?? null }; } async function loadMyRiderRatingSummaryFromSupabase() { if (!hasSupabaseRuntime() || !state.rider?.id) { state.riderRatingSummary = null; return null; } try { const rows = await callSupabaseRpcResult( "my_rider_rating_summary", {}, "Loading anonymous rider rating summary", optionalSupabaseRequestTimeoutMs ); const summary = mapRiderRatingSummaryFromDatabase(Array.isArray(rows) ? rows[0] : rows); state.riderRatingSummary = summary; return summary; } catch (error) { if (typeof adminDirectoryRpcMissing === "function" && adminDirectoryRpcMissing(error)) { logClientWarning("Anonymous rider rating summary RPC is not installed yet.", error); state.riderRatingSummary = null; return null; } logClientWarning("Anonymous rider rating summary could not be loaded.", error); state.riderRatingSummary = null; return null; } } function riderApplicationErrorMessage(error) { if (/rider_applications_rider_id_fkey/i.test(error.message)) { return "Rider account was created, but the Waka profile row is missing, so the admin application could not be submitted. Use the same email/password and submit again after correcting any duplicate phone or driver's license values."; } if (/duplicate key|unique constraint/i.test(error.message)) { return "This phone number, driver's license, or rider application is already used by another Waka account. Use unique rider details or sign in with the existing account."; } return error.message; } function passengerAccountErrorMessage(error) { const message = String(error?.message || error || ""); if (/profiles_email|email|already exists with this email/i.test(message)) { return roleSetupBlockedMessage("passenger", "credentials"); } if (/profiles_phone|phone|already exists with this phone/i.test(message)) { return roleSetupBlockedMessage("passenger", "phone"); } if (/duplicate key|unique constraint/i.test(message)) { return roleSetupBlockedMessage("passenger", "contact"); } return message; } async function sendVerificationCode(type) { const phoneInput = type === "passenger" ? els.passengerPhone : els.riderPhone; const status = type === "passenger" ? els.passengerStatus : els.riderStatus; const phone = phoneInput.value.trim(); if (phone.length < 8) { setTranslatedStatus(status, "validPhoneRequired"); return; } const cooldownSeconds = phoneOtpCooldownSeconds(type, phone); if (cooldownSeconds > 0) { setTranslatedStatus(status, "phoneOtpCooldown", { seconds: cooldownSeconds }); return; } if (smsVerificationRelaxedForTesting()) { markSmsRelaxedPhoneVerified(type, phone, status); return; } if (usesManualPhoneVerification()) { markManualPhoneVerified(type, phone, status); return; } if (isSupabaseMode()) { startPhoneOtpCooldown(type, phone); setTranslatedStatus(status, "sendingVerificationCode"); const { error } = await supabaseClient.auth.signInWithOtp({ phone }); if (error) { if (error.status !== 429 && !/rate limit|too many/i.test(error.message)) clearPhoneOtpCooldown(type, phone); status.textContent = phoneOtpErrorMessage(error); return; } state.verification[type] = { phone, phoneDigits: phoneDigits(phone), verifiedPhone: null, provider: "supabase-otp" }; saveState(); setTranslatedStatus(status, "verificationCodeSent", { phone }); return; } const code = makeVerificationCode(); state.verification[type] = { phone, phoneDigits: phoneDigits(phone), code, verifiedPhone: null }; saveState(); setTranslatedStatus(status, "demoCode", { code, phone }); } async function verifyPhone(type) { const phoneInput = type === "passenger" ? els.passengerPhone : els.riderPhone; const codeInput = type === "passenger" ? els.passengerVerificationCode : els.riderVerificationCode; const status = type === "passenger" ? els.passengerStatus : els.riderStatus; const verification = state.verification[type]; const phone = phoneInput.value.trim(); if (!verification || !phoneMatches(verification.verifiedPhone ?? verification.phone, phone)) { setTranslatedStatus(status, "freshVerificationCodeRequired"); return false; } if (smsVerificationRelaxedForTesting()) { return markSmsRelaxedPhoneVerified(type, phone, status); } if (usesManualPhoneVerification()) { return markManualPhoneVerified(type, phone, status); } if (isSupabaseMode()) { setTranslatedStatus(status, "verifyingPhoneNumber"); const { data, error } = await supabaseClient.auth.verifyOtp({ phone, token: codeInput.value.trim(), type: "sms" }); if (error) { status.textContent = error.message; return false; } state.verification[type] = { ...verification, phone, phoneDigits: phoneDigits(phone), verifiedPhone: phone, verifiedAt: new Date().toISOString(), userId: data.user?.id ?? null, provider: "supabase-otp" }; state.sessions[type] = { phone, userId: data.user?.id ?? null, signedInAt: new Date().toISOString() }; saveState(); setTranslatedStatus(status, "phoneNumberVerified"); return true; } if (codeInput.value.trim() !== verification.code) { setTranslatedStatus(status, "verificationCodeIncorrect"); return false; } state.verification[type] = { ...verification, phone, phoneDigits: phoneDigits(phone), verifiedPhone: phone, verifiedAt: new Date().toISOString() }; state.sessions[type] = { phone, userId: null, signedInAt: new Date().toISOString() }; saveState(); setTranslatedStatus(status, "phoneNumberVerified"); return true; } function hasVerifiedPhone(type, phone) { const account = type === "passenger" ? state.passenger : state.rider; if (account?.phone && account.phoneVerified && phoneMatches(account.phone, phone)) return true; const verification = state.verification[type]; if (!verification?.verifiedAt) return false; return phoneMatches(verification.verifiedPhone ?? verification.phone, phone); } function phoneVerificationCodeInput(type) { return type === "passenger" ? els.passengerVerificationCode : els.riderVerificationCode; } function phoneVerificationStatusKey(type) { return type === "passenger" ? "passengerPhoneBeforeSave" : "riderPhoneBeforeReview"; } function supabaseUserPhoneVerifiedAt(user) { return user?.phone_confirmed_at ?? user?.confirmed_at ?? user?.last_sign_in_at ?? null; } async function markPhoneVerifiedFromSupabaseSession(type, phone, status) { if (!isSupabaseMode()) return false; let user = null; try { user = await getSupabaseUser(); } catch (error) { logClientWarning("Current Supabase phone session could not be checked.", error); return false; } if (!user?.phone || !phoneMatches(user.phone, phone)) return false; const verifiedAt = supabaseUserPhoneVerifiedAt(user) ?? new Date().toISOString(); state.verification[type] = { ...(state.verification[type] ?? {}), phone, phoneDigits: phoneDigits(phone), verifiedPhone: user.phone, verifiedAt, userId: user.id ?? null, provider: "supabase-otp" }; state.sessions[type] = { ...(state.sessions[type] ?? {}), phone, userId: user.id ?? null, signedInAt: new Date().toISOString() }; saveState(); setTranslatedStatus(status, "phoneNumberVerified"); return true; } async function ensureVerifiedPhoneForAccount(type, phone, status) { if (hasVerifiedPhone(type, phone)) return true; if (smsVerificationRelaxedForTesting()) { return markSmsRelaxedPhoneVerified(type, phone, status); } if (usesManualPhoneVerification()) { return markManualPhoneVerified(type, phone, status); } const verification = state.verification[type]; const codeInput = phoneVerificationCodeInput(type); if (codeInput?.value.trim() && verification && phoneMatches(verification.verifiedPhone ?? verification.phone, phone)) { if (await verifyPhone(type)) return true; return false; } if (await markPhoneVerifiedFromSupabaseSession(type, phone, status)) return true; setTranslatedStatus(status, phoneVerificationStatusKey(type)); return false; } function hasSignedIn(type) { return Boolean(state.sessions[type]); } function passengerSignInHasRideDeepLink() { if (typeof requestedRideRequestIdFromLocation !== "function") return false; const requestId = requestedRideRequestIdFromLocation(); if (!requestId) return false; if (typeof requestedPassengerWorkspacePageFromLocation !== "function") return false; return requestedPassengerWorkspacePageFromLocation() === "trips"; } function routePassengerToRequestAfterSignIn() { if (typeof passengerWorkspacePageSelectedInSession !== "undefined") { passengerWorkspacePageSelectedInSession = false; } state.activeTab = "passenger"; state.showRoleEntry = false; state.passengerPage = "request"; state.selectedRequestId = null; if (typeof updatePassengerWorkspaceRoute === "function") { updatePassengerWorkspaceRoute("request", { replace: true, preferPathRoute: true }); } return true; } function signInMeta(type) { if (type === "passenger") { return { emailInput: els.passengerSignInEmail, passwordInput: els.passengerSignInPassword, phoneInput: els.passengerSignInPhone, codeInput: els.passengerSignInCode, status: els.passengerSignInStatus, verificationKey: "passengerSignIn" }; } return { emailInput: els.riderSignInEmail, passwordInput: els.riderSignInPassword, phoneInput: els.riderSignInPhone, codeInput: els.riderSignInCode, status: els.riderSignInStatus, verificationKey: "riderSignIn" }; } function passwordResetMeta(type) { if (type === "passenger") { return { form: els.passengerSignInForm, emailInput: els.passengerSignInEmail, status: els.passengerSignInStatus, panel: els.passengerPasswordResetPanel, phoneStep: els.passengerPasswordResetPhoneStep, phoneHint: els.passengerPasswordResetPhoneHint, phoneCodeInput: els.passengerPasswordResetPhoneCode, sendPhoneOtpButton: els.sendPassengerPasswordResetPhoneOtp, verifyPhoneOtpButton: els.verifyPassengerPasswordResetPhoneOtp, phoneStatus: els.passengerPasswordResetPhoneStatus, passwordFields: els.passengerPasswordResetPasswordFields, passwordInput: els.passengerResetPassword, confirmInput: els.passengerResetPasswordConfirm, saveButton: els.savePassengerResetPassword }; } return { form: els.riderSignInForm, emailInput: els.riderSignInEmail, status: els.riderSignInStatus, panel: els.riderPasswordResetPanel, phoneStep: els.riderPasswordResetPhoneStep, phoneHint: els.riderPasswordResetPhoneHint, phoneCodeInput: els.riderPasswordResetPhoneCode, sendPhoneOtpButton: els.sendRiderPasswordResetPhoneOtp, verifyPhoneOtpButton: els.verifyRiderPasswordResetPhoneOtp, phoneStatus: els.riderPasswordResetPhoneStatus, passwordFields: els.riderPasswordResetPasswordFields, passwordInput: els.riderResetPassword, confirmInput: els.riderResetPasswordConfirm, saveButton: els.saveRiderResetPassword }; } function normalizedPasswordResetRole(value) { return value === "passenger" || value === "rider" ? value : ""; } function passwordResetRoleFromLocation() { try { const params = new URLSearchParams(window.location.search); const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, "")); const role = normalizedPasswordResetRole(params.get("wakaPasswordReset")); if (role) return role; const hasRecoveryMarker = String(params.get("type") || hashParams.get("type") || "").toLowerCase() === "recovery" || params.has("code") || params.has("token_hash") || hashParams.has("access_token"); if (hasRecoveryMarker) { return normalizedPasswordResetRole(params.get("tab") || params.get("role") || roleRequestedByCurrentShell() || state.activeTab) || "passenger"; } } catch (_error) {} return normalizedPasswordResetRole(passwordResetRoleCapturedBeforeSupabaseInit); } function passwordResetRedirectUrl(type) { const role = normalizedPasswordResetRole(type) || "passenger"; const url = new URL(`/${role}/password-reset`, window.location.origin); url.searchParams.set("tab", role); url.searchParams.set("wakaPasswordReset", role); url.hash = ""; return url.href; } const passwordResetCooldownStorageKey = "waka-password-reset-cooldowns-v1"; const passwordResetCooldownMs = 60 * 1000; const passwordResetRecoverySessionMaxAgeMs = 15 * 60 * 1000; function passwordResetCooldownKey(type, email) { return `${type}:${String(email || "").trim().toLowerCase()}`; } function passwordResetCooldownMap() { try { const stored = JSON.parse(localStorage.getItem(passwordResetCooldownStorageKey) || "{}"); return stored && typeof stored === "object" ? stored : {}; } catch (_error) { return {}; } } function passwordResetCooldownSeconds(type, email) { const sentAt = Number(passwordResetCooldownMap()[passwordResetCooldownKey(type, email)] || 0); const remainingMs = passwordResetCooldownMs - (Date.now() - sentAt); return remainingMs > 0 ? Math.ceil(remainingMs / 1000) : 0; } function rememberPasswordResetRequest(type, email) { const cooldowns = passwordResetCooldownMap(); const now = Date.now(); cooldowns[passwordResetCooldownKey(type, email)] = now; Object.keys(cooldowns).forEach((key) => { if (now - Number(cooldowns[key] || 0) > passwordResetCooldownMs * 4) delete cooldowns[key]; }); try { localStorage.setItem(passwordResetCooldownStorageKey, JSON.stringify(cooldowns)); } catch (error) { logClientWarning("Password reset cooldown could not be saved.", error); } } function clearPasswordResetLocationFlag() { try { const url = new URL(window.location.href); url.searchParams.delete("wakaPasswordReset"); url.searchParams.delete("code"); url.searchParams.delete("token_hash"); url.searchParams.delete("type"); url.hash = ""; passwordResetRoleCapturedBeforeSupabaseInit = ""; window.history.replaceState(null, "", `${url.pathname}${url.search}${url.hash}`); } catch (_error) {} } function clearPasswordResetCredentialsFromLocation() { try { const role = passwordResetRoleFromLocation() || state.passwordReset?.role; const url = new URL(window.location.href); url.searchParams.delete("code"); url.searchParams.delete("token_hash"); url.searchParams.delete("type"); if (role) { url.pathname = `/${role}/password-reset`; url.searchParams.set("tab", role); url.searchParams.set("wakaPasswordReset", role); } url.hash = ""; window.history.replaceState(null, "", `${url.pathname}${url.search}${url.hash}`); } catch (_error) {} } async function ensureSupabaseAuthClientForPasswordReset(status) { if (supabaseClient?.auth) return true; if (!hasSupabaseConfig()) { status.textContent = "Password reset requires WakaGood hosted account recovery."; return false; } status.textContent = "Connecting to WakaGood account recovery..."; await initSupabaseClient(); if (supabaseClient?.auth) return true; status.textContent = "WakaGood account recovery is not available right now. Please try again."; return false; } function ensureSupabaseConfigForPasswordReset(status) { if (hasSupabaseConfig()) return true; status.textContent = "Password reset requires WakaGood hosted account recovery."; return false; } function passwordResetRoleLabel(role) { return role === "rider" ? "rider" : "passenger"; } function passwordResetRequestAcceptedMessage(role) { return `If that email belongs to a WakaGood ${passwordResetRoleLabel(role)} account, a reset link has been sent. Open the email link to create a new password.`; } function passwordResetRoleMismatchMessage(role) { return `This reset link is not valid for a WakaGood ${passwordResetRoleLabel(role)} account. Use the correct WakaGood portal or contact support.`; } function passwordResetModeActive(type) { const role = normalizedPasswordResetRole(type); return Boolean(role && state.passwordReset?.active === true && state.passwordReset?.role === role); } function setPasswordResetMode(type, active, options = {}) { const role = normalizedPasswordResetRole(type); const resetPhoneOtp = options.resetPhoneOtp === true; const existing = !resetPhoneOtp && state.passwordReset?.role === role ? state.passwordReset : null; state.passwordReset = { role: active && role ? role : "", active: Boolean(active && role), startedAt: active && role ? existing?.startedAt || new Date().toISOString() : null, phoneOtpSentAt: active && role ? existing?.phoneOtpSentAt || null : null, phoneOtpMaskedPhone: active && role ? existing?.phoneOtpMaskedPhone || "" : "", phoneOtpVerified: Boolean(active && role && existing?.phoneOtpVerified === true), phoneOtpVerifiedAt: active && role ? existing?.phoneOtpVerifiedAt || null : null, recoverySessionUserId: active && role ? existing?.recoverySessionUserId || null : null, recoverySessionActivatedAt: active && role ? existing?.recoverySessionActivatedAt || null : null }; } function updatePasswordResetFormMode(type, active = passwordResetModeActive(type)) { const role = normalizedPasswordResetRole(type); if (!role) return; const meta = passwordResetMeta(role); const phoneOtpRequired = passwordResetPhoneOtpRequired(); const phoneOtpVerified = Boolean(state.passwordReset?.role === role && state.passwordReset?.phoneOtpVerified === true); const phoneOtpSatisfied = !phoneOtpRequired || phoneOtpVerified; meta.form?.classList.toggle("auth-recovery-mode", Boolean(active)); const heading = meta.form?.querySelector(".auth-form-heading"); const kicker = heading?.querySelector(".card-kicker"); const title = heading?.querySelector("h2"); const copy = heading?.querySelector("p"); if (active) { if (kicker) kicker.textContent = "Password recovery"; if (title) title.textContent = `Create a new ${passwordResetRoleLabel(role)} password`; if (copy) { copy.textContent = phoneOtpRequired ? "First verify the registered phone on this account, then enter the new password twice." : "Enter your new password twice. You will return to sign in after it is saved."; } if (meta.phoneStep) meta.phoneStep.hidden = !phoneOtpRequired; if (meta.passwordFields) meta.passwordFields.hidden = !phoneOtpSatisfied; if (meta.saveButton) meta.saveButton.disabled = !phoneOtpSatisfied; if (meta.phoneHint && phoneOtpRequired) { meta.phoneHint.textContent = state.passwordReset?.phoneOtpMaskedPhone ? `Enter the code sent to the registered phone ${state.passwordReset.phoneOtpMaskedPhone}.` : "Send a code to the verified phone saved on this account."; } if (meta.phoneStatus && !phoneOtpRequired) { meta.phoneStatus.textContent = passwordResetPhoneOtpRelaxedForTesting() ? "Staging recovery phone verification is relaxed for testing." : ""; } else if (meta.phoneStatus && phoneOtpVerified) { meta.phoneStatus.textContent = "Registered phone verified. Create your new password below."; } } else { if (kicker) kicker.textContent = "Welcome back"; if (title) title.textContent = role === "rider" ? "Rider sign in" : "Passenger sign in"; if (copy) copy.textContent = `Sign in to your WakaGood ${passwordResetRoleLabel(role)} account.`; if (meta.phoneStep) meta.phoneStep.hidden = true; if (meta.passwordFields) meta.passwordFields.hidden = false; if (meta.saveButton) meta.saveButton.disabled = false; } } function clearPasswordResetMode(type = "") { const role = normalizedPasswordResetRole(type || state.passwordReset?.role); setPasswordResetMode(role, false); if (role) updatePasswordResetFormMode(role, false); } function markPasswordResetRecoverySession(session) { const userId = session?.user?.id || session?.data?.user?.id || ""; if (!userId || !state.passwordReset?.active) return; state.passwordReset.recoverySessionUserId = userId; state.passwordReset.recoverySessionActivatedAt = new Date().toISOString(); saveState(); } function passwordResetRecoverySessionRecentlyActivated(userId) { if (!userId || !state.passwordReset?.active) return false; if (state.passwordReset.recoverySessionUserId !== userId) return false; const activatedAt = Date.parse(state.passwordReset.recoverySessionActivatedAt || ""); return Number.isFinite(activatedAt) && Date.now() - activatedAt <= passwordResetRecoverySessionMaxAgeMs; } function clearStalePasswordResetModeForCurrentLocation() { const role = normalizedPasswordResetRole(state.passwordReset?.role); if (!role || state.passwordReset?.active !== true) return false; if (passwordResetRoleFromLocation() === role) return false; clearPasswordResetMode(role); state.accountMode[role] = "signin"; state.activeTab = role; state.showRoleEntry = false; saveState(); return true; } async function abortPasswordResetToSignIn(type, message = "", options = {}) { const role = normalizedPasswordResetRole(type); if (!role) return; const meta = passwordResetMeta(role); if (options.clearAuthSession === true) { await clearStaleSupabaseSession(); } if (meta.passwordInput) meta.passwordInput.value = ""; if (meta.confirmInput) meta.confirmInput.value = ""; if (meta.phoneCodeInput) meta.phoneCodeInput.value = ""; if (meta.panel) meta.panel.hidden = true; clearPasswordResetMode(role); clearPasswordResetLocationFlag(); state.sessions[role] = null; if (role === "passenger") state.passenger = null; if (role === "rider") state.rider = null; state.accountMode[role] = "signin"; state.activeTab = role; state.showRoleEntry = false; saveState(); if (typeof hydrateForms === "function") hydrateForms(); if (typeof switchTab === "function") switchTab(role, { updateUrl: true, preserveEntry: false }); if (typeof renderAll === "function") renderAll(); if (message && meta.status) meta.status.textContent = message; } function preparePasswordResetReturnFromLocation() { const role = passwordResetRoleFromLocation(); if (!role) return ""; setPasswordResetMode(role, true, { resetPhoneOtp: true }); state.sessions[role] = null; if (role === "passenger") state.passenger = null; if (role === "rider") state.rider = null; state.activeTab = role; state.showRoleEntry = false; state.accountMode[role] = "signin"; saveState(); updatePasswordResetFormMode(role, true); const meta = passwordResetMeta(role); if (meta.panel) meta.panel.hidden = false; if (meta.status) meta.status.textContent = "Verifying your WakaGood password reset link..."; return role; } function showPasswordResetPanel(type, message = "", options = {}) { const role = normalizedPasswordResetRole(type); if (!role) return; const meta = passwordResetMeta(role); setPasswordResetMode(role, true, options); state.sessions[role] = null; if (role === "passenger") state.passenger = null; if (role === "rider") state.rider = null; state.activeTab = role; state.showRoleEntry = false; state.accountMode[role] = "signin"; saveState(); if (typeof switchTab === "function") switchTab(role, { updateUrl: false, preserveEntry: false }); updatePasswordResetFormMode(role, true); if (meta.panel) meta.panel.hidden = false; if (message && meta.status) meta.status.textContent = message; if (typeof renderAll === "function") renderAll(); updatePasswordResetFormMode(role, true); if (meta.panel) meta.panel.hidden = false; if (passwordResetPhoneOtpRequired() && !passwordResetPhoneOtpSatisfied(role)) { meta.sendPhoneOtpButton?.focus(); } else { meta.passwordInput?.focus(); } } async function sendRoleScopedPasswordResetRequest(role, email, redirectTo = passwordResetRedirectUrl(role)) { const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${passwordResetRequestFunctionName()}`, { method: "POST", headers: { "Content-Type": "application/json", apikey: appConfig.supabaseAnonKey, Authorization: `Bearer ${appConfig.supabaseAnonKey}` }, body: JSON.stringify({ role, email, redirectTo }) }), "Requesting WakaGood password reset", optionalSupabaseRequestTimeoutMs ); const text = await response.text(); let payload = null; if (text) { try { payload = JSON.parse(text); } catch { payload = { error: text }; } } if (!response.ok) { throw new Error(payload?.error || payload?.message || text || `WakaGood account recovery failed with HTTP ${response.status}.`); } return payload; } function passwordResetPhoneOtpSatisfied(role) { if (!passwordResetPhoneOtpRequired()) return true; return Boolean(state.passwordReset?.role === role && state.passwordReset?.phoneOtpVerified === true); } async function passwordResetRecoveryAccessToken() { const session = await withSupabaseTimeout( supabaseClient.auth.getSession(), "Reading WakaGood recovery session", optionalSupabaseRequestTimeoutMs ); return session?.data?.session?.access_token || ""; } async function callPasswordResetPhoneOtpFunction(role, payload) { const accessToken = await passwordResetRecoveryAccessToken(); if (!accessToken) throw new Error("Open the reset link from your email first."); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${passwordResetPhoneOtpFunctionName()}`, { method: "POST", headers: { "Content-Type": "application/json", apikey: appConfig.supabaseAnonKey, Authorization: `Bearer ${accessToken}` }, body: JSON.stringify({ role, ...payload }) }), "Completing WakaGood phone recovery", optionalSupabaseRequestTimeoutMs ); const text = await response.text(); let body = null; if (text) { try { body = JSON.parse(text); } catch { body = { error: text }; } } if (!response.ok) { throw new Error(body?.error || body?.message || text || `WakaGood phone recovery failed with HTTP ${response.status}.`); } return body || {}; } async function callPasswordResetCompleteFunction(role, password) { const accessToken = await passwordResetRecoveryAccessToken(); if (!accessToken) throw new Error("Open the reset link from your email first."); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${passwordResetCompleteFunctionName()}`, { method: "POST", headers: { "Content-Type": "application/json", apikey: appConfig.supabaseAnonKey, Authorization: `Bearer ${accessToken}` }, body: JSON.stringify({ role, password, phoneOtpVerifiedAt: state.passwordReset?.phoneOtpVerifiedAt || null }) }), "Updating WakaGood password", supabaseProfileSaveTimeoutMs ); const text = await response.text(); let body = null; if (text) { try { body = JSON.parse(text); } catch { body = { error: text }; } } if (!response.ok) { throw new Error(body?.error || body?.message || text || `WakaGood password update failed with HTTP ${response.status}.`); } return body || {}; } function passwordResetPhoneOtpErrorMessage(error) { const message = String(error?.message || error || ""); if (/open the reset link/i.test(message)) return "Open the reset link from your email first."; if (/too many|rate|wait/i.test(message)) return "Too many phone-code attempts. Wait before trying again."; if (/incorrect|expired|tried too many/i.test(message)) return "The phone code is incorrect, expired, or has been tried too many times."; if (/sms recovery is not configured|Twilio|could not send/i.test(message)) return "WakaGood phone recovery is not available right now. Please contact support."; return message || "WakaGood phone recovery could not be completed."; } async function sendPasswordResetPhoneOtp(type) { const role = normalizedPasswordResetRole(type); if (!role) return; const meta = passwordResetMeta(role); let cooldownKeyPhone = ""; const clientReady = await ensureSupabaseAuthClientForPasswordReset(meta.status); if (!clientReady) return; if (!passwordResetPhoneOtpRequired()) { state.passwordReset.phoneOtpVerified = true; state.passwordReset.phoneOtpVerifiedAt = new Date().toISOString(); updatePasswordResetFormMode(role, true); if (meta.status) meta.status.textContent = "Phone recovery check is relaxed for this staging test. Create your new password below."; meta.passwordInput?.focus(); return; } try { const sessionReady = await ensurePasswordResetSessionFromUrl(meta); if (!sessionReady) { await abortPasswordResetToSignIn(role, "This password reset link could not be verified. Request a new reset email.", { clearAuthSession: true }); return; } const roleAccess = await passwordResetSessionRoleAccess(role); if (!roleAccess.ok) { await rejectPasswordResetForRoleMismatch(role, meta); return; } const cooldownPhone = roleAccess.profile?.phone || role; cooldownKeyPhone = cooldownPhone; const cooldownSeconds = phoneOtpCooldownSeconds(`passwordReset:${role}`, cooldownPhone); if (cooldownSeconds > 0) { meta.phoneStatus.textContent = `For account security, wait ${cooldownSeconds} seconds before requesting another phone code.`; return; } meta.phoneStatus.textContent = "Sending phone verification code..."; startPhoneOtpCooldown(`passwordReset:${role}`, cooldownPhone); const body = await callPasswordResetPhoneOtpFunction(role, { action: "request" }); state.passwordReset.phoneOtpSentAt = new Date().toISOString(); state.passwordReset.phoneOtpMaskedPhone = String(body.maskedPhone || ""); saveState(); updatePasswordResetFormMode(role, true); meta.phoneStatus.textContent = `Code sent to the registered phone ${state.passwordReset.phoneOtpMaskedPhone || "on file"}. It expires in 10 minutes.`; meta.phoneCodeInput?.focus(); } catch (error) { if (cooldownKeyPhone && !/too many|rate|wait/i.test(String(error?.message || ""))) { clearPhoneOtpCooldown(`passwordReset:${role}`, cooldownKeyPhone); } logClientWarning("Password reset phone OTP request was not completed.", error); meta.phoneStatus.textContent = passwordResetPhoneOtpErrorMessage(error); } } async function verifyPasswordResetPhoneOtp(type) { const role = normalizedPasswordResetRole(type); if (!role) return; const meta = passwordResetMeta(role); const code = meta.phoneCodeInput?.value.trim() || ""; if (!/^\d{6}$/.test(code)) { meta.phoneStatus.textContent = "Enter the 6-digit phone code."; meta.phoneCodeInput?.focus(); return; } const clientReady = await ensureSupabaseAuthClientForPasswordReset(meta.status); if (!clientReady) return; try { const sessionReady = await ensurePasswordResetSessionFromUrl(meta); if (!sessionReady) { await abortPasswordResetToSignIn(role, "This password reset link could not be verified. Request a new reset email.", { clearAuthSession: true }); return; } const roleAccess = await passwordResetSessionRoleAccess(role); if (!roleAccess.ok) { await rejectPasswordResetForRoleMismatch(role, meta); return; } meta.phoneStatus.textContent = "Verifying phone code..."; const body = await callPasswordResetPhoneOtpFunction(role, { action: "verify", code }); state.passwordReset.phoneOtpVerified = true; state.passwordReset.phoneOtpVerifiedAt = String(body.verifiedAt || new Date().toISOString()); if (meta.phoneCodeInput) meta.phoneCodeInput.value = ""; saveState(); updatePasswordResetFormMode(role, true); meta.status.textContent = "Registered phone verified. Create your new password below."; meta.passwordInput?.focus(); } catch (error) { logClientWarning("Password reset phone OTP verification was not completed.", error); meta.phoneStatus.textContent = passwordResetPhoneOtpErrorMessage(error); } } async function signedInProfileCanRecoverRole(profile, role) { if (!profile?.id || !["passenger", "rider"].includes(role)) return false; if (strictRoleAuthIsolationEnabled()) return signedInProfileHasPrimaryRole(profile, role); const primaryRole = String(profile.role || "").toLowerCase(); if (primaryRole === "admin" && !(await signedInProfileHasRole(profile, role))) return false; if (await signedInProfileHasRole(profile, role)) return true; if (role === "rider" && (primaryRole === "passenger" || primaryRole === "rider")) return true; return role === "rider" && await signedInProfileHasRiderApplication(profile); } async function passwordResetSessionRoleAccess(role) { if (!normalizedPasswordResetRole(role)) return { ok: false, reason: "invalid_role" }; const user = await getSupabaseUser(); if (!user?.id) return { ok: false, reason: "missing_session" }; const profile = await loadSupabaseProfileForUser(user, "*", `Checking the WakaGood ${role} reset session`); if (!profile?.id) return { ok: false, reason: "missing_profile", user }; const allowed = await signedInProfileCanRecoverRole(profile, role); return { ok: allowed, reason: allowed ? "" : "role_mismatch", user, profile }; } async function ensurePasswordResetSessionFromUrl(meta) { let hasRecoveryCredential = false; try { const params = new URLSearchParams(window.location.search); const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, "")); const code = params.get("code"); hasRecoveryCredential = Boolean(code); if (code && typeof supabaseClient.auth.exchangeCodeForSession === "function") { meta.status.textContent = "Verifying your WakaGood reset link..."; const { data, error } = await withSupabaseTimeout( supabaseClient.auth.exchangeCodeForSession(code), "Verifying WakaGood password reset link", optionalSupabaseRequestTimeoutMs ); if (error) throw error; if (data?.session) { markPasswordResetRecoverySession(data.session); clearPasswordResetCredentialsFromLocation(); return true; } } const accessToken = hashParams.get("access_token"); const refreshToken = hashParams.get("refresh_token"); hasRecoveryCredential = hasRecoveryCredential || Boolean(accessToken && refreshToken); if (accessToken && refreshToken && typeof supabaseClient.auth.setSession === "function") { meta.status.textContent = "Verifying your WakaGood reset link..."; const { data, error } = await withSupabaseTimeout( supabaseClient.auth.setSession({ access_token: accessToken, refresh_token: refreshToken }), "Activating WakaGood password reset link", optionalSupabaseRequestTimeoutMs ); if (error) throw error; if (data?.session) { markPasswordResetRecoverySession(data.session); clearPasswordResetCredentialsFromLocation(); return true; } } const tokenHash = params.get("token_hash") || hashParams.get("token_hash"); hasRecoveryCredential = hasRecoveryCredential || Boolean(tokenHash); if (tokenHash && typeof supabaseClient.auth.verifyOtp === "function") { meta.status.textContent = "Verifying your WakaGood reset link..."; const { data, error } = await withSupabaseTimeout( supabaseClient.auth.verifyOtp({ token_hash: tokenHash, type: "recovery" }), "Activating WakaGood password reset token", optionalSupabaseRequestTimeoutMs ); if (error) throw error; if (data?.session) { markPasswordResetRecoverySession(data.session); clearPasswordResetCredentialsFromLocation(); return true; } } } catch (error) { try { const fallbackSession = await withSupabaseTimeout( supabaseClient.auth.getSession(), "Checking WakaGood password reset session after link activation", optionalSupabaseRequestTimeoutMs ); const fallbackUserId = fallbackSession?.data?.session?.user?.id || ""; if (passwordResetRecoverySessionRecentlyActivated(fallbackUserId)) { clearPasswordResetCredentialsFromLocation(); return true; } } catch (_fallbackError) {} logClientWarning("Password reset link session could not be activated.", error); meta.status.textContent = "This password reset link could not be verified. Request a new reset email."; return false; } if (hasRecoveryCredential) { meta.status.textContent = "This password reset link could not be verified. Request a new reset email."; return false; } const next = await withSupabaseTimeout( supabaseClient.auth.getSession(), "Checking WakaGood password reset session", optionalSupabaseRequestTimeoutMs ); const userId = next?.data?.session?.user?.id || ""; if (passwordResetRecoverySessionRecentlyActivated(userId)) return true; if (meta?.status) { meta.status.textContent = "Open the reset link from your email first. A normal signed-in session cannot update a password here."; } return false; } async function rejectPasswordResetForRoleMismatch(role, meta) { await clearStaleSupabaseSession(); state.sessions[role] = null; if (role === "passenger") state.passenger = null; if (role === "rider") state.rider = null; clearPasswordResetMode(role); if (meta?.panel) meta.panel.hidden = true; clearPasswordResetLocationFlag(); if (meta?.status) meta.status.textContent = passwordResetRoleMismatchMessage(role); saveState(); if (typeof renderAll === "function") renderAll(); if (meta?.status) meta.status.textContent = passwordResetRoleMismatchMessage(role); } async function requestPasswordReset(type) { const role = normalizedPasswordResetRole(type); if (!role) return; const meta = passwordResetMeta(role); const email = meta.emailInput.value.trim().toLowerCase(); if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { meta.status.textContent = "Enter your email above, then tap Forgot password."; meta.emailInput.focus(); return; } const cooldownSeconds = passwordResetCooldownSeconds(role, email); if (cooldownSeconds > 0) { meta.status.textContent = `For account security, wait ${cooldownSeconds} seconds before requesting another reset email.`; return; } if (!ensureSupabaseConfigForPasswordReset(meta.status)) return; meta.status.textContent = "Sending WakaGood password reset email..."; try { await sendRoleScopedPasswordResetRequest(role, email); rememberPasswordResetRequest(role, email); clearPasswordResetMode(role); if (meta.panel) meta.panel.hidden = true; meta.status.textContent = passwordResetRequestAcceptedMessage(role); } catch (error) { logClientWarning("Password reset email request was not completed.", error); meta.status.textContent = "WakaGood account recovery is being secured right now. Please try again shortly or contact support."; } } async function completePasswordReset(type) { const role = normalizedPasswordResetRole(type); if (!role) return; const meta = passwordResetMeta(role); const clientReady = await ensureSupabaseAuthClientForPasswordReset(meta.status); if (!clientReady) return; try { if (!passwordResetModeActive(role)) { showPasswordResetPanel(role, "Open the reset link from your email first, then enter the new password here."); } const sessionReady = await ensurePasswordResetSessionFromUrl(meta); if (!sessionReady) { await abortPasswordResetToSignIn(role, "This password reset link could not be verified. Request a new reset email.", { clearAuthSession: true }); return; } const roleAccess = await passwordResetSessionRoleAccess(role); if (!roleAccess.ok) { await rejectPasswordResetForRoleMismatch(role, meta); return; } if (!passwordResetPhoneOtpSatisfied(role)) { updatePasswordResetFormMode(role, true); meta.phoneStatus.textContent = "Verify the registered phone before saving a new password."; meta.phoneCodeInput?.focus(); return; } const password = meta.passwordInput.value; const confirm = meta.confirmInput.value; if (password.length < 8) { meta.status.textContent = "Use a new password with at least 8 characters."; meta.passwordInput.focus(); return; } if (password !== confirm) { meta.status.textContent = "The two password entries do not match."; meta.confirmInput.focus(); return; } meta.status.textContent = "Updating your WakaGood password..."; await callPasswordResetCompleteFunction(role, password); meta.passwordInput.value = ""; meta.confirmInput.value = ""; if (meta.phoneCodeInput) meta.phoneCodeInput.value = ""; if (meta.panel) meta.panel.hidden = true; clearPasswordResetLocationFlag(); await clearStaleSupabaseSession(); state.sessions[role] = null; if (role === "passenger") state.passenger = null; if (role === "rider") state.rider = null; clearPasswordResetMode(role); state.accountMode[role] = "signin"; state.activeTab = role; state.showRoleEntry = false; saveState(); hydrateForms(); if (typeof switchTab === "function") switchTab(role, { updateUrl: true, preserveEntry: false }); if (roleAccess.user?.email && meta.emailInput) meta.emailInput.value = roleAccess.user.email; meta.status.textContent = "Password updated. Sign in with your new password."; } catch (error) { meta.status.textContent = error.message || "Password update could not be completed."; } } async function handlePasswordResetReturnFromLocation() { const role = passwordResetRoleFromLocation(); if (!role) return false; const meta = passwordResetMeta(role); showPasswordResetPanel(role, "Verifying your WakaGood password reset link...", { resetPhoneOtp: true }); state.sessions[role] = null; if (role === "passenger") state.passenger = null; if (role === "rider") state.rider = null; saveState(); const clientReady = await ensureSupabaseAuthClientForPasswordReset(meta.status); if (!clientReady) { await abortPasswordResetToSignIn(role, "Open the reset link again when WakaGood account recovery is available.", { clearAuthSession: true }); return true; } try { const sessionReady = await ensurePasswordResetSessionFromUrl(meta); if (!sessionReady) { await abortPasswordResetToSignIn(role, "This password reset link could not be verified. Request a new reset email.", { clearAuthSession: true }); return true; } const roleAccess = await passwordResetSessionRoleAccess(role); if (!roleAccess.ok) { if (roleAccess.reason === "missing_session") { await abortPasswordResetToSignIn(role, "This password reset session expired. Request a new reset email.", { clearAuthSession: true }); return true; } await rejectPasswordResetForRoleMismatch(role, meta); return true; } showPasswordResetPanel( role, passwordResetPhoneOtpRequired() ? "Verify your registered phone before creating a new password." : `Choose a new password for your WakaGood ${passwordResetRoleLabel(role)} account.` ); } catch (error) { logClientWarning("Password reset session role check was not completed.", error); await rejectPasswordResetForRoleMismatch(role, meta); } return true; } async function sendSignInCode(type) { const meta = signInMeta(type); if (!phoneOtpSignInEnabled()) { setTranslatedStatus(meta.status, "passwordSignInOnly"); return; } const phone = meta.phoneInput.value.trim(); if (phone.length < 8) { setTranslatedStatus(meta.status, "validPhoneRequired"); return; } const cooldownSeconds = phoneOtpCooldownSeconds(meta.verificationKey, phone); if (cooldownSeconds > 0) { setTranslatedStatus(meta.status, "phoneOtpCooldown", { seconds: cooldownSeconds }); return; } if (isSupabaseMode() && !usesManualPhoneVerification()) { startPhoneOtpCooldown(meta.verificationKey, phone); setTranslatedStatus(meta.status, "sendingSignInCode"); const { error } = await supabaseClient.auth.signInWithOtp({ phone }); if (error) { if (error.status !== 429 && !/rate limit|too many/i.test(error.message)) clearPhoneOtpCooldown(meta.verificationKey, phone); meta.status.textContent = phoneOtpErrorMessage(error); return; } state.verification[meta.verificationKey] = { phone, provider: "supabase-otp" }; saveState(); setTranslatedStatus(meta.status, "signInCodeSent", { phone }); return; } const code = makeVerificationCode(); state.verification[meta.verificationKey] = { phone, code, provider: "demo" }; saveState(); setTranslatedStatus(meta.status, "demoSignInCode", { code, phone }); } function localAccountForSignIn(type, meta) { const phone = meta.phoneInput.value.trim(); const email = meta.emailInput.value.trim().toLowerCase(); const records = type === "passenger" ? [state.passenger, ...state.passengers] : [state.rider, ...state.riders]; return records .filter(Boolean) .find((record) => (phone && record.phone === phone) || (email && record.email === email)) ?? null; } function applyLocalSignIn(type, meta) { const account = localAccountForSignIn(type, meta); if (!account) { setTranslatedStatus(meta.status, "localSignInAccountMissing", { type }); return false; } state.sessions[type] = { phone: account.phone, email: account.email, userId: account.supabaseUserId ?? account.id ?? null, signedInAt: new Date().toISOString() }; if (type === "passenger") { state.passenger = account; state.passengers = upsertById(state.passengers, account); } else { state.rider = account; state.riders = upsertById(state.riders, account); } state.accountMode[type] = "signin"; state.activeTab = type; state.showRoleEntry = false; if (type === "passenger") routePassengerToRequestAfterSignIn(); if (type === "rider" && typeof restoreWorkspaceUiState === "function") restoreWorkspaceUiState("rider", { replaceRoute: true }); saveState(); populateLocationFields(); hydrateForms(); switchTab(type); setTranslatedStatus(meta.status, "signedInAs", { identity: account.email ?? account.phone }); if (type === "rider") els.riderSessionSummary.textContent = riderWorkspaceStatusMessage(currentRiderRecord()); return true; } async function signInWithEmailPassword(type, meta) { const email = meta.emailInput.value.trim().toLowerCase(); const password = meta.passwordInput.value; if (!email || !password) return false; if (separateRoleAuthTenantsRequested()) { const configured = roleAuthTenantConfigured(type); meta.status.textContent = configured ? "Separate passenger/rider Auth projects are configured, but the Waka database identity bridge must be enabled before browser sign-in can use them." : `The WakaGood ${type} Auth project is not configured yet. Keep strict single-tenant mode until the separate ${type} tenant is provisioned.`; return true; } setTranslatedStatus(meta.status, "signingInPassword"); try { let user; let profile; if (supabaseClient) { const { data, error } = await withSupabaseTimeout( supabaseClient.auth.signInWithPassword({ email, password }), `Signing in as ${type}` ); if (error) throw error; setTranslatedStatus(meta.status, "loadingWakaProfile"); const { data: profileData, error: profileError } = await withSupabaseTimeout( supabaseClient .from("profiles") .select("*") .eq("id", data.user.id) .maybeSingle(), `Loading the ${type} profile`, supabaseProfileSaveTimeoutMs ); if (profileError) throw profileError; user = data.user; profile = profileData; } else if (hasSupabaseConfig()) { const session = await signInWithSupabasePasswordRest(email, password); setTranslatedStatus(meta.status, "loadingWakaProfile"); user = session.user; profile = await selectProfileRest(user.id, "*", session.access_token); } else { setTranslatedStatus(meta.status, "supabaseConfigNeeded"); return true; } if (!profile) { profile = await recoverMissingSupabaseProfileFromLocalAccount(type, user, (message) => { meta.status.textContent = message; }).catch((error) => { logClientWarning("Missing Waka profile recovery was skipped.", error); return null; }); } if (!profile) { setPendingProfileRecovery(type, user, email); state.accountMode[type] = "create"; state.activeTab = type; state.showRoleEntry = false; saveState(); hydrateForms(); switchTab(type); const createEmailInput = type === "rider" ? els.riderEmail : els.passengerEmail; const createStatus = type === "rider" ? els.riderStatus : els.passengerStatus; if (createEmailInput) createEmailInput.value = email; setTranslatedStatus(createStatus ?? meta.status, "supabaseProfileMissing"); return true; } const profileCanUseRole = await signedInProfileCanUseWorkspaceRole(profile, type); const riderHasApplication = !strictRoleAuthIsolationEnabled() && type === "rider" && await signedInProfileHasRiderApplication(profile); const adminIdentity = type !== "admin" && !profileCanUseRole && !riderHasApplication && await signedInProfileIsAdminIdentity(profile); if (adminIdentity) { setTranslatedStatus(meta.status, "adminPublicPortalBlocked"); await clearStaleSupabaseSession(); return true; } const riderNeedsApplication = type === "rider" && !riderHasApplication && !adminIdentity; const riderCanUseExistingApplication = type === "rider" && riderHasApplication && !adminIdentity; const riderCanStartApplication = !strictRoleAuthIsolationEnabled() && riderNeedsApplication && !riderCanUseExistingApplication && (profileCanUseRole || profile.role === "passenger" || profile.role === "rider"); if (!profileCanUseRole && !riderCanUseExistingApplication && !riderCanStartApplication) { setTranslatedStatus(meta.status, strictRoleAuthIsolationEnabled() ? "wrongProfileRoleStrict" : "wrongProfileRole", { role: profile.role, type }); await clearStaleSupabaseSession(); return true; } const signedInProfile = profileForWorkspaceRole(profile, { role: type, needsApplication: riderCanStartApplication }); if (profileAccountIsBlocked(profile)) { meta.status.textContent = profileAccountBlockedMessage(profile); await clearStaleSupabaseSession(); return true; } applySignedInProfile(type, signedInProfile, user); state.accountMode[type] = "signin"; state.activeTab = type; state.showRoleEntry = false; if (type === "passenger") routePassengerToRequestAfterSignIn(); if (riderCanStartApplication) state.riderPage = "profile"; if (type === "rider" && typeof setRiderWorkspaceLandingAfterSignIn === "function") { setRiderWorkspaceLandingAfterSignIn(riderCanStartApplication, { honorRequestedPage: false, replaceRoute: true }); } saveState(); populateLocationFields(); hydrateForms(); switchTab(type); renderAll(); setTranslatedStatus(meta.status, type === "passenger" ? "signedInPassengerLoaded" : "signedInRiderLoaded", { email }); if (type === "rider") els.riderSessionSummary.textContent = riderWorkspaceStatusMessage(currentRiderRecord()); void refreshWorkspaceAfterEmailSignIn(type, email, meta.status, { riderCanStartApplication }).catch((error) => { logClientWarning(`${type} workspace refresh after sign-in was skipped.`, error); }); } catch (error) { meta.status.textContent = error.message; } return true; } async function refreshWorkspaceAfterEmailSignIn(type, email, status, { riderCanStartApplication = false } = {}) { if (type === "rider") { try { await hydrateProfileFromSupabase(type); } catch (error) { logClientWarning("Rider profile hydration after sign-in was skipped.", error); } if (typeof setRiderWorkspaceLandingAfterSignIn === "function") { setRiderWorkspaceLandingAfterSignIn(riderCanStartApplication); } populateLocationFields(); hydrateForms(); if (typeof activeRole !== "function" || activeRole() === type) { switchTab(type, { updateUrl: false }); } } await refreshPaymentAccountsFromSupabase(type).catch((error) => { logClientWarning("Payment account refresh after sign-in was skipped.", error); }); if (type === "passenger") { await loadPassengerRideRequestsFromSupabase(state.passenger?.id).catch((error) => { logClientWarning("Passenger ride requests could not be reloaded after sign-in.", error); }); } await loadMarketplaceFromSupabase({ includeAccountData: true }).catch((error) => { logClientWarning("Marketplace refresh after sign-in was skipped.", error); }); if (type === "passenger") { if (paymentAccountReady("passenger", state.passenger)) clearPendingPaymentSetup(); routePassengerToRequestAfterSignIn(); saveState(); } if (typeof handlePaymentSetupReturnFromLocation === "function") { await handlePaymentSetupReturnFromLocation().catch((error) => { logClientWarning("Payment setup return after sign-in was skipped.", error); }); } if (typeof handleSubscriptionCheckoutReturnFromLocation === "function") { await handleSubscriptionCheckoutReturnFromLocation().catch((error) => { logClientWarning("Subscription checkout return after sign-in was skipped.", error); }); } renderAll(); setTranslatedStatus(status, type === "passenger" ? "signedInPassengerLoaded" : "signedInRiderLoaded", { email }); if (type === "rider") els.riderSessionSummary.textContent = riderWorkspaceStatusMessage(currentRiderRecord()); } async function verifySignIn(type) { const meta = signInMeta(type); const phone = meta.phoneInput.value.trim(); const verification = state.verification[meta.verificationKey]; if (hasSupabaseConfig() && (!usesManualPhoneVerification() || (meta.emailInput.value.trim() && meta.passwordInput.value))) { if (await signInWithEmailPassword(type, meta)) { return; } } if (!phoneOtpSignInEnabled()) { setTranslatedStatus(meta.status, "passwordSignInOnly"); return; } if (usesManualPhoneVerification()) { applyLocalSignIn(type, meta); return; } if (!verification || verification.phone !== phone) { setTranslatedStatus(meta.status, "signInCodeRequired"); return; } if (isSupabaseMode() && !usesManualPhoneVerification()) { setTranslatedStatus(meta.status, "signingIn"); const { data, error } = await supabaseClient.auth.verifyOtp({ phone, token: meta.codeInput.value.trim(), type: "sms" }); if (error) { meta.status.textContent = error.message; return; } state.sessions[type] = { phone, userId: data.user?.id ?? null, signedInAt: new Date().toISOString() }; saveState(); setTranslatedStatus(meta.status, "signedInAs", { identity: phone }); hydrateProfileFromSupabase(type); return; } if (meta.codeInput.value.trim() !== verification.code) { setTranslatedStatus(meta.status, "signInCodeIncorrect"); return; } applyLocalSignIn(type, meta); } async function hydrateProfileFromSupabase(type) { if (!hasSupabaseRuntime()) return; if (type === "rider" && riderProfileHydrationInFlight) return riderProfileHydrationInFlight; const run = hydrateProfileFromSupabaseInternal(type); if (type !== "rider") return run; riderProfileHydrationInFlight = run.finally(() => { riderProfileHydrationInFlight = null; }); return riderProfileHydrationInFlight; } function scheduleRiderProfileHydration(reason = "workspace") { if (!hasSupabaseRuntime() || !hasSignedIn("rider")) return; if (typeof activeRole === "function" && activeRole() !== "rider") return; const now = Date.now(); if (riderProfileHydrationInFlight || now - riderProfileHydrationRefreshAt < riderProfileHydrationRefreshMs) return; riderProfileHydrationRefreshAt = now; void hydrateProfileFromSupabase("rider").catch((error) => { logClientWarning(`Rider profile refresh skipped after ${reason}.`, error); }); } async function hydrateProfileFromSupabaseInternal(type) { if (!hasSupabaseRuntime()) return; const user = await getSupabaseUser(); if (!user) return; let data = null; try { data = await loadSupabaseProfileForUser(user, "*", `Loading the ${type} profile`); } catch { return; } if (!data) { data = await recoverMissingSupabaseProfileFromLocalAccount(type, user).catch((error) => { logClientWarning("Missing Waka profile recovery during hydration was skipped.", error); return null; }); } if (!data) return; if (type === "passenger") { state.passenger = { id: data.id, supabaseUserId: data.id, name: data.full_name, email: data.email, phone: data.phone, phoneVerified: Boolean(data.phone_verified_at), phoneVerifiedAt: data.phone_verified_at, nationalId: data.national_id_number, dateOfBirth: data.date_of_birth, preferredLanguage: data.preferred_language, country: data.country, city: data.city, profilePhotoPath: data.profile_photo_path, accountStatus: data.account_status ?? "active", accountStatusReason: data.account_status_reason ?? "", accountStatusChangedAt: data.account_status_changed_at ?? null, accountStatusChangedBy: data.account_status_changed_by ?? null, accountClosedAt: data.account_closed_at ?? null, createdAt: data.created_at }; state.passengers = upsertById(state.passengers, state.passenger); } if (type === "rider") { let application = null; let subscription = null; let taxIdentityReference = null; let taxDocumentRows = []; let backgroundCheckRows = []; if (supabaseClient) { const { data: applicationRows, error: applicationError } = await withSupabaseTimeout( supabaseClient .from("rider_applications") .select("*") .eq("rider_id", user.id) .order("created_at", { ascending: false }) .limit(10), "Loading the rider application", supabaseProfileSaveTimeoutMs ); const { data: subscriptionData } = await withSupabaseTimeout( supabaseClient .from("rider_subscriptions") .select("*") .eq("rider_id", user.id) .maybeSingle(), "Loading the rider subscription", optionalSupabaseRequestTimeoutMs ).catch((error) => { logClientWarning("Rider subscription refresh was skipped.", error); return { data: null }; }); const { data: taxIdentityData } = await withSupabaseTimeout( supabaseClient .from("rider_tax_identity_references") .select("*") .eq("rider_id", user.id) .maybeSingle(), "Loading the rider tax profile", optionalSupabaseRequestTimeoutMs ).catch((error) => { logClientWarning("Rider tax profile refresh was skipped.", error); return { data: null }; }); const { data: taxDocumentData } = await withSupabaseTimeout( supabaseClient .from("rider_tax_documents") .select("*") .eq("rider_id", user.id) .order("tax_year", { ascending: false }), "Loading rider tax documents", optionalSupabaseRequestTimeoutMs ).catch((error) => { logClientWarning("Rider tax document refresh was skipped.", error); return { data: [] }; }); const { data: backgroundCheckData } = await withSupabaseTimeout( supabaseClient .from("rider_background_checks") .select("*") .eq("rider_id", user.id) .order("created_at", { ascending: false }) .limit(10), "Loading rider background checks", optionalSupabaseRequestTimeoutMs ).catch((error) => { logClientWarning("Rider background-check refresh was skipped.", error); return { data: [] }; }); if (applicationError) throw applicationError; application = chooseRiderApplicationForWorkspace(applicationRows ?? []); subscription = subscriptionData; taxIdentityReference = taxIdentityData; taxDocumentRows = taxDocumentData ?? []; backgroundCheckRows = backgroundCheckData ?? []; } else { application = await selectRiderApplicationRest(user.id, supabaseRestSession?.access_token); subscription = await selectRiderSubscriptionRest(user.id, supabaseRestSession?.access_token); const rows = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rider_tax_identity_references?rider_id=eq.${user.id}&select=*&limit=1`, { accessToken: supabaseRestSession?.access_token }), "Loading the rider tax profile", optionalSupabaseRequestTimeoutMs ).catch(() => []); taxIdentityReference = rows?.[0] ?? null; taxDocumentRows = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rider_tax_documents?rider_id=eq.${user.id}&select=*&order=tax_year.desc`, { accessToken: supabaseRestSession?.access_token }), "Loading rider tax documents", optionalSupabaseRequestTimeoutMs ).catch(() => []); backgroundCheckRows = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rider_background_checks?rider_id=eq.${user.id}&select=*&order=created_at.desc&limit=10`, { accessToken: supabaseRestSession?.access_token }), "Loading rider background checks", optionalSupabaseRequestTimeoutMs ).catch(() => []); } if (application?.status === "approved" && !subscription?.trial_ends_at && !subscription?.paid_until) { subscription = { ...(subscription ?? {}), trial_ends_at: riderTrialEndsFromApproval(application, subscription) }; } const needsApplication = !application; const documents = parseRiderDocuments(application?.document_path ?? state.rider?.documentName); const navigationOverride = riderNavigationPreferenceOverrideForRider(data.id); if (navigationOverride) documents.navigationPreference = navigationOverride; const applicationBodyType = normalizeCarBodyType(application?.car_body_type ?? state.rider?.carBodyType); state.rider = { ...(state.rider ?? {}), id: data.id, supabaseUserId: data.id, name: data.full_name, email: data.email, phone: data.phone, phoneVerified: Boolean(data.phone_verified_at), phoneVerifiedAt: data.phone_verified_at, nationalId: data.national_id_number, dateOfBirth: data.date_of_birth, preferredLanguage: data.preferred_language, country: data.country, city: data.city, profilePhotoPath: data.profile_photo_path, accountStatus: data.account_status ?? "active", accountStatusReason: data.account_status_reason ?? "", accountStatusChangedAt: data.account_status_changed_at ?? null, accountStatusChangedBy: data.account_status_changed_by ?? null, accountClosedAt: data.account_closed_at ?? null, area: application?.operating_area ?? state.rider?.area ?? "", vehicle: application?.vehicle ?? state.rider?.vehicle ?? "car", credential: application?.credential_number ?? state.rider?.credential ?? "", registration: application?.vehicle_registration ?? state.rider?.registration ?? "", carMake: application?.car_make ?? state.rider?.carMake ?? "", carModel: application?.car_model ?? state.rider?.carModel ?? "", carBodyType: applicationBodyType, vehicleDesignation: normalizeRiderVehicleDesignation(application?.vehicle_designation ?? documents.vehicleDesignation ?? state.rider?.vehicleDesignation, applicationBodyType), navigationPreference: normalizeRiderNavigationPreference(navigationOverride ?? documents.navigationPreference ?? state.rider?.navigationPreference), carYear: application?.car_year ?? state.rider?.carYear ?? "", carColor: application?.car_color ?? state.rider?.carColor ?? "", vehicleVin: application?.vehicle_vin ?? state.rider?.vehicleVin ?? "", insuranceProvider: application?.insurance_provider ?? state.rider?.insuranceProvider ?? "", insuranceNumber: application?.insurance_number ?? state.rider?.insuranceNumber ?? "", driverLicenseExpiresOn: application?.driver_license_expires_on ?? state.rider?.driverLicenseExpiresOn ?? "", insuranceExpiresOn: application?.insurance_expires_on ?? state.rider?.insuranceExpiresOn ?? "", complianceSuspendedAt: application?.compliance_suspended_at ?? state.rider?.complianceSuspendedAt ?? null, complianceSuspensionReason: application?.compliance_suspension_reason ?? state.rider?.complianceSuspensionReason ?? "", backgroundCheckConsentAt: application?.background_check_consent_at ?? state.rider?.backgroundCheckConsentAt ?? null, backgroundCheckProvider: application?.background_check_consent_provider ?? state.rider?.backgroundCheckProvider ?? "", backgroundCheckConsentVersion: application?.background_check_consent_version ?? state.rider?.backgroundCheckConsentVersion ?? "", backgroundCheckStatus: application?.background_check_status ?? state.rider?.backgroundCheckStatus ?? "not requested", backgroundCheckDecision: application?.background_check_decision ?? state.rider?.backgroundCheckDecision ?? "pending", documentName: navigationOverride ? riderDocumentPayload(documents) : application?.document_path ?? state.rider?.documentName ?? "", documents, needsApplication, driverLicenseDocumentPath: documents.driverLicense, vehicleRegistrationDocumentPath: documents.vehicleRegistration, insuranceDocumentPath: documents.insurance, vehicleInspectionDocumentPath: documents.vehicleInspection, status: application?.status ?? (needsApplication ? "profile only" : state.rider?.status ?? "pending"), reviewNote: application?.review_note ?? state.rider?.reviewNote ?? "", approvedAt: application?.reviewed_at ?? state.rider?.approvedAt ?? null, trialEndsAt: subscription?.trial_ends_at ?? state.rider?.trialEndsAt ?? null, subscriptionPaidUntil: subscription?.paid_until ?? state.rider?.subscriptionPaidUntil ?? null, rating: state.rider?.rating ?? "new", createdAt: application?.created_at ?? state.rider?.createdAt ?? data.created_at }; state.riders = upsertById(state.riders, state.rider); if (taxIdentityReference?.id) { state.taxIdentityReferences = upsertById( state.taxIdentityReferences.filter((item) => item.riderId !== user.id), mapTaxIdentityReferenceFromDatabase(taxIdentityReference) ); } state.taxDocuments = [ ...state.taxDocuments.filter((item) => item.riderId !== user.id), ...taxDocumentRows.map((document) => mapTaxDocumentFromDatabase(document)) ]; state.backgroundChecks = [ ...state.backgroundChecks.filter((item) => item.riderId !== user.id), ...backgroundCheckRows.map((check) => mapRiderBackgroundCheckFromDatabase(check)) ]; } let financeAdjustmentRows = []; if (supabaseClient) { const { data: adjustmentData } = await withSupabaseTimeout( supabaseClient .from("finance_adjustments") .select("*") .eq("subject_id", user.id) .order("created_at", { ascending: false }) .limit(25), "Loading finance adjustment notices", optionalSupabaseRequestTimeoutMs ).catch((error) => { logClientWarning("Finance adjustment refresh was skipped.", error); return { data: [] }; }); financeAdjustmentRows = adjustmentData ?? []; } else { financeAdjustmentRows = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/finance_adjustments?subject_id=eq.${user.id}&select=*&order=created_at.desc&limit=25`, { accessToken: supabaseRestSession?.access_token }), "Loading finance adjustment notices", optionalSupabaseRequestTimeoutMs ).catch(() => []); } state.financeAdjustments = [ ...state.financeAdjustments.filter((item) => item.subjectId !== user.id), ...financeAdjustmentRows.map((adjustment) => mapFinanceAdjustmentFromDatabase(adjustment)) ]; saveState(); populateLocationFields(); hydrateForms(); switchTab(type); renderAll(); } async function restoreSignedInRoleFromSupabaseSession() { if (!hasSupabaseRuntime()) return false; let user = null; try { user = await getSupabaseUser(); } catch (error) { logClientWarning("Stored Supabase session could not be restored.", error); return false; } if (!user) return false; let profile = null; try { profile = await loadSupabaseProfileForUser(user, "*", "Restoring the signed-in Waka profile"); } catch (error) { logClientWarning("Signed-in Waka profile could not be restored.", error); return false; } if (!profile) { const requestedRole = roleRequestedByCurrentShell() || (["passenger", "rider"].includes(state.activeTab) ? state.activeTab : null); if (requestedRole) { profile = await recoverMissingSupabaseProfileFromLocalAccount(requestedRole, user).catch((error) => { logClientWarning("Missing Waka profile recovery during session restore was skipped.", error); return null; }); } } await pruneUnauthorizedWorkspaceSessionsForProfile(profile); const requestedRole = roleRequestedByCurrentShell(); if (requestedRole && !(await signedInProfileCanRestoreWorkspaceRole(profile, requestedRole))) { clearWorkspaceRoleSession(requestedRole); state.activeTab = requestedRole; state.showRoleEntry = false; saveState(); renderAll(); return false; } const roleResolution = await resolveSupabaseSessionWorkspaceRole(profile); if (!roleResolution) return false; const { role } = roleResolution; if (requestedRole && role !== requestedRole) { clearWorkspaceRoleSession(requestedRole); state.activeTab = requestedRole; state.showRoleEntry = false; saveState(); renderAll(); return false; } if (profileAccountIsBlocked(profile)) { await clearStaleSupabaseSession(); return false; } const keepPublicHomeVisible = role === "passenger" ? false : keepPublicHomeVisibleForSessionRestore(); applySignedInProfile(role, profileForWorkspaceRole(profile, roleResolution), user); state.accountMode[role] = "signin"; if (!keepPublicHomeVisible) { state.activeTab = role; state.showRoleEntry = false; } if (!keepPublicHomeVisible && role === "passenger") routePassengerToRequestAfterSignIn(); saveState(); populateLocationFields(); hydrateForms(); if (typeof applyRouteTab === "function") { applyRouteTab(); } else { renderAll(); } try { await hydrateProfileFromSupabase(role); } catch (error) { logClientWarning("Profile hydration after session restore was skipped.", error); } await refreshPaymentAccountsFromSupabase(role).catch((error) => { logClientWarning("Payment account refresh after session restore was skipped.", error); }); if (role === "passenger") { await loadPassengerRideRequestsFromSupabase(state.passenger?.id).catch((error) => { logClientWarning("Passenger ride requests could not be reloaded after session restore.", error); }); } await loadMarketplaceFromSupabase({ includeAccountData: true }).catch((error) => { logClientWarning("Marketplace refresh after session restore was skipped.", error); }); if (!keepPublicHomeVisible && role === "passenger") { if (paymentAccountReady("passenger", state.passenger)) clearPendingPaymentSetup(); routePassengerToRequestAfterSignIn(); saveState(); } if (typeof applyRouteTab === "function") { applyRouteTab(); return true; } renderAll(); return true; } // Marketplace matching, GPS/proximity, routing links, and live market refresh helpers. let marketRefreshInFlight = false; let marketplaceRealtimeChannel = null; let marketplaceRealtimeSignature = ""; let marketplaceRealtimeRefreshTimer = null; let marketplaceRealtimeReconnectTimer = null; let marketplaceRealtimeRefreshPendingReason = ""; let lastMarketRefreshAt = null; let lastMarketplaceSyncSource = "not refreshed"; let passengerApproachRefreshTimerDelayMs = 0; let riderMarketplaceRefreshTimerDelayMs = 0; const accountNotificationAutoRefreshTimers = { passenger: null, rider: null }; const accountNotificationAutoRefreshTimerDelayMs = { passenger: 0, rider: 0 }; let lastPassengerApproachSource = "not refreshed"; let areaProximityRpcUnavailable = false; let gpsMatchingRpcUnavailable = false; let riderMarketplaceRpcUnavailable = false; let passengerApproachRpcUnavailable = false; function passengerPickupGpsAccuracyLimitMeters() { return passengerPickupGpsMaxAccuracyMeters; } function riderLiveGpsAccuracyLimitMeters() { return riderLiveGpsMaxAccuracyMeters; } let activeRideContactRpcUnavailable = false; let rideRequestRpcUnavailable = false; let lastRidePostSource = "not used"; const addressPickupMatchAccuracyMeters = 100; function fareDistanceEstimateKm(country, city, pickupAreaName, destinationAreaName, pickupGps = pendingPickupGps) { const pickupArea = findArea(country, city, pickupAreaName); const destinationArea = findArea(country, city, destinationAreaName); const areaDistance = estimatedAreaDistanceKm(country, city, pickupArea, destinationArea); if (areaDistance == null) return null; const gpsConfidenceBoost = pickupGps ? 1 : 1.15; return Math.max(1, areaDistance * riderPickupEtaRoadFactor * gpsConfidenceBoost); } function fareDistanceEstimateMiles(country, city, pickupAreaName, destinationAreaName, pickupGps = pendingPickupGps) { const distanceKm = fareDistanceEstimateKm(country, city, pickupAreaName, destinationAreaName, pickupGps); return distanceKm == null ? null : Math.max(0.6, distanceKm * kmToMiles); } function roundToPricingStep(value, step) { const cleanValue = Number(value); const cleanStep = Number(step); if (!Number.isFinite(cleanValue) || cleanValue <= 0) return null; if (!Number.isFinite(cleanStep) || cleanStep <= 0) return cleanValue; return Math.max(cleanStep, Math.round(cleanValue / cleanStep) * cleanStep); } function insurancePricingRuleForMarket(country, city) { const key = `${country || ""}|${city || ""}`; return insurancePricingConfig.regionalRules[key] ?? Object.entries(insurancePricingConfig.regionalRules) .find(([regionKey]) => regionKey.toLowerCase().startsWith(`${String(country || "").toLowerCase()}|`))?.[1] ?? null; } function insurancePricingLoadForFare(distanceMiles, country, city) { if (!configFlagEnabled(appConfig.insurancePricingEnabled ?? insurancePricingConfig.enabled)) { return { amountUsd: 0, activeMiles: 0, perActiveMileUsd: 0, regionLabel: "" }; } const rule = insurancePricingRuleForMarket(country, city); const perActiveMileUsd = Number(rule?.perActiveMileUsd ?? insurancePricingConfig.defaultPerActiveMileUsd); const pickupMileAllowance = Number(rule?.pickupMileAllowance ?? insurancePricingConfig.defaultPickupMileAllowance); const minTripInsuranceUsd = Number(rule?.minTripInsuranceUsd ?? insurancePricingConfig.minTripInsuranceUsd); const tripDistanceMultiplier = Number(rule?.tripDistanceMultiplier ?? insurancePricingConfig.tripDistanceMultiplier); const activeMiles = Math.max(0, Number(distanceMiles || 0) * tripDistanceMultiplier + Math.max(0, pickupMileAllowance)); const amountUsd = Math.max(0, Math.max(minTripInsuranceUsd, activeMiles * perActiveMileUsd)); return { amountUsd: Math.round(amountUsd * 100) / 100, activeMiles: Math.round(activeMiles * 10) / 10, perActiveMileUsd, regionLabel: [country, city].filter(Boolean).join(" / "), commercialLiabilityLimitUsd: rule?.commercialLiabilityLimitUsd ?? null, requiresTelematicsSdk: Boolean(rule?.requiresTelematicsSdk) }; } function fareGuidanceFromDistance(distanceMiles, minutes, stops = [], meta = {}) { const stopCount = normalizeRideStops(stops).length; const rawDistanceMiles = Math.max(0.6, Number(distanceMiles) || 0); const cleanDistanceMiles = Math.max( 0.6, roundToPricingStep(rawDistanceMiles, fareGuidanceConfig.distanceStepMiles) ?? rawDistanceMiles ); const rawMinutes = Math.max(1, Math.ceil(Number(minutes) || cleanDistanceMiles * 4)); const cleanMinutes = Math.max( 1, Math.round(roundToPricingStep(rawMinutes, fareGuidanceConfig.minuteStep) ?? rawMinutes) ); const insuranceLoad = insurancePricingLoadForFare(cleanDistanceMiles, meta.country, meta.city); const midpoint = Math.max( fareGuidanceConfig.minFareUsd, (fareGuidanceConfig.baseFareUsd + cleanDistanceMiles * fareGuidanceConfig.perMileUsd + cleanMinutes * fareGuidanceConfig.perMinuteUsd + stopCount * fareGuidanceConfig.perStopUsd + insuranceLoad.amountUsd) * fareGuidanceConfig.fuelIndex ); return { distanceKm: cleanDistanceMiles / kmToMiles, distanceMiles: cleanDistanceMiles, minutes: cleanMinutes, stopCount, midpoint: Math.round(midpoint), min: Math.max(fareGuidanceConfig.minFareUsd, Math.round(midpoint * fareGuidanceConfig.minMultiplier)), max: Math.max(fareGuidanceConfig.minFareUsd + 1, Math.round(midpoint * fareGuidanceConfig.maxMultiplier)), source: meta.source ?? "zone", provider: meta.provider ?? "zone", cached: Boolean(meta.cached), routeKey: meta.routeKey ?? null, routePolyline: meta.routePolyline ?? null, insuranceCostUsd: insuranceLoad.amountUsd, insuranceActiveMiles: insuranceLoad.activeMiles, insurancePerActiveMileUsd: insuranceLoad.perActiveMileUsd, insuranceRegion: insuranceLoad.regionLabel, insuranceCommercialLiabilityLimitUsd: insuranceLoad.commercialLiabilityLimitUsd, insuranceRequiresTelematicsSdk: insuranceLoad.requiresTelematicsSdk, destinationFingerprint: meta.destinationFingerprint ?? null, estimatedAt: meta.estimatedAt ?? new Date().toISOString() }; } const routeGuidanceAbsoluteMaxDistanceMiles = 600; const routeGuidanceAbsoluteMaxMinutes = 18 * 60; function routeGuidanceHasUsableNumbers(guidance) { const distance = Number(guidance?.distanceMiles); const minutes = Number(guidance?.minutes); return Number.isFinite(distance) && distance > 0 && Number.isFinite(minutes) && minutes > 0; } function fareGuidanceForRide(country, city, pickupAreaName, destinationAreaName, pickupGps = pendingPickupGps, stops = []) { const distanceMiles = fareDistanceEstimateMiles(country, city, pickupAreaName, destinationAreaName, pickupGps); if (distanceMiles == null) return null; const stopCount = normalizeRideStops(stops).length; const adjustedDistanceMiles = distanceMiles * (1 + stopCount * fareGuidanceConfig.stopDistanceMultiplier); const adjustedDistanceKm = adjustedDistanceMiles / kmToMiles; const minutes = (pickupEtaMinutes(adjustedDistanceKm, { vehicle: "car" }) ?? Math.ceil(adjustedDistanceMiles * 4)) + stopCount * fareGuidanceConfig.perStopMinutes; return fareGuidanceFromDistance(adjustedDistanceMiles, minutes, stops, { source: "zone", provider: "zone", country, city }); } function fareGuidanceMessage(guidance) { if (!guidance) return "Enter pickup and destination addresses to see the estimated fare before publishing."; const stops = guidance.stopCount ? `, ${guidance.stopCount} stop${guidance.stopCount === 1 ? "" : "s"}` : ""; const prefix = guidance.source === "place-preview" ? "Quick estimate" : "Estimated fare"; const designation = normalizeCarTypePreference(els.vehiclePreference?.value); const nonNegotiableFare = passengerFareMode() === "non_negotiable" ? passengerMinimumFareFromGuidance(guidance, designation) : 0; if (nonNegotiableFare) return `$${nonNegotiableFare}`; const routeSummary = `${formatRouteDistanceForRequest(guidance.distanceMiles, { country: selectedPassengerCountry() })} and about ${guidance.minutes} minutes${stops}`; if (els.fareOffer) { els.fareOffer.placeholder = designation === "suv" ? String(Number(guidance.max) + 1) : String(guidance.min); } if (designation === "suv") { const minimum = Number(guidance.max) + 1; const high = Math.max(minimum, Math.ceil(Number(guidance.max) * 1.25)); const suffix = guidance.source === "place-preview" ? " Google Routes will refine this automatically." : passengerFareMode() === "non_negotiable" ? ` No negotiation will publish at the XL/Special minimum of $${minimum}; riders can accept or decline.` : ` Normal range is $${guidance.min}-$${guidance.max}; Waka will decline XL/Special offers at $${guidance.max} or below.`; return `${prefix}: $${minimum}-$${high} XL/Special range for ${routeSummary}.${suffix}`; } const suffix = guidance.source === "place-preview" ? " Google Routes will refine this automatically." : passengerFareMode() === "non_negotiable" ? ` No negotiation will publish at the minimum fare of $${guidance.min}; riders can accept or decline.` : " Final fare is negotiable."; return `${prefix}: $${guidance.min}-$${guidance.max} Normal range for ${routeSummary}.${suffix}`; } function routeGuidancePendingMessage() { return routeEstimatesEnabled() ? "Enter a destination and either use current location or enter a pickup address to estimate the fare before publishing." : fareGuidanceMessage(null); } function routeGuidanceUnavailableMessage() { return routeEstimateFallbackAllowedForTesting() ? "Google route distance is unavailable right now. For staging, Waka will publish with the fare you enter." : "Google route distance is unavailable right now. Please try again in a moment."; } function routeGuidancePickupDescriptionKey(origin, pickupDescription) { return origin?.source === "browser-gps" ? "current-location" : String(pickupDescription ?? "").trim(); } function routeGuidanceGpsKey(point) { const gps = normalizeGpsPoint(point); return gps ? `${gps.latitude.toFixed(3)},${gps.longitude.toFixed(3)}` : ""; } function routeGuidanceInputKey(country, city, pickupAreaName, destinationAreaName, pickupDescription, destination, stops = [], pickupGpsForRoute = pendingPickupGps, destinationPlaceOverride = null) { const legacyCall = arguments.length <= 7; const origin = routeOriginForEstimate(country, city, pickupAreaName, pickupDescription, pickupGpsForRoute); const pickupPlace = origin.source === "google-places" ? { placeId: origin.placeId, formattedAddress: origin.formattedAddress, displayName: origin.address } : pickupPlaceForRoute(pickupDescription); const overridePlace = normalizedPlaceSelection(destinationPlaceOverride); const destinationPlace = legacyCall ? destinationPlaceForRoute(destination) : destinationPlaceMatchesInput(overridePlace, destination) ? overridePlace : destinationPlaceForRoute(destination); const pickupGps = origin.source === "browser-gps" && validGpsCoordinate(Number(origin.latitude), Number(origin.longitude)) ? { latitude: Number(origin.latitude), longitude: Number(origin.longitude) } : null; const gpsKey = routeGuidanceGpsKey(pickupGps); return JSON.stringify({ country, city, pickupAreaName, destinationAreaName, pickupDescription: routeGuidancePickupDescriptionKey(origin, pickupDescription), destination: String(destination ?? "").trim(), pickupPlaceId: pickupPlace?.placeId ?? null, destinationPlaceId: destinationPlace?.placeId ?? null, gpsKey, stops: normalizeRideStops(stops) }); } function routeGuidanceAttemptKeyForInputs(country, city, pickupAreaName, destinationAreaName, pickupDescription, destination, stops = [], pickupGpsForRoute = pendingPickupGps) { const destinationPlace = destinationPlaceForRoute(destination); const origin = routeOriginForEstimate(country, city, pickupAreaName, pickupDescription, pickupGpsForRoute); const pickupGps = routeGuidanceGpsKey(origin); const destinationPoint = validGpsCoordinate(Number(destinationPlace?.latitude), Number(destinationPlace?.longitude)) ? `${Number(destinationPlace.latitude).toFixed(3)},${Number(destinationPlace.longitude).toFixed(3)}` : ""; return JSON.stringify({ country, city, pickupAreaName, destinationAreaName, originSource: origin?.source ?? "", pickupAddress: routeGuidancePickupDescriptionKey(origin, origin?.address || pickupDescription).toLowerCase(), pickupGps, destinationPlaceId: destinationPlace?.placeId ?? null, destinationAddress: String(destinationPlace?.formattedAddress || destination || "").trim().toLowerCase(), destinationPoint, stops: normalizeRideStops(stops) }); } function cachedConfirmedFareGuidanceForKey(key) { const latestGuidance = lastRouteFareGuidance && key && key === lastRouteFareGuidanceKey && routeGuidanceConfirmedForPublish(lastRouteFareGuidance) ? lastRouteFareGuidance : null; if (latestGuidance) return latestGuidance; return stablePassengerFareGuidance && key && key === stablePassengerFareGuidanceKey && routeGuidanceConfirmedForPublish(stablePassengerFareGuidance) ? stablePassengerFareGuidance : null; } async function waitForConfirmedFareGuidance(key, attempts = 16) { for (let attempt = 0; attempt < attempts; attempt += 1) { const cachedGuidance = cachedConfirmedFareGuidanceForKey(key); if (cachedGuidance || fareGuidanceInFlightKey !== key) return cachedGuidance; await pause(250); } return cachedConfirmedFareGuidanceForKey(key); } function fareGuidancePreviewInputs() { const enteredPickupDescription = els.pickupDescription?.value.trim() ?? ""; const selectedPickupGps = selectedCurrentPickupGps ?? pendingPickupGps; const pickupDescription = els.pickupUseCurrentLocation?.checked ? (currentPickupLocationLabel(selectedPickupGps) || enteredPickupDescription) : enteredPickupDescription; const destination = els.destination?.value.trim() ?? ""; const country = selectedPassengerCountry(); const city = state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(country); return { country, city, pickupAreaName: els.pickupArea?.value ?? "", destinationAreaName: els.destinationArea?.value ?? "", pickupDescription, destination, stops: rideStopsFormValue() }; } function routeEstimateErrorMessage(error) { const message = String(error?.message || "").trim(); if (!message) return routeGuidanceUnavailableMessage(); if (/edge function returned a non-2xx status code/i.test(message)) return routeGuidanceUnavailableMessage(); return message; } function clearFareGuidancePreview() { window.clearTimeout(fareGuidanceTimer); fareGuidanceInFlightKey = ""; lastRouteFareGuidance = null; lastRouteFareGuidanceKey = ""; lastRouteEstimateAttemptKey = ""; stablePassengerFareGuidance = null; stablePassengerFareGuidanceKey = ""; } function rememberStablePassengerFareGuidance(key, guidance) { if (!key || !guidance) return; stablePassengerFareGuidance = guidance; stablePassengerFareGuidanceKey = key; } function immediateCoordinateFareGuidance(country, city, pickupAreaName, destinationAreaName, pickupDescription, destination, stops = [], destinationPlaceOverride = null, pickupGps = pendingPickupGps) { const origin = routeOriginForEstimate(country, city, pickupAreaName, pickupDescription, pickupGps); if (routeOriginNeedsConfirmedAddressForPricing(origin)) return null; const overridePlace = normalizedPlaceSelection(destinationPlaceOverride); const destinationPlace = destinationPlaceMatchesInput(overridePlace, destination) ? overridePlace : destinationPlaceForRoute(destination); const originPoint = validGpsCoordinate(Number(origin?.latitude), Number(origin?.longitude)) ? { latitude: Number(origin.latitude), longitude: Number(origin.longitude) } : null; const destinationPoint = validGpsCoordinate(Number(destinationPlace?.latitude), Number(destinationPlace?.longitude)) ? { latitude: Number(destinationPlace.latitude), longitude: Number(destinationPlace.longitude) } : null; if (!originPoint || !destinationPoint) return null; const stopPoints = normalizeRideStops(stops).map(stopRoutePoint); const points = [originPoint, ...stopPoints.filter(Boolean), destinationPoint]; const straightKm = points.slice(1).reduce((total, point, index) => ( total + gpsDistanceKmBetween(points[index], point) ), 0); if (!Number.isFinite(straightKm) || straightKm <= 0) return null; const missingStopPointCount = Math.max(0, normalizeRideStops(stops).length - stopPoints.filter(Boolean).length); const distanceMiles = Math.max(0.6, straightKm * riderPickupEtaRoadFactor * kmToMiles); const minutes = Math.max(3, Math.ceil(distanceMiles * 2.1 + missingStopPointCount * fareGuidanceConfig.perStopMinutes)); const guidance = fareGuidanceFromDistance(distanceMiles, minutes, stops, { source: "place-preview", provider: "local-place-preview", country, city }); const areaGuidance = fareGuidanceForRide(country, city, pickupAreaName, destinationAreaName, pickupGps, stops); if (routeGuidanceLooksImplausible(guidance, areaGuidance)) { lastRouteEstimateError = new Error("Route estimate looked unrealistic for this service area. Re-select the pickup and destination addresses, then try again."); logClientWarning("Ignoring implausible local route preview.", { preview: guidance, area: areaGuidance }); return null; } return guidance; } function routeGuidanceLooksImplausible(guidance, localGuidance, options = {}) { if (!guidance) return false; if (!routeGuidanceHasUsableNumbers(guidance)) return true; const distance = Number(guidance.distanceMiles); const minutes = Number(guidance.minutes); const absoluteMaxDistance = Number(options.maxDistanceMiles ?? routeGuidanceAbsoluteMaxDistanceMiles); const absoluteMaxMinutes = Number(options.maxMinutes ?? routeGuidanceAbsoluteMaxMinutes); if (Number.isFinite(absoluteMaxDistance) && absoluteMaxDistance > 0 && distance > absoluteMaxDistance) return true; if (Number.isFinite(absoluteMaxMinutes) && absoluteMaxMinutes > 0 && minutes > absoluteMaxMinutes) return true; if (!localGuidance || !routeGuidanceHasUsableNumbers(localGuidance)) return false; const localDistance = Number(localGuidance.distanceMiles); const localMinutes = Number(localGuidance.minutes); const distanceLimit = Number(options.distanceRatio ?? 2.6); const minuteLimit = Number(options.minuteRatio ?? 3.2); const absoluteDistanceLimit = Number(options.absoluteDistanceMiles ?? 12); const absoluteMinuteLimit = Number(options.absoluteMinutes ?? 60); return (distance > localDistance * distanceLimit && distance - localDistance > absoluteDistanceLimit) || (minutes > localMinutes * minuteLimit && minutes - localMinutes > absoluteMinuteLimit); } function safeRouteGuidance(guidance, localGuidance, context = "route") { if (!routeGuidanceLooksImplausible(guidance, localGuidance)) return guidance; lastRouteEstimateError = new Error(`Google Routes returned an unusually large ${context} estimate, so Waka kept the local address-based estimate.`); logClientWarning("Ignoring implausible Google route estimate.", { context, google: guidance, local: localGuidance }); return localGuidance ?? null; } function scheduleFareGuidancePreview() { if (!els.fareGuidance) return null; const inputs = fareGuidancePreviewInputs(); const pickupGpsForPreview = passengerPickupGpsForFormChoice(); const origin = routeOriginForEstimate( inputs.country, inputs.city, inputs.pickupAreaName, inputs.pickupDescription, pickupGpsForPreview ); if (!inputs.destination || !routeOriginIsSpecific(origin)) { clearFareGuidancePreview(); els.fareGuidance.textContent = routeGuidancePendingMessage(); if (typeof updatePassengerFareModeControls === "function") updatePassengerFareModeControls(null); return null; } const key = routeGuidanceInputKey( inputs.country, inputs.city, inputs.pickupAreaName, inputs.destinationAreaName, inputs.pickupDescription, inputs.destination, inputs.stops, pickupGpsForPreview, selectedDestinationPlace ); const cachedGuidance = cachedConfirmedFareGuidanceForKey(key); if (cachedGuidance) { els.fareGuidance.textContent = fareGuidanceMessage(cachedGuidance); if (typeof updatePassengerFareModeControls === "function") updatePassengerFareModeControls(cachedGuidance); return cachedGuidance; } const immediateGuidance = immediateCoordinateFareGuidance( inputs.country, inputs.city, inputs.pickupAreaName, inputs.destinationAreaName, inputs.pickupDescription, inputs.destination, inputs.stops, null, pickupGpsForPreview ); els.fareGuidance.textContent = "Checking accurate driving distance before publishing..."; if (key === fareGuidanceInFlightKey) return null; const attemptKey = routeGuidanceAttemptKeyForInputs( inputs.country, inputs.city, inputs.pickupAreaName, inputs.destinationAreaName, inputs.pickupDescription, inputs.destination, inputs.stops, pickupGpsForPreview ); lastRouteEstimateAttemptKey = attemptKey; const requestId = ++fareGuidanceRequestId; window.clearTimeout(fareGuidanceTimer); fareGuidanceInFlightKey = key; fareGuidanceTimer = window.setTimeout(async () => { try { const guidance = await accurateFareGuidanceForRide( inputs.country, inputs.city, inputs.pickupAreaName, inputs.destinationAreaName, inputs.destination, pickupGpsForPreview, inputs.stops, inputs.pickupDescription ); if (requestId !== fareGuidanceRequestId) return; fareGuidanceInFlightKey = ""; const finalGuidance = safeRouteGuidance(guidance, immediateGuidance, "fare") ?? immediateGuidance; lastRouteFareGuidance = finalGuidance; lastRouteFareGuidanceKey = key; rememberStablePassengerFareGuidance(key, finalGuidance); if (els.fareGuidance) { els.fareGuidance.textContent = finalGuidance ? fareGuidanceMessage(finalGuidance) : routeEstimateErrorMessage(lastRouteEstimateError); } if (typeof updatePassengerFareModeControls === "function") updatePassengerFareModeControls(finalGuidance); } catch (error) { if (requestId !== fareGuidanceRequestId) return; fareGuidanceInFlightKey = ""; lastRouteFareGuidance = immediateGuidance; lastRouteFareGuidanceKey = immediateGuidance ? key : ""; rememberStablePassengerFareGuidance(lastRouteFareGuidanceKey, immediateGuidance); if (els.fareGuidance) { els.fareGuidance.textContent = immediateGuidance ? `${fareGuidanceMessage(immediateGuidance)} Google Routes is unavailable right now, so this quick estimate is shown.` : routeEstimateErrorMessage(error); } if (typeof updatePassengerFareModeControls === "function") updatePassengerFareModeControls(immediateGuidance); } }, 250); return null; } function updateFareGuidance() { if (!els.fareGuidance) return null; if (routeEstimatesEnabled()) { return scheduleFareGuidancePreview(); } if (!els.destination?.value.trim()) { els.fareGuidance.textContent = fareGuidanceMessage(null); if (typeof updatePassengerFareModeControls === "function") updatePassengerFareModeControls(null); return null; } const country = selectedPassengerCountry(); const city = state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(country); const guidance = fareGuidanceForRide(country, city, els.pickupArea?.value, els.destinationArea?.value, pendingPickupGps, rideStopsFormValue()); els.fareGuidance.textContent = fareGuidanceMessage(guidance); if (els.fareOffer && guidance) { els.fareOffer.placeholder = normalizeCarTypePreference(els.vehiclePreference?.value) === "suv" ? String(Number(guidance.max) + 1) : String(guidance.min); } if (typeof updatePassengerFareModeControls === "function") updatePassengerFareModeControls(guidance); return guidance; } function routeEstimatesEnabled() { return String(appConfig.routeEstimatesProvider || "zone").toLowerCase() === "google-routes"; } function accurateRouteEstimateRequired() { return routeEstimatesEnabled() && (configFlagEnabled(appConfig.requireRouteEstimateBeforePublish) || strictProductionModeEnabled()); } function routeEstimateFallbackAllowedForTesting() { return configFlagEnabled(appConfig.relaxRouteEstimateForTesting) && /\b(staging|pilot|test|preview)\b/i.test(String(appConfig.projectName || "")); } function routeOriginNeedsConfirmedAddressForPricing(origin) { return false; } function currentLocationNeedsAddressMessage() { return "Current location was captured. Waka will use the GPS point for route pricing."; } function normalizedRouteEstimateSourceForDatabase(source) { return String(source || "zone").toLowerCase() === "google-routes" ? "google-routes" : "zone"; } function normalizedRouteEstimateProviderForDatabase(source, provider) { return normalizedRouteEstimateSourceForDatabase(source) === "google-routes" ? String(provider || "google-routes").trim() || "google-routes" : "zone"; } function routeGuidanceConfirmedForPublish(guidance) { if (!routeEstimatesEnabled() || !accurateRouteEstimateRequired()) return true; if (routeEstimateFallbackAllowedForTesting()) return !guidance || !routeGuidanceLooksImplausible(guidance, null); return normalizedRouteEstimateSourceForDatabase(guidance?.source) === "google-routes"; } function routeEstimateFunctionName() { return String(appConfig.routeEstimateFunctionName || "route-estimate").trim() || "route-estimate"; } function typedRouteAddress(text, city, country) { return compactLocationQuery([text, city, country]); } function destinationRouteAddress(destination, destinationAreaName, city, country) { const destinationText = String(destination ?? "").trim(); return destinationText.length >= 6 ? typedRouteAddress(destinationText, city, country) : compactLocationQuery([destinationText, destinationAreaName, city, country]); } function placesAutocompleteEnabled() { return String(appConfig.placesAutocompleteProvider || "none").toLowerCase() === "google-places"; } function placesAutocompleteFunctionName() { return String(appConfig.placesAutocompleteFunctionName || "place-autocomplete").trim() || "place-autocomplete"; } function autoPickupGpsEnabled() { return configFlagEnabled(appConfig.autoPickupGpsEnabled); } function autoRiderGpsEnabled() { return configFlagEnabled(appConfig.autoRiderGpsEnabled); } function passengerPickupGpsForFormChoice(fallbackGps = pendingPickupGps) { if (!els.pickupUseCurrentLocation?.checked) return null; return normalizeGpsPoint(selectedCurrentPickupGps) ?? normalizeGpsPoint(fallbackGps); } function normalizedPlaceSelection(place) { if (!place || typeof place !== "object") return null; const placeId = String(place.placeId ?? "").trim(); const displayName = String(place.displayName ?? "").trim(); const formattedAddress = String(place.formattedAddress ?? "").trim(); const latitude = Number(place.latitude); const longitude = Number(place.longitude); const source = String(place.source ?? "").trim(); return { placeId: placeId || null, displayName: displayName || formattedAddress || null, formattedAddress: formattedAddress || null, latitude: Number.isFinite(latitude) ? latitude : null, longitude: Number.isFinite(longitude) ? longitude : null, accuracyMeters: place.accuracyMeters ?? null, capturedAt: place.capturedAt ?? null, source: source || null, selectedAt: place.selectedAt ?? new Date().toISOString() }; } function placeMatchesInput(place, inputValue) { if (!place) return false; const input = String(inputValue ?? "").trim().toLowerCase(); if (!input) return false; return [place.displayName, place.formattedAddress].some((value) => String(value ?? "").trim().toLowerCase() === input); } function destinationPlaceMatchesInput(place, destination) { return placeMatchesInput(place, destination); } function pickupPlaceMatchesInput(place, pickup) { return placeMatchesInput(place, pickup); } function pickupPlaceForRoute(pickup = els.pickupDescription?.value) { const place = normalizedPlaceSelection(selectedPickupPlace); return pickupPlaceMatchesInput(place, pickup) ? place : null; } function currentPickupLocationLabel(point = pendingPickupGps) { const gps = normalizeGpsPoint(point); return gps ? "Current location" : ""; } function pickupUsesCurrentLocationText(value) { return /^current location\b/i.test(String(value ?? "").trim()); } function pickupUsesGpsFallbackText(value) { return /^verified gps pickup\b/i.test(String(value ?? "").trim()); } function destinationPlaceForRoute(destination = els.destination?.value) { const place = normalizedPlaceSelection(selectedDestinationPlace); return destinationPlaceMatchesInput(place, destination) ? place : null; } function stopPlaceKey(stop) { return String(stop ?? "").replace(/\s+/g, " ").trim().toLowerCase(); } function rememberSelectedStopPlace(place) { const normalized = normalizedPlaceSelection(place); if (!normalized) return; [normalized.formattedAddress, normalized.displayName].forEach((label) => { const key = stopPlaceKey(label); if (key) selectedStopPlaces.set(key, normalized); }); while (selectedStopPlaces.size > placeDetailsCacheLimit) { selectedStopPlaces.delete(selectedStopPlaces.keys().next().value); } } function stopPlaceForRoute(stop) { return normalizedPlaceSelection(selectedStopPlaces.get(stopPlaceKey(stop))); } function stopRoutePoint(stop) { const place = stopPlaceForRoute(stop); if (validGpsCoordinate(Number(place?.latitude), Number(place?.longitude))) { return { latitude: Number(place.latitude), longitude: Number(place.longitude) }; } return null; } function stopRouteAddress(stop, city, country) { const place = stopPlaceForRoute(stop); return { address: place?.formattedAddress || place?.displayName || typedRouteAddress(stop, city, country), placeId: place?.placeId ?? null, formattedAddress: place?.formattedAddress ?? null, latitude: place?.latitude ?? null, longitude: place?.longitude ?? null }; } function routeOriginForEstimate(country, city, pickupAreaName, pickupDescription, pickupGps = pendingPickupGps) { const pickupText = String(pickupDescription ?? "").trim(); const currentPickupSelected = typeof passengerWantsCurrentPickup === "function" ? passengerWantsCurrentPickup() : Boolean(els.pickupUseCurrentLocation?.checked); const rawSelectedPlace = pickupPlaceForRoute(pickupText); const selectedPlace = rawSelectedPlace?.source === "browser-gps" && !currentPickupSelected ? null : rawSelectedPlace; const selectedPlaceIsBrowserGps = selectedPlace?.source === "browser-gps"; const selectedPlaceGps = selectedPlaceIsBrowserGps && validGpsCoordinate(Number(selectedPlace?.latitude), Number(selectedPlace?.longitude)) ? normalizeGpsPoint({ latitude: selectedPlace.latitude, longitude: selectedPlace.longitude, accuracyMeters: selectedPlace.accuracyMeters ?? null, capturedAt: selectedPlace.capturedAt ?? null }) : null; const selectedGps = currentPickupSelected ? normalizeGpsPoint(selectedCurrentPickupGps) ?? selectedPlaceGps : selectedPlaceGps; const selectedPlaceHasRoutePoint = Boolean(selectedPlace?.placeId) || validGpsCoordinate(Number(selectedPlace?.latitude), Number(selectedPlace?.longitude)); if (selectedPlaceIsBrowserGps && selectedGps) { return { latitude: selectedGps.latitude, longitude: selectedGps.longitude, accuracyMeters: selectedGps.accuracyMeters ?? null, capturedAt: selectedGps.capturedAt ?? null, address: pickupText || selectedPlace.formattedAddress || selectedPlace.displayName || compactLocationQuery([pickupAreaName, city, country]), placeId: null, formattedAddress: selectedPlace.formattedAddress ?? pickupText ?? null, area: pickupAreaName, city, country, source: "browser-gps" }; } const useSelectedGps = selectedGps && (!pickupText || pickupUsesCurrentLocationText(pickupText) || pickupUsesGpsFallbackText(pickupText) || (selectedPlace && !selectedPlaceHasRoutePoint)); const gps = normalizeGpsPoint(useSelectedGps ? selectedGps : pickupGps); if (gps && selectedPlace && !selectedPlaceHasRoutePoint && selectedGps) { return { latitude: gps.latitude, longitude: gps.longitude, accuracyMeters: gps.accuracyMeters ?? null, capturedAt: gps.capturedAt ?? null, address: pickupText || selectedPlace.formattedAddress || selectedPlace.displayName || compactLocationQuery([pickupAreaName, city, country]), placeId: null, formattedAddress: selectedPlace.formattedAddress ?? pickupText ?? null, area: pickupAreaName, city, country, source: "browser-gps" }; } if (selectedPlace) { return { address: selectedPlace.formattedAddress || selectedPlace.displayName || compactLocationQuery([pickupText, pickupAreaName, city, country]), placeId: selectedPlace.placeId ?? null, formattedAddress: selectedPlace.formattedAddress ?? null, latitude: selectedPlace.latitude ?? null, longitude: selectedPlace.longitude ?? null, accuracyMeters: selectedPlace.accuracyMeters ?? null, capturedAt: selectedPlace.capturedAt ?? null, area: pickupAreaName, city, country, source: "google-places" }; } if (gps && (pickupUsesCurrentLocationText(pickupText) || pickupUsesGpsFallbackText(pickupText))) { return { latitude: gps.latitude, longitude: gps.longitude, accuracyMeters: gps.accuracyMeters ?? null, capturedAt: gps.capturedAt ?? null, address: pickupText || compactLocationQuery([pickupAreaName, city, country]), placeId: null, formattedAddress: null, area: pickupAreaName, city, country, source: "browser-gps" }; } const typedAddress = pickupText.length >= 6 ? typedRouteAddress(pickupText, city, country) : compactLocationQuery([pickupText, pickupAreaName, city, country]); if (pickupText.length >= 6) { return { address: typedAddress, placeId: null, formattedAddress: null, latitude: null, longitude: null, area: pickupAreaName, city, country, source: "typed-address" }; } if (gps) { return { latitude: gps.latitude, longitude: gps.longitude, accuracyMeters: gps.accuracyMeters ?? null, capturedAt: gps.capturedAt ?? null, address: typedAddress, placeId: null, formattedAddress: null, area: pickupAreaName, city, country, source: "browser-gps" }; } return { address: typedAddress, placeId: null, formattedAddress: null, latitude: null, longitude: null, area: pickupAreaName, city, country, source: "typed-address" }; } function routeOriginIsSpecific(origin) { return Boolean(origin?.placeId) || validGpsCoordinate(Number(origin?.latitude), Number(origin?.longitude)) || String(origin?.address ?? "").trim().length >= 6; } function requestPickupGpsFromRouteOrigin(origin, fallbackGps = pendingPickupGps) { if (validGpsCoordinate(Number(origin?.latitude), Number(origin?.longitude))) { const fallback = normalizeGpsPoint(fallbackGps); const originSource = String(origin?.source ?? "").trim().toLowerCase(); const addressAccuracyMeters = originSource === "google-places" ? addressPickupMatchAccuracyMeters : null; return normalizeGpsPoint({ latitude: Number(origin.latitude), longitude: Number(origin.longitude), accuracyMeters: origin.accuracyMeters ?? fallback?.accuracyMeters ?? addressAccuracyMeters, capturedAt: origin.capturedAt ?? fallback?.capturedAt ?? new Date().toISOString() }); } if (origin?.source !== "browser-gps") return null; return normalizeGpsPoint(selectedCurrentPickupGps) ?? normalizeGpsPoint(fallbackGps); } function routeEstimateRequestBody(country, city, pickupGps, destinationAreaName, destination, stops = [], pickupDescription = els.pickupDescription?.value, pickupAreaName = els.pickupArea?.value, destinationPlaceOverride = null) { const origin = routeOriginForEstimate(country, city, pickupAreaName, pickupDescription, pickupGps); const overridePlace = normalizedPlaceSelection(destinationPlaceOverride); const selectedPlace = destinationPlaceMatchesInput(overridePlace, destination) ? overridePlace : destinationPlaceForRoute(destination); return { origin, destination: { address: selectedPlace?.formattedAddress || selectedPlace?.displayName || destinationRouteAddress(destination, destinationAreaName, city, country), placeId: selectedPlace?.placeId ?? null, formattedAddress: selectedPlace?.formattedAddress ?? null, latitude: selectedPlace?.latitude ?? null, longitude: selectedPlace?.longitude ?? null, area: destinationAreaName, city, country }, stops: normalizeRideStops(stops).map((stop) => stopRouteAddress(stop, city, country).address), travelMode: "DRIVE" }; } async function currentSupabaseAccessToken() { if (supabaseClient?.auth?.getSession) { const { data } = await supabaseClient.auth.getSession(); return data?.session?.access_token ?? supabaseRestSession?.access_token ?? null; } return supabaseRestSession?.access_token ?? null; } async function fetchRouteEstimateFromEdge(body) { if (!hasSupabaseRuntime()) throw new Error("Supabase runtime is required for accurate route estimates."); await assertPlatformFeatureEnabled("route_estimates_enabled", "Route estimates"); const functionName = routeEstimateFunctionName(); const token = await currentSupabaseAccessToken(); if (!token) throw new Error("Passenger sign-in is required for accurate route estimates."); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${functionName}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify(body) }), "Fetching accurate route estimate", optionalSupabaseRequestTimeoutMs ); const payload = await response.json().catch(() => null); if (!response.ok) throw new Error(payload?.error || "Route estimate Edge Function failed."); return payload; } function fareGuidanceFromRouteEstimate(routeEstimate, stops = [], meta = {}) { const distanceMeters = Number(routeEstimate?.distanceMeters); const durationSeconds = Number(routeEstimate?.durationSeconds); if (!Number.isFinite(distanceMeters) || distanceMeters <= 0) return null; if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) return null; return fareGuidanceFromDistance( Math.max(0.6, distanceMeters * metersToMiles), Math.max(1, Math.ceil(durationSeconds / 60)), stops, { source: "google-routes", provider: routeEstimate.provider ?? "google-routes", cached: Boolean(routeEstimate.cached), routeKey: routeEstimate.routeKey ?? null, routePolyline: routeEstimate.routePolyline ?? null, country: meta.country, city: meta.city, destinationFingerprint: routeEstimate.destinationFingerprint ?? null, estimatedAt: routeEstimate.estimatedAt ?? new Date().toISOString() } ); } async function accurateFareGuidanceForRide(country, city, pickupAreaName, destinationAreaName, destination, pickupGps = pendingPickupGps, stops = [], pickupDescription = els.pickupDescription?.value, destinationPlaceOverride = null) { const fallback = routeEstimatesEnabled() ? null : fareGuidanceForRide(country, city, pickupAreaName, destinationAreaName, pickupGps, stops); if (!routeEstimatesEnabled()) return fallback; const localGuidance = immediateCoordinateFareGuidance(country, city, pickupAreaName, destinationAreaName, pickupDescription, destination, stops, destinationPlaceOverride, pickupGps); const body = routeEstimateRequestBody(country, city, pickupGps, destinationAreaName, destination, stops, pickupDescription, pickupAreaName, destinationPlaceOverride); if (!routeOriginIsSpecific(body.origin)) { if (accurateRouteEstimateRequired()) throw new Error("Pickup address or pickup GPS is required before accurate route pricing."); return null; } if (routeOriginNeedsConfirmedAddressForPricing(body.origin)) { throw new Error(currentLocationNeedsAddressMessage()); } if (!body.destination.address) { if (accurateRouteEstimateRequired()) throw new Error("Destination address is required before accurate route pricing."); return null; } try { lastRouteEstimateError = null; const estimate = await fetchRouteEstimateFromEdge(body); const guidance = fareGuidanceFromRouteEstimate(estimate, stops, { country, city }); if (!guidance) throw new Error("Google Routes did not return a usable driving distance."); const safeGuidance = safeRouteGuidance(guidance, localGuidance, "fare"); if (safeGuidance === guidance) lastRouteEstimateError = null; return safeGuidance; } catch (error) { logClientWarning("Accurate route estimate failed.", error); lastRouteEstimateError = error; if (routeEstimateFallbackAllowedForTesting() && localGuidance) return localGuidance; if (accurateRouteEstimateRequired() && !routeEstimateFallbackAllowedForTesting()) { throw new Error("Driving distance could not be confirmed. Please try again in a moment."); } return null; } } function validGpsCoordinate(latitude, longitude) { return Number.isFinite(latitude) && Number.isFinite(longitude) && latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180; } function normalizeGpsPoint(value) { if (!value) return null; const latitude = Number(value.latitude ?? value.lat); const longitude = Number(value.longitude ?? value.lng ?? value.lon); if (!validGpsCoordinate(latitude, longitude)) return null; const accuracyMeters = Number(value.accuracyMeters ?? value.accuracy); return { latitude, longitude, accuracyMeters: Number.isFinite(accuracyMeters) ? Math.round(accuracyMeters) : null, capturedAt: value.capturedAt ?? new Date().toISOString() }; } function gpsPointFromPosition(position) { return normalizeGpsPoint({ latitude: position.coords.latitude, longitude: position.coords.longitude, accuracyMeters: position.coords.accuracy, capturedAt: new Date(position.timestamp || Date.now()).toISOString() }); } function parseDatabasePointObject(value) { if (!value || typeof value !== "object") return null; if (Array.isArray(value) && value.length >= 2) { return { longitude: Number(value[0]), latitude: Number(value[1]) }; } if (Array.isArray(value.coordinates) && value.coordinates.length >= 2) { return { longitude: Number(value.coordinates[0]), latitude: Number(value.coordinates[1]) }; } return { latitude: Number(value.latitude ?? value.lat ?? value.y), longitude: Number(value.longitude ?? value.lng ?? value.lon ?? value.x) }; } function parseDatabasePointText(value) { const text = String(value ?? "").trim(); if (!text) return null; try { return parseDatabasePointObject(JSON.parse(text)); } catch { // Continue with WKT/EWKB parsing below. } const pointMatch = text.match(/^(?:SRID=\d+;)?POINT\s*\(\s*(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)\s*\)$/i); if (pointMatch) { return { longitude: Number(pointMatch[1]), latitude: Number(pointMatch[2]) }; } if (!/^[0-9a-f]+$/i.test(text) || text.length % 2 !== 0) return null; const bytes = Uint8Array.from(text.match(/.{2}/g).map((byte) => parseInt(byte, 16))); if (bytes.length < 21) return null; const view = new DataView(bytes.buffer); const littleEndian = view.getUint8(0) === 1; let geometryType = view.getUint32(1, littleEndian); let offset = 5; if (geometryType & 0x20000000) { offset += 4; geometryType &= ~0x20000000; } if ((geometryType & 0xff) !== 1 || bytes.length < offset + 16) return null; return { longitude: view.getFloat64(offset, littleEndian), latitude: view.getFloat64(offset + 8, littleEndian) }; } function gpsPointFromDatabaseLocation(value, accuracyMeters = null, capturedAt = null) { const point = typeof value === "string" ? parseDatabasePointText(value) : parseDatabasePointObject(value); if (!validGpsCoordinate(Number(point?.latitude), Number(point?.longitude))) return null; return normalizeGpsPoint({ latitude: point.latitude, longitude: point.longitude, accuracyMeters, capturedAt }); } function gpsPointToDatabase(value) { const point = normalizeGpsPoint(value); return point ? `SRID=4326;POINT(${point.longitude} ${point.latitude})` : null; } function gpsStatusLabel(value, emptyText = "GPS not shared") { const point = normalizeGpsPoint(value); if (!point) return emptyText; return "Location active"; } function passengerPickupGpsReadyLabel(value) { return normalizeGpsPoint(value) ? "Exact pickup location is ready." : "Exact pickup location is off."; } function gpsDistanceMetersBetween(a, b) { const first = normalizeGpsPoint(a); const second = normalizeGpsPoint(b); if (!first || !second) return null; return gpsDistanceKmBetween(first, second) * 1000; } function gpsAgeMinutes(point) { const capturedAt = point?.capturedAt; if (!capturedAt) return null; const capturedTime = new Date(capturedAt).getTime(); if (!Number.isFinite(capturedTime)) return null; return Math.max(0, Math.floor((Date.now() - capturedTime) / 60000)); } function pickupGpsQualityIssue(point) { const pickupGps = normalizeGpsPoint(point); if (!pickupGps) return null; const ageMinutes = gpsAgeMinutes(pickupGps); if (ageMinutes == null) { return "Exact pickup capture time is unavailable. Try again so nearby riders are matched from the correct pickup point."; } if (ageMinutes > passengerPickupGpsFreshMinutes) { return `Exact pickup location is ${ageMinutes} minutes old. Try again so nearby riders are matched to the current pickup point.`; } if (pickupGps.accuracyMeters == null) { return "Exact pickup accuracy is unavailable. Try again or enter the pickup address."; } if (pickupGps.accuracyMeters > passengerPickupGpsAccuracyLimitMeters()) { return "Exact pickup location needs a clearer signal. Try again from the pickup point or enter the pickup address."; } return null; } function riderLiveGpsQualityIssue(point) { const liveGps = normalizeGpsPoint(point); if (!liveGps) return null; const ageMinutes = gpsAgeMinutes(liveGps); if (ageMinutes == null) { return "Live rider GPS capture time is unavailable. Capture it again before sharing live GPS for matching."; } if (ageMinutes > riderLiveGpsFreshMinutes) { return `Live rider GPS is ${ageMinutes} minutes old. Capture it again so nearby passengers are matched to your current position.`; } if (liveGps.accuracyMeters == null) { return "Rider location quality is still loading. Keep Waka open for a moment, then activate again."; } if (liveGps.accuracyMeters > riderLiveGpsAccuracyLimitMeters()) { return "Rider location is not clear enough yet. Move into a clearer spot before activating ride matching."; } return null; } function formatGpsAgeLabel(point) { const ageMinutes = gpsAgeMinutes(point); if (ageMinutes == null) return "capture time unknown"; if (ageMinutes === 0) return "captured just now"; return `captured ${ageMinutes} min ago`; } function pickupGpsQualityChip(request) { if (activeRole() !== "rider") return null; const pickupGps = requestPickupGps(request); if (!request?.pickupLocationShared && !pickupGps) return null; return "Pickup location verified"; } function requestPickupGps(request) { return normalizeGpsPoint(request?.pickupGps ?? { latitude: request?.pickupLatitude, longitude: request?.pickupLongitude, accuracyMeters: request?.pickupGpsAccuracyMeters, capturedAt: request?.pickupGpsCapturedAt }); } function fareHistoryTrail(source, currentFare, currentCreatedAt = null) { const rows = Array.isArray(source?.fareHistory ?? source?.fare_history) ? (source.fareHistory ?? source.fare_history) : []; const trail = rows .map((entry) => ({ fare: Number(entry?.fare ?? entry?.amount ?? entry?.fare_xaf ?? entry), createdAt: entry?.createdAt ?? entry?.created_at ?? currentCreatedAt ?? source?.createdAt ?? source?.created_at ?? null })) .filter((entry) => Number.isFinite(entry.fare) && entry.fare > 0) .sort((a, b) => new Date(a.createdAt ?? 0).getTime() - new Date(b.createdAt ?? 0).getTime()) .reduce((deduped, entry) => { const previous = deduped[deduped.length - 1]; if (!previous || Number(previous.fare) !== Number(entry.fare)) deduped.push(entry); return deduped; }, []); const resolvedCurrentFare = Number(currentFare); if (Number.isFinite(resolvedCurrentFare) && resolvedCurrentFare > 0 && (!trail.length || Number(trail[trail.length - 1].fare) !== resolvedCurrentFare)) { trail.push({ fare: resolvedCurrentFare, createdAt: currentCreatedAt ?? source?.updatedAt ?? source?.createdAt ?? null }); } return trail; } function fareChangeFromTrail(trail) { if (!Array.isArray(trail) || trail.length < 2) return null; const initialFare = Number(trail[0]?.fare); const currentFare = Number(trail[trail.length - 1]?.fare); const delta = currentFare - initialFare; if (!Number.isFinite(delta) || delta === 0) return null; return { direction: delta > 0 ? "up" : "down", amount: Math.abs(delta), previousFare: initialFare, currentFare, changedAt: trail[trail.length - 1]?.createdAt ?? new Date().toISOString() }; } function fareChangeChipFromTrail(trail, country, prefix = "") { const change = fareChangeFromTrail(trail); if (!change) return null; const direction = change.direction === "up" ? "\u2191" : "\u2193"; return `${prefix}${direction} ${formatMoney(change.amount, country)} from ${formatMoney(change.previousFare, country)}`; } function requestMarketplaceFareChange(nextRequest, previousRequest) { if (!nextRequest || nextRequest.status !== "open") return null; const historyChange = fareChangeFromTrail(fareHistoryTrail(nextRequest, nextRequest.fareOffer, nextRequest.updatedAt ?? nextRequest.createdAt)); if (historyChange?.direction === "up") return historyChange; const currentFare = Number(nextRequest.fareOffer); const previousFare = Number(previousRequest?.fareOffer); if (Number.isFinite(currentFare) && Number.isFinite(previousFare) && currentFare > previousFare) { return { direction: "up", amount: currentFare - previousFare, previousFare, currentFare, changedAt: new Date().toISOString() }; } const existingChange = normalizeMarketplaceFareChange(previousRequest?.marketplaceFareChange); if (existingChange && Number.isFinite(currentFare) && Number(existingChange.currentFare) === currentFare) { return existingChange; } return null; } function preserveRideRequestPickup(nextRequest, previousRequest) { if (!nextRequest) return nextRequest; const previousGps = requestPickupGps(previousRequest); const nextGps = requestPickupGps(nextRequest); const pickupGps = nextGps ?? previousGps; const pickupDescription = String(nextRequest.pickupDescription ?? "").trim() ? nextRequest.pickupDescription : previousRequest?.pickupDescription ?? nextRequest.pickupDescription; const pickupArea = String(nextRequest.pickupArea ?? "").trim() ? nextRequest.pickupArea : previousRequest?.pickupArea ?? nextRequest.pickupArea; return { ...nextRequest, pickupArea, pickupDescription, pickupLocationShared: Boolean(nextRequest.pickupLocationShared || previousRequest?.pickupLocationShared || pickupGps), pickupGps: pickupGps ?? nextRequest.pickupGps ?? previousRequest?.pickupGps ?? null, pickupLatitude: pickupGps?.latitude ?? nextRequest.pickupLatitude ?? previousRequest?.pickupLatitude ?? null, pickupLongitude: pickupGps?.longitude ?? nextRequest.pickupLongitude ?? previousRequest?.pickupLongitude ?? null, pickupGpsAccuracyMeters: pickupGps?.accuracyMeters ?? nextRequest.pickupGpsAccuracyMeters ?? previousRequest?.pickupGpsAccuracyMeters ?? null, pickupGpsCapturedAt: pickupGps?.capturedAt ?? nextRequest.pickupGpsCapturedAt ?? previousRequest?.pickupGpsCapturedAt ?? null, marketplaceFareChange: requestMarketplaceFareChange(nextRequest, previousRequest) }; } function riderMarketplaceFareChangeChip(request) { if (activeRole() !== "rider" || request?.status !== "open") return null; const change = normalizeMarketplaceFareChange(request?.marketplaceFareChange); const currentFare = Number(request?.fareOffer); if (!change || change.direction !== "up" || !Number.isFinite(currentFare) || Number(change.currentFare) !== currentFare) return null; return `\u2191 ${formatMoney(change.amount, request.country)} from ${formatMoney(change.previousFare, request.country)}`; } function clearRequestMarketplaceFareChange(requestId) { if (!requestId) return; let changed = false; state.requests = state.requests.map((request) => { if (request.id !== requestId || !request.marketplaceFareChange) return request; changed = true; return { ...request, marketplaceFareChange: null }; }); if (changed) clearStateLookupIndexes(); } function pickupGpsIsUsableForMatching(request) { const pickupGps = requestPickupGps(request); if (!pickupGps) return false; if (pickupGps.accuracyMeters == null || pickupGps.accuracyMeters > passengerPickupGpsAccuracyLimitMeters()) return false; const capturedTime = pickupGps.capturedAt ? new Date(pickupGps.capturedAt).getTime() : null; const createdTime = request?.createdAt ? new Date(request.createdAt).getTime() : null; if (!Number.isFinite(capturedTime)) return false; if (Number.isFinite(capturedTime) && Number.isFinite(createdTime)) { const maxAgeMs = passengerPickupGpsFreshMinutes * 60000; if (capturedTime < createdTime - maxAgeMs) return false; if (capturedTime > createdTime + 5 * 60000) return false; } else { const currentAgeMs = Date.now() - capturedTime; if (currentAgeMs > passengerPickupGpsFreshMinutes * 60000) return false; if (currentAgeMs < -5 * 60000) return false; } return true; } function requestPickupGpsForMatching(request) { if (!pickupGpsIsUsableForMatching(request)) return null; return requestPickupGps(request); } function riderCurrentGps(rider) { return normalizeGpsPoint(rider?.currentGps ?? { latitude: rider?.currentLatitude, longitude: rider?.currentLongitude, accuracyMeters: rider?.currentGpsAccuracyMeters, capturedAt: rider?.currentGpsCapturedAt }); } function clearRiderLiveGpsFields(rider) { if (!rider) return rider; return { ...rider, currentGps: null, currentLatitude: null, currentLongitude: null, currentGpsAccuracyMeters: null, currentGpsCapturedAt: null }; } function saveCurrentRiderRecord(rider) { if (!rider) return; state.rider = state.rider?.id === rider.id ? rider : state.rider; state.riders = upsertById(state.riders, rider); saveState(); } function riderLiveGpsAgeMinutes(rider) { const capturedAt = rider?.currentGps?.capturedAt ?? rider?.currentGpsCapturedAt; if (!capturedAt) return null; const capturedTime = new Date(capturedAt).getTime(); if (!Number.isFinite(capturedTime)) return null; return Math.max(0, Math.floor((Date.now() - capturedTime) / 60000)); } function riderLiveGpsIsFresh(rider = currentRiderRecord()) { const ageMinutes = riderLiveGpsAgeMinutes(rider); return ageMinutes != null && ageMinutes <= riderLiveGpsFreshMinutes; } function riderLiveGpsIsUsable(rider = currentRiderRecord()) { const currentGps = riderCurrentGps(rider); return Boolean(currentGps && riderLiveGpsIsFresh(rider) && !riderLiveGpsQualityIssue(currentGps)); } function riderCurrentFreshGps(rider = currentRiderRecord()) { if (!riderLiveGpsIsUsable(rider)) return null; return riderCurrentGps(rider); } function riderLiveGpsStatusSummary(rider = currentRiderRecord()) { if (!riderCurrentGps(rider)) return `Live GPS required before receiving requests.`; const ageMinutes = riderLiveGpsAgeMinutes(rider); if (ageMinutes == null) return `Live GPS needs a fresh capture before receiving requests.`; const qualityIssue = riderLiveGpsQualityIssue(riderCurrentGps(rider)); if (qualityIssue) return qualityIssue; if (ageMinutes <= riderLiveGpsFreshMinutes) { const remaining = Math.max(1, riderLiveGpsFreshMinutes - ageMinutes); return `Live GPS active for about ${remaining} min.`; } return `Live GPS expired ${ageMinutes} min ago; automatic refresh is needed for GPS matching.`; } function riderLiveGpsNeedsClearing(rider = currentRiderRecord()) { return Boolean(riderCurrentGps(rider) && !riderLiveGpsIsUsable(rider)); } function gpsDistanceKmBetween(first, second) { const a = normalizeGpsPoint(first); const b = normalizeGpsPoint(second); if (!a || !b) return null; const toRadians = (value) => (value * Math.PI) / 180; const lat1 = toRadians(a.latitude); const lat2 = toRadians(b.latitude); const deltaLat = toRadians(b.latitude - a.latitude); const deltaLng = toRadians(b.longitude - a.longitude); const haversine = Math.sin(deltaLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(deltaLng / 2) ** 2; return 6371 * 2 * Math.atan2(Math.sqrt(haversine), Math.sqrt(1 - haversine)); } function gpsDistanceKmForRequest(request, rider = currentRiderRecord()) { if (request?.matchSource === "area_fallback") return null; const rpcDistance = request?.gpsDistanceMeters; if (rpcDistance !== null && rpcDistance !== undefined && Number.isFinite(Number(rpcDistance))) { return Number(rpcDistance) / 1000; } return gpsDistanceKmBetween(requestPickupGpsForMatching(request), riderCurrentFreshGps(rider)); } function riderProximityToRequest(request, rider = currentRiderRecord()) { if (!request || !rider || request.country !== rider.country || request.city !== rider.city) return null; const pickup = findArea(request.country, request.city, request.pickupArea); const riderArea = findArea(rider.country, rider.city, rider.area); const distanceKm = estimatedAreaDistanceKm(request.country, request.city, pickup, riderArea); if (distanceKm == null) return null; return { distanceKm, pickupArea: pickup?.name ?? request.pickupArea, riderArea: riderArea?.name ?? rider.area, limit: riderServiceRadius(rider, request), label: distanceKm < 1 ? "Closest pickup area" : distanceKm <= 3 ? "Near pickup area" : "Within service range" }; } function riderWithinRequestProximity(request, rider = currentRiderRecord()) { const proximity = riderProximityToRequest(request, rider); return Boolean(proximity && proximity.distanceKm <= proximity.limit && riderWithinPickupEta(proximity.distanceKm, rider, request)); } function riderWithinGpsProximity(request, rider = currentRiderRecord()) { if (!request || !rider) return false; const distanceKm = gpsDistanceKmForRequest(request, rider); return distanceKm != null && distanceKm <= riderServiceRadius(rider, request) && riderWithinPickupEta(distanceKm, rider, request); } function riderPickupMaxEtaMinutesForRequest(request) { return isScheduledRequest(request) ? scheduledRiderPickupMaxEtaMinutes : riderPickupMaxEtaMinutes; } function riderWithinPickupEta(distanceKm, rider = currentRiderRecord(), request = null) { const eta = pickupEtaMinutes(distanceKm, rider); return eta != null && eta <= riderPickupMaxEtaMinutesForRequest(request); } function pickupProximityModel(request, rider = currentRiderRecord()) { if (!request || !rider) return null; const gpsDistanceKm = gpsDistanceKmForRequest(request, rider); if (gpsDistanceKm != null) { return { source: request.matchSource === "postgis" ? "GPS/PostGIS" : "GPS", distanceKm: gpsDistanceKm, etaMinutes: pickupEtaMinutes(gpsDistanceKm, rider), label: "GPS pickup" }; } const proximity = riderProximityToRequest(request, rider); if (!proximity) return null; return { source: "Area estimate", distanceKm: proximity.distanceKm, etaMinutes: pickupEtaMinutes(proximity.distanceKm, rider), label: proximity.label }; } function pickupProximitySortValue(request, rider = currentRiderRecord()) { return pickupProximityModel(request, rider)?.etaMinutes ?? Number.POSITIVE_INFINITY; } function pickupAreasWithinRiderRadius(rider = currentRiderRecord()) { if (!rider) return []; const riderArea = findArea(rider.country, rider.city, rider.area); const limit = riderServiceRadius(rider); const nearbyAreas = areas(rider.country, rider.city) .filter((area) => { const distanceKm = estimatedAreaDistanceKm(rider.country, rider.city, area, riderArea); return distanceKm != null && distanceKm <= limit; }) .map((area) => area.name); return nearbyAreas.length ? nearbyAreas : [rider.area].filter(Boolean); } function riderActiveImmediateRide(rider = currentRiderRecord()) { if (!rider) return null; return state.requests.find((request) => selectedRiderIdForRequest(request) === rider.id && !isScheduledRequest(request) && ["matched", "arrived", "in_progress"].includes(request.status)) ?? null; } function riderInProgressImmediateRide(rider = currentRiderRecord()) { if (!rider) return null; return state.requests.find((request) => selectedRiderIdForRequest(request) === rider.id && !isScheduledRequest(request) && request.status === "in_progress") ?? null; } function requestDestinationGps(request) { return normalizeGpsPoint({ latitude: request?.destinationLatitude, longitude: request?.destinationLongitude }); } function riderDropoffEtaMinutes(request, rider = currentRiderRecord()) { const riderGps = riderCurrentFreshGps(rider); const destinationGps = requestDestinationGps(request); if (!riderGps || !destinationGps) return null; const distanceMeters = gpsDistanceMetersBetween(riderGps, destinationGps); if (distanceMeters == null || !Number.isFinite(Number(distanceMeters))) return null; return pickupEtaMinutes(Number(distanceMeters) / 1000, rider); } function riderInProgressRideNearDropoff(rider = currentRiderRecord()) { const ride = riderInProgressImmediateRide(rider); if (!ride) return false; const eta = riderDropoffEtaMinutes(ride, rider); return eta != null && eta <= riderDropoffRequestLeadMinutes; } function riderBlockingImmediateRide(rider = currentRiderRecord()) { const ride = riderActiveImmediateRide(rider); if (!ride) return null; if (ride.status === "in_progress" && riderInProgressRideNearDropoff(rider)) return null; return ride; } function riderCanReviewAnotherImmediateRequest(request, rider = currentRiderRecord()) { const blockingRide = riderBlockingImmediateRide(rider); return !blockingRide || blockingRide.id === request?.id; } function riderShouldHoldNextRideNavigation(request, rider = currentRiderRecord()) { const inProgressRide = riderInProgressImmediateRide(rider); return Boolean(inProgressRide && request && inProgressRide.id !== request.id && !isScheduledRequest(request)); } function riderPickupNavigationShouldWaitForDropoff(request, rider = currentRiderRecord()) { return Boolean(request && request.status === "matched" && riderShouldHoldNextRideNavigation(request, rider)); } function queuedRiderPickupAfterDropoff(completedRequestId = "", rider = currentRiderRecord()) { if (!rider?.id) return null; return state.requests .filter((request) => request.id !== completedRequestId && selectedRiderIdForRequest(request) === rider.id && request.status === "matched" && !isScheduledRequest(request)) .sort((a, b) => new Date(a.matchedAt ?? a.createdAt ?? 0).getTime() - new Date(b.matchedAt ?? b.createdAt ?? 0).getTime())[0] ?? null; } function selectedRequest() { const request = stateLookupIndexes().requestMap.get(state.selectedRequestId) ?? null; if (activeRole() === "passenger" && request && !passengerRideRequestVisibleInActiveBoard(request)) return null; return request; } function selectedPassengerCountry() { const country = state.passenger?.country ?? els.passengerCountry?.value ?? els.passengerActiveCountry?.value ?? defaultLaunchCountry(); return enabledLaunchCountries().includes(country) ? country : defaultLaunchCountry(); } function selectedPassengerCity() { const country = selectedPassengerCountry(); const city = state.passenger?.city ?? els.passengerCity?.value ?? els.passengerActiveCity?.value ?? defaultLaunchCity(country); return cityNames(country).includes(city) ? city : defaultLaunchCity(country); } function selectedRiderCountry() { const country = state.rider?.country ?? els.riderActiveCountry?.value ?? els.riderCountry?.value ?? defaultLaunchCountry(); return enabledLaunchCountries().includes(country) ? country : defaultLaunchCountry(); } function selectedRiderCity() { const country = selectedRiderCountry(); const city = state.rider?.city ?? els.riderActiveCity?.value ?? els.riderCity?.value ?? defaultLaunchCity(country); return cityNames(country).includes(city) ? city : defaultLaunchCity(country); } function activeRole() { return availableWorkspaceTab(state.activeTab) ?? defaultRuntimeTab(); } function currentRiderRecord() { if (!state.rider) return null; const indexed = stateLookupIndexes().riderMap.get(state.rider.id); return indexed ? { ...indexed, ...state.rider } : state.rider; } function requestBelongsToPassenger(request) { return Boolean(request && state.passenger && request.passengerId === state.passenger.id); } function passengerPendingRide(requests = state.requests) { if (!state.passenger) return null; return requests.find((request) => requestBelongsToPassenger(request) && ["open", "matched", "arrived", "in_progress"].includes(request.status)) ?? null; } function passengerActiveRideRequestStatuses() { return ["open", "matched", "arrived", "in_progress"]; } function passengerRideRequestVisibleInActiveBoard(request) { return Boolean(requestBelongsToPassenger(request) && passengerActiveRideRequestStatuses().includes(request.status)); } function focusPassengerPendingRideAfterMarketplaceSync() { const pendingRide = passengerPendingRide(); if (!pendingRide) return false; state.selectedRequestId = pendingRide.id; state.passengerPage = "trips"; state.activeTab = "passenger"; state.showRoleEntry = false; saveState(); return true; } function passengerPendingRideMessage(request = passengerPendingRide()) { if (!request) return ""; const status = rideStatusLabel(request).toLowerCase(); return `You already have a ${status} ride request. Complete or cancel it before requesting another ride.`; } function offerBelongsToRider(offer) { return riderIdentityMatches(offer?.riderId); } function selectedRiderIdForRequest(request) { if (!request) return null; const indexes = stateLookupIndexes(); const requestOffers = indexes.offersByRequestId.get(request.id) ?? []; const explicitSelectedOffer = indexes.offerMap.get(request.selectedOfferId) ?? null; const storedRiderOffer = requestOffers.find((offer) => request.selectedRiderId && offer.riderId === request.selectedRiderId) ?? null; const acceptedOffer = requestOffers.find((offer) => offer.type === "accepted") ?? null; const onlyCompletedOffer = request.status === "completed" && requestOffers.length === 1 ? requestOffers[0] : null; const completedRideSettlement = request.status === "completed" ? (state.rideSettlements ?? []).find((settlement) => settlement.requestId === request.id && settlement.riderId) : null; if (request.status === "completed") { return completedRideSettlement?.riderId ?? explicitSelectedOffer?.riderId ?? storedRiderOffer?.riderId ?? acceptedOffer?.riderId ?? onlyCompletedOffer?.riderId ?? request.selectedRiderId ?? null; } return request.selectedRiderId ?? explicitSelectedOffer?.riderId ?? storedRiderOffer?.riderId ?? acceptedOffer?.riderId ?? null; } function requestHasSelectedOffer(request) { return Boolean(request?.selectedOfferId || request?.selected_offer_id); } function riderIdentityIds(rider = currentRiderRecord()) { return uniqueMarketplaceIds([ rider?.id, rider?.supabaseUserId, state.sessions?.rider?.userId ]).map((id) => String(id)); } function riderIdentityMatches(id, rider = currentRiderRecord()) { const value = String(id ?? ""); return Boolean(value && riderIdentityIds(rider).includes(value)); } function selectedRiderNameForRequest(request) { if (!request) return null; const selectedRiderId = selectedRiderIdForRequest(request); const rider = stateLookupIndexes().riderMap.get(selectedRiderId); const completedRideSettlement = request.status === "completed" ? (state.rideSettlements ?? []).find((settlement) => settlement.requestId === request.id && settlement.riderId === selectedRiderId) : null; return request.selectedRiderName ?? rider?.name ?? completedRideSettlement?.riderName ?? null; } function firstNameOnly(name, fallback = "Matched contact") { const normalized = String(name ?? "").trim().replace(/\s+/g, " "); return normalized ? normalized.split(" ")[0] : fallback; } function passengerFirstNameForRequest(request) { return firstNameOnly(request?.passengerName, "Passenger"); } function selectedRiderFirstNameForRequest(request) { return firstNameOnly(selectedRiderNameForRequest(request), "Rider"); } function requestHasRiderMatch(request) { if (!request || !state.rider) return false; return riderIdentityMatches(selectedRiderIdForRequest(request)); } function requestIsActiveForCurrentRider(request, rider = currentRiderRecord()) { return Boolean(request && rider && !riderPrePickupCancellationClearedForCurrentRider(request, rider) && riderIdentityMatches(selectedRiderIdForRequest(request), rider) && ["matched", "arrived", "in_progress"].includes(request.status)); } function requestPickupCanShowPreciseText(request) { if (!request) return false; if (activeRole() === "passenger") return requestBelongsToPassenger(request); if (activeRole() === "rider") return requestIsActiveForCurrentRider(request); return activeRole() === "admin"; } function requestPickupAreaText(request, fallback = "Pickup") { const town = requestPickupTownText(request, ""); if (town) return town; const area = String(request?.pickupArea ?? "").trim(); if (area) return area; if (requestPickupGps(request) || request?.pickupLocationShared) return "Verified pickup location"; return String(request?.city ?? "").trim() || fallback; } function requestPickupDisplayText(request, fallback = "Pickup") { if (activeRole() === "rider" && !requestPickupCanShowPreciseText(request)) { return requestPickupAreaText(request, fallback); } const description = String(request?.pickupFormattedAddress ?? request?.pickupDescription ?? "").trim(); if (description && !pickupUsesGpsFallbackText(description)) return description; if (requestPickupGps(request) || request?.pickupLocationShared) return "Verified pickup location"; return String(request?.pickupArea ?? "").trim() || fallback; } function stripPostalFromAddressRegion(value) { return String(value ?? "") .replace(/\b\d{4,}(?:-\d{4})?\b.*$/, "") .replace(/\s+[A-Z]\d[A-Z]\s*\d[A-Z]\d.*$/i, "") .trim(); } function addressTownLabel(address) { const parts = String(address ?? "") .split(",") .map((part) => part.trim()) .filter(Boolean); if (!parts.length) return ""; const last = parts[parts.length - 1]?.toLowerCase(); const withoutCountry = ["usa", "us", "united states", "cameroon"].includes(last) ? parts.slice(0, -1) : parts; if (withoutCountry.length >= 3 && /\d/.test(withoutCountry[0])) { const region = stripPostalFromAddressRegion(withoutCountry[2]); return [withoutCountry[1], region].filter(Boolean).join(", "); } if (withoutCountry.length >= 2) { const region = stripPostalFromAddressRegion(withoutCountry[1]); return [withoutCountry[0], region].filter(Boolean).join(", "); } return withoutCountry[0] ?? ""; } function nearestKnownGpsTownLabel(request, maxDistanceKm = 10) { const gps = requestPickupGps(request); const townCenters = launchGpsTownCenters?.[request?.country]?.[request?.city] ?? []; if (!gps || !townCenters.length) return ""; const nearest = townCenters .map((town) => ({ name: town.name, distanceKm: gpsDistanceKmBetween(gps, { latitude: town.latitude, longitude: town.longitude }) })) .filter((town) => town.name && Number.isFinite(Number(town.distanceKm))) .sort((a, b) => a.distanceKm - b.distanceKm)[0]; return nearest && nearest.distanceKm <= maxDistanceKm ? nearest.name : ""; } function matchingLaunchAreaName(country, city, label) { const town = String(label ?? "").split(",")[0]?.trim().toLowerCase(); if (!town) return ""; return areas(country, city).find((area) => area.name.toLowerCase() === town)?.name ?? ""; } function nearestKnownGpsAreaName(country, city, gps, maxDistanceKm = 10) { const townCenters = launchGpsTownCenters?.[country]?.[city] ?? []; const point = normalizeGpsPoint(gps); if (!point || !townCenters.length) return ""; const nearest = townCenters .map((town) => ({ name: matchingLaunchAreaName(country, city, town.name), distanceKm: gpsDistanceKmBetween(point, { latitude: town.latitude, longitude: town.longitude }) })) .filter((town) => town.name && Number.isFinite(Number(town.distanceKm))) .sort((a, b) => a.distanceKm - b.distanceKm)[0]; return nearest && nearest.distanceKm <= maxDistanceKm ? nearest.name : ""; } function pickupAreaForPublish(country, city, selectedArea, pickupDescription, pickupGps) { const addressTown = pickupUsesCurrentLocationText(pickupDescription) || pickupUsesGpsFallbackText(pickupDescription) ? "" : addressTownLabel(pickupDescription); return matchingLaunchAreaName(country, city, addressTown) || nearestKnownGpsAreaName(country, city, pickupGps) || selectedArea; } function destinationAreaForPublish(country, city, selectedArea, destination, destinationPlace) { const destinationTown = addressTownLabel(destinationPlace?.formattedAddress || destination); return matchingLaunchAreaName(country, city, destinationTown) || selectedArea; } function requestPickupTownText(request, fallback = "Pickup") { const address = String(request?.pickupFormattedAddress ?? request?.pickupDescription ?? "").trim(); const addressTown = pickupUsesCurrentLocationText(address) || pickupUsesGpsFallbackText(address) ? "" : addressTownLabel(address); if (addressTown) return addressTown; const gpsTown = nearestKnownGpsTownLabel(request); if (gpsTown) return gpsTown; if (requestPickupGps(request) || request?.pickupLocationShared) return "Verified GPS pickup"; return String(request?.pickupArea ?? "").trim() || String(request?.city ?? "").trim() || fallback; } function requestDestinationTownText(request, fallback = "Destination") { const displayRequest = typeof riderVisibleRouteRequest === "function" ? riderVisibleRouteRequest(request) : request; const addressTown = addressTownLabel(displayRequest?.destinationFormattedAddress || displayRequest?.destination); return addressTown || String(displayRequest?.destinationArea ?? "").trim() || String(displayRequest?.city ?? "").trim() || fallback; } function requestDestinationPreviewText(request) { const displayRequest = typeof riderVisibleRouteRequest === "function" ? riderVisibleRouteRequest(request) : request; const destination = requestDestinationTownText(displayRequest); const stopCount = normalizeRideStops(displayRequest?.rideStops).length; return stopCount ? `${stopCount} stop${stopCount === 1 ? "" : "s"} then ${destination}` : destination; } function requestDestinationDisplayText(request) { const displayRequest = typeof riderVisibleRouteRequest === "function" ? riderVisibleRouteRequest(request) : request; if (activeRole() === "rider" && !requestIsActiveForCurrentRider(request)) { return requestDestinationPreviewText(displayRequest); } return requestDestinationText(displayRequest); } function riderCanShowOfferControls(rider = currentRiderRecord(), request = selectedRequest()) { if (activeRole() !== "rider") return false; if (!riderCanSeeRequests(rider)) return false; if (!request) return false; return request.status === "open" && roleCanSeeRequest(request) && !requestIsActiveForCurrentRider(request); } function riderCanLeaveSelectedRequest(rider = currentRiderRecord(), request = selectedRequest()) { if (activeRole() !== "rider" || !rider?.id || !request?.id) return false; if (["completed", "cancelled"].includes(request.status)) return false; if (requestIsActiveForCurrentRider(request, rider)) return false; if (request.status === "open") return true; return state.offers.some((offer) => offer.requestId === request.id && offer.riderId === rider.id); } function activeMarketLocation() { const request = selectedRequest(); if (request && activeRole() !== "admin" && roleCanSeeRequest(request)) return { country: request.country, city: request.city }; if (activeRole() === "rider") return { country: selectedRiderCountry(), city: selectedRiderCity() }; return { country: selectedPassengerCountry(), city: selectedPassengerCity() }; } function requestMatchesVehicleFilter(request) { if (activeRole() === "rider") return true; return state.filter === "all" || request.vehicle === "car"; } function riderMarketplaceDestinationFilter() { return normalizeRiderMarketplaceDestinationFilter(state.riderMarketplaceDestinationFilter); } function riderMarketplaceDestinationFilterIsActive(filter = riderMarketplaceDestinationFilter()) { return Boolean(filter.enabled && filter.consent && (filter.country || filter.city || filter.area || filter.query)); } function riderMarketplaceDestinationFilterSummary(filter = riderMarketplaceDestinationFilter()) { if (!riderMarketplaceDestinationFilterIsActive(filter)) return "Showing all nearby rides."; const parts = [ filter.area, filter.city, filter.country, filter.query ? `contains "${filter.query}"` : "" ].filter(Boolean); const nearby = filter.area ? `, including nearby towns within ${riderDestinationFilterNeighborRadiusMiles} miles` : ""; return `Showing rides headed toward ${parts.join(", ")}${nearby}.`; } function requestDestinationFilterSearchText(request) { return [ request?.country, request?.city, request?.destinationArea, request?.destination, request?.destinationFormattedAddress, requestDestinationText(request), ...normalizeRideStops(request?.rideStops) ].map((value) => String(value ?? "").trim().toLowerCase()).filter(Boolean).join(" "); } function launchGpsTownCenterForName(country, city, name) { const town = String(name ?? "").split(",")[0]?.trim().toLowerCase(); if (!town) return null; const center = (launchGpsTownCenters?.[country]?.[city] ?? []) .find((item) => String(item?.name ?? "").trim().toLowerCase() === town); return center ? normalizeGpsPoint({ latitude: center.latitude, longitude: center.longitude }) : null; } function requestDestinationFilterPoint(request) { const directGps = requestDestinationGps(request); if (directGps) return directGps; const country = request?.country; const city = request?.city; const candidates = [ request?.destinationArea, addressTownLabel(request?.destinationFormattedAddress || request?.destination), requestDestinationTownText(request) ]; for (const candidate of candidates) { const areaName = matchingLaunchAreaName(country, city, candidate) || candidate; const point = launchGpsTownCenterForName(country, city, areaName); if (point) return point; } return null; } function riderDestinationFilterPreferenceDistanceKm(request, filter = riderMarketplaceDestinationFilter(), searchText = null) { if (!riderMarketplaceDestinationFilterIsActive(filter) || !filter.area) return null; const haystack = searchText ?? requestDestinationFilterSearchText(request); const areaNeedle = filter.area.toLowerCase(); if (request?.destinationArea === filter.area || haystack.includes(areaNeedle)) return 0; const selectedAreaPoint = launchGpsTownCenterForName(filter.country || request?.country, filter.city || request?.city, filter.area); const requestDestinationPoint = requestDestinationFilterPoint(request); const distanceKm = gpsDistanceKmBetween(selectedAreaPoint, requestDestinationPoint); return Number.isFinite(Number(distanceKm)) ? Number(distanceKm) : null; } function riderDestinationFilterSortValue(request) { const distanceKm = riderDestinationFilterPreferenceDistanceKm(request); return distanceKm == null ? Number.POSITIVE_INFINITY : distanceKm; } function requestMatchesRiderMarketplaceDestinationFilter(request) { if (activeRole() !== "rider") return true; const filter = riderMarketplaceDestinationFilter(); if (!riderMarketplaceDestinationFilterIsActive(filter)) return true; if (requestIsActiveForCurrentRider(request)) return true; const searchText = requestDestinationFilterSearchText(request); if (filter.country && request?.country !== filter.country && !searchText.includes(filter.country.toLowerCase())) return false; if (filter.city) { const cityNeedle = filter.city.toLowerCase(); const cityMatches = request?.city === filter.city || searchText.includes(cityNeedle) || String(request?.destinationFormattedAddress ?? request?.destination ?? "").toLowerCase().includes(cityNeedle) || addressTownLabel(request?.destinationFormattedAddress || request?.destination).toLowerCase().includes(cityNeedle); if (!cityMatches) return false; } if (filter.area) { const preferenceDistanceKm = riderDestinationFilterPreferenceDistanceKm(request, filter, searchText); if (preferenceDistanceKm == null || preferenceDistanceKm > riderDestinationFilterNeighborRadiusMiles / kmToMiles) return false; } if (filter.query && !searchText.includes(filter.query.toLowerCase())) return false; return true; } function requestMatchesRiderVehicle(request, rider = currentRiderRecord()) { if (!request || !rider) return false; if (request.vehicle !== "car" || rider.vehicle !== "car") return false; return riderCanServeCarTypePreference(rider, request.carTypePreference); } function isScheduledRequest(request) { return Boolean(request?.scheduledAt); } function scheduleChip(request) { return isScheduledRequest(request) ? `Scheduled: ${formatDateTime(request.scheduledAt)}` : "Immediate ride"; } function requestReopenedAfterRiderCancellation(request, previousRequest = null) { const wasMatchedToRider = previousRequest?.selectedRiderId && ["matched", "arrived"].includes(previousRequest.status); const looksLikeServerReopen = request?.status === "open" && request.releasedAt && !request.selectedOfferId && !request.cancelledAt && !request.cancelledBy; return Boolean(request && request.status === "open" && request.releasedAt && !request.selectedOfferId && (looksLikeServerReopen || (request.cancelledBy && request.cancelledBy !== request.passengerId) || wasMatchedToRider)); } function requestCancelledByMatchedRider(request) { if (requestCancelledByPassenger(request)) return false; const riderId = selectedRiderIdForRequest(request) ?? request?.cancellationFeeRiderId; return Boolean(request?.cancelledBy && riderId && request.cancelledBy === riderId); } function requestCancelledByCurrentRider(request) { if (!request) return false; if (requestCancelledByPassenger(request) && !riderInitiatedRideCancellationRequestIds.has(request.id)) return false; if (riderInitiatedRideCancellationRequestIds.has(request.id)) return true; const riderIds = [ state.rider?.id, state.rider?.supabaseUserId, selectedRiderIdForRequest(request), request?.cancellationFeeRiderId ].filter(Boolean).map(String); return Boolean(request.cancelledBy && riderIds.includes(String(request.cancelledBy))); } function requestCancelledByPassenger(request) { return Boolean(request?.cancelledBy && request?.passengerId && request.cancelledBy === request.passengerId); } function rideCancelledNoticeBodyForRider(request) { if (requestCancelledByPassenger(request)) { return "Passenger has canceled this ride request. It has been removed from your active marketplace."; } if (requestCancelledByCurrentRider(request)) { return "You cancelled this ride. It has been removed from your active trip list."; } if (requestCancelledByMatchedRider(request)) { return "You cancelled this ride. It has been removed from your active trip list."; } return "This ride was cancelled. It has been removed from your active marketplace."; } function rideStatusLabel(request) { if (requestReopenedAfterRiderCancellation(request)) return "Reopened"; return { open: "Open", matched: "Matched", arrived: "Rider arrived", in_progress: "Ride in progress", completed: "Completed", cancelled: "Cancelled" }[request?.status] ?? request?.status ?? "Unknown"; } function reopenedRequestChip(request) { return requestReopenedAfterRiderCancellation(request) ? "Rider cancelled; reposted to nearby riders" : null; } function proximityChip(request, rider = currentRiderRecord()) { if (activeRole() !== "rider") return null; if (selectedRiderIdForRequest(request) === rider?.id) return "Matched to you"; const model = pickupProximityModel(request, rider); if (!model) return null; return `Pickup ETA: ${formatPickupEta(model.etaMinutes)}`; } function offerDistanceChip(offer, request) { if (activeRole() !== "passenger") return null; const rider = stateLookupIndexes().riderMap.get(offer.riderId); if (Number.isFinite(Number(offer?.pickupDistanceMeters))) { const distanceKm = Number(offer.pickupDistanceMeters) / 1000; return `Rider pickup ETA: ${formatPickupEta(pickupEtaMinutes(distanceKm, rider))}`; } const model = pickupProximityModel(request, rider); if (!model) return null; return `Rider pickup ETA: ${formatPickupEta(model.etaMinutes)}`; } function formatPickupDistanceKm(distanceKm, request) { if (distanceKm == null || !Number.isFinite(Number(distanceKm))) return "distance not estimated"; return request?.country === "United States" ? formatDistanceMiles(Number(distanceKm) * kmToMiles) : formatDistanceKm(Number(distanceKm)); } function formatRouteDistanceForRequest(distanceMiles, request) { if (distanceMiles == null || !Number.isFinite(Number(distanceMiles))) return "distance not estimated"; if (request?.country === "United States") return formatDistanceMiles(distanceMiles).replace(/ away$/, ""); return formatDistanceKm(Number(distanceMiles) / kmToMiles).replace(/ away$/, ""); } function destinationDriveChip(request) { const displayRequest = typeof riderVisibleRouteRequest === "function" ? riderVisibleRouteRequest(request) : request; return Number.isFinite(Number(displayRequest?.estimatedDistanceMiles)) ? `Destination drive: ${formatRouteDistanceForRequest(Number(displayRequest.estimatedDistanceMiles), displayRequest)}` : null; } function pickupDistanceSourceLabel(source) { return { postgis: "GPS/PostGIS", gps: "GPS", area_estimate: "area estimate" }[source] ?? source ?? "area estimate"; } function approachDistanceSourceIsLive(source) { return ["postgis", "gps", "gps/postgis"].includes(String(source ?? "").trim().toLowerCase()); } function riderApproachGps(request) { return normalizeGpsPoint(request?.riderApproachGps ?? { latitude: request?.riderApproachLatitude, longitude: request?.riderApproachLongitude, accuracyMeters: request?.riderApproachAccuracyMeters, capturedAt: request?.riderApproachCapturedAt }); } function selectedOfferForRequest(request) { if (!request) return null; const indexes = stateLookupIndexes(); return indexes.offerMap.get(request.selectedOfferId) ?? (indexes.offersByRequestId.get(request.id) ?? []).find((offer) => offer.riderId === selectedRiderIdForRequest(request)) ?? null; } function riderApproachModel(request) { const selectedRiderId = selectedRiderIdForRequest(request); if (!selectedRiderId) return null; const rider = stateLookupIndexes().riderMap.get(selectedRiderId); if (request.riderApproachIsLive && approachDistanceSourceIsLive(request.riderApproachSource) && Number.isFinite(Number(request.riderApproachDistanceMeters))) { const distanceKm = Number(request.riderApproachDistanceMeters) / 1000; return { source: pickupDistanceSourceLabel(request.riderApproachSource), distanceKm, etaMinutes: pickupEtaMinutes(distanceKm, rider), isLive: Boolean(request.riderApproachIsLive), riderGps: riderApproachGps(request), pickupGps: requestPickupGps(request), destinationGps: requestDestinationGps(request), capturedAt: request.riderApproachCapturedAt ?? null, accuracyMeters: request.riderApproachAccuracyMeters ?? null }; } const selectedOffer = selectedOfferForRequest(request); if (approachDistanceSourceIsLive(selectedOffer?.distanceSource) && Number.isFinite(Number(selectedOffer?.pickupDistanceMeters))) { const distanceKm = Number(selectedOffer.pickupDistanceMeters) / 1000; return { source: pickupDistanceSourceLabel(selectedOffer.distanceSource), distanceKm, etaMinutes: pickupEtaMinutes(distanceKm, rider), isLive: selectedOffer.distanceSource === "postgis" || selectedOffer.distanceSource === "gps", riderGps: riderApproachGps(request), pickupGps: requestPickupGps(request), destinationGps: requestDestinationGps(request), capturedAt: null, accuracyMeters: null }; } const model = pickupProximityModel(request, rider); if (!model) return null; if (!approachDistanceSourceIsLive(model.source)) return null; return { source: model.source, distanceKm: model.distanceKm, etaMinutes: model.etaMinutes, isLive: model.source === "GPS/PostGIS" || model.source === "GPS", riderGps: riderApproachGps(request), pickupGps: requestPickupGps(request), destinationGps: requestDestinationGps(request), capturedAt: null, accuracyMeters: null }; } function riderApproachChip(request) { if (activeRole() !== "passenger" || !selectedRiderIdForRequest(request)) return null; const model = riderApproachModel(request); if (!model) return "Rider approach: waiting for live update"; return `Rider approach: ${formatPickupEta(model.etaMinutes)}`; } function passengerHasTrackableRide() { return activeRole() === "passenger" && hasSignedIn("passenger") && state.requests.some((request) => requestBelongsToPassenger(request) && selectedRiderIdForRequest(request) && ["matched", "arrived", "in_progress"].includes(request.status)); } function passengerShouldAutoRefreshMarketplace() { return activeRole() === "passenger" && hasSignedIn("passenger") && Boolean(passengerPendingRide()); } function passengerMarketplaceRefreshDelayMs() { const request = passengerPendingRide(); if (!passengerShouldAutoRefreshMarketplace() || document.hidden || !request) return 0; if (request.status === "open") return marketplaceNegotiationRefreshIntervalMs; if (["matched", "arrived", "in_progress"].includes(request.status)) return passengerApproachRefreshIntervalMs; return riderMarketplaceRefreshIntervalMs; } function stopPassengerApproachAutoRefresh() { if (passengerApproachRefreshTimer == null) return; window.clearTimeout(passengerApproachRefreshTimer); passengerApproachRefreshTimer = null; passengerApproachRefreshTimerDelayMs = 0; } function ensurePassengerApproachAutoRefresh() { const delayMs = passengerMarketplaceRefreshDelayMs(); if (!delayMs) { stopPassengerApproachAutoRefresh(); return; } if (passengerApproachRefreshTimer != null && passengerApproachRefreshTimerDelayMs === delayMs) return; stopPassengerApproachAutoRefresh(); passengerApproachRefreshTimerDelayMs = delayMs; passengerApproachRefreshTimer = window.setTimeout(() => { passengerApproachRefreshTimer = null; passengerApproachRefreshTimerDelayMs = 0; if (!passengerShouldAutoRefreshMarketplace() || document.hidden) return; if (marketRefreshInFlight) { ensurePassengerApproachAutoRefresh(); return; } void refreshMarketplace({ silent: true, reason: "passenger_adaptive_poll" }) .finally(() => ensurePassengerApproachAutoRefresh()); }, delayMs); } function riderShouldAutoRefreshMarketplace() { return activeRole() === "rider" && hasSignedIn("rider"); } function riderHasOpenOfferNegotiation(rider = currentRiderRecord()) { if (!rider) return false; const request = selectedRequest(); if (request?.status === "open" && roleCanSeeRequest(request)) return true; const requestMap = stateLookupIndexes().requestMap; return state.offers.some((offer) => riderIdentityMatches(offer.riderId, rider) && requestMap.get(offer.requestId)?.status === "open"); } function riderHasMatchedOrActiveRide(rider = currentRiderRecord()) { return Boolean(rider && state.requests.some((request) => requestIsActiveForCurrentRider(request, rider))); } function riderAvailabilityIsActivated() { return state.riderAvailabilityActivated === true; } function riderAvailabilityRequiredText() { return "Activate rider availability before reviewing ride requests or sending offers."; } function riderMarketplaceRefreshDelayMs() { const rider = currentRiderRecord(); if (!riderShouldAutoRefreshMarketplace() || document.hidden) return 0; if (!rider) return 0; if (riderHasOpenOfferNegotiation(rider)) return marketplaceNegotiationRefreshIntervalMs; if (riderHasMatchedOrActiveRide(rider)) return passengerApproachRefreshIntervalMs; if (!riderAvailabilityIsActivated()) return 0; if (!riderCanSeeRequests(rider)) return riderMarketplaceRefreshIntervalMs; return riderMarketplaceRefreshIntervalMs; } function stopRiderMarketplaceAutoRefresh() { if (riderMarketplaceRefreshTimer == null) return; window.clearTimeout(riderMarketplaceRefreshTimer); riderMarketplaceRefreshTimer = null; riderMarketplaceRefreshTimerDelayMs = 0; } function ensureRiderMarketplaceAutoRefresh() { const delayMs = riderMarketplaceRefreshDelayMs(); if (!delayMs) { stopRiderMarketplaceAutoRefresh(); return; } if (riderMarketplaceRefreshTimer != null && riderMarketplaceRefreshTimerDelayMs === delayMs) return; stopRiderMarketplaceAutoRefresh(); riderMarketplaceRefreshTimerDelayMs = delayMs; riderMarketplaceRefreshTimer = window.setTimeout(() => { riderMarketplaceRefreshTimer = null; riderMarketplaceRefreshTimerDelayMs = 0; if (!riderShouldAutoRefreshMarketplace() || document.hidden) return; if (marketRefreshInFlight) { ensureRiderMarketplaceAutoRefresh(); return; } void refreshMarketplace({ silent: true, reason: "rider_adaptive_poll" }) .finally(() => ensureRiderMarketplaceAutoRefresh()); }, delayMs); } function accountNoticeRefreshDelayMs(type) { if (document.hidden || activeRole() !== type || !hasSignedIn(type)) return 0; if (type === "passenger") { return passengerPendingRide() ? accountNotificationActiveRefreshIntervalMs : accountNotificationIdleRefreshIntervalMs; } if (type === "rider") { const rider = currentRiderRecord(); if (!rider) return 0; return riderAvailabilityIsActivated() || riderHasOpenOfferNegotiation(rider) || riderHasMatchedOrActiveRide(rider) ? accountNotificationActiveRefreshIntervalMs : 0; } return 0; } function stopAccountNoticeAutoRefresh(type) { const timer = accountNotificationAutoRefreshTimers[type]; if (timer == null) return; window.clearTimeout(timer); accountNotificationAutoRefreshTimers[type] = null; accountNotificationAutoRefreshTimerDelayMs[type] = 0; } function ensureAccountNoticeAutoRefresh(type) { const delayMs = accountNoticeRefreshDelayMs(type); if (!delayMs) { stopAccountNoticeAutoRefresh(type); return; } if (accountNotificationAutoRefreshTimers[type] != null && accountNotificationAutoRefreshTimerDelayMs[type] === delayMs) { return; } stopAccountNoticeAutoRefresh(type); accountNotificationAutoRefreshTimerDelayMs[type] = delayMs; accountNotificationAutoRefreshTimers[type] = window.setTimeout(() => { accountNotificationAutoRefreshTimers[type] = null; accountNotificationAutoRefreshTimerDelayMs[type] = 0; if (!accountNoticeRefreshDelayMs(type)) return; void refreshAccountNotificationsFromSupabase(type, { deliverPhone: true, refreshRide: true }).finally(() => ensureAccountNoticeAutoRefresh(type)); }, delayMs); } function ensureAccountNoticeAutoRefreshes() { ensureAccountNoticeAutoRefresh("passenger"); ensureAccountNoticeAutoRefresh("rider"); } function stopAccountNoticeAutoRefreshes() { stopAccountNoticeAutoRefresh("passenger"); stopAccountNoticeAutoRefresh("rider"); } function marketplaceVisibleResumeIsStale() { if (!lastMarketRefreshAt) return true; return Date.now() - lastMarketRefreshAt.getTime() >= marketplaceVisibleResumeRefreshStaleMs; } function resumeMarketplaceAutoRefreshAfterVisibilityChange() { if (document.hidden) { stopPassengerApproachAutoRefresh(); stopRiderMarketplaceAutoRefresh(); stopAccountNoticeAutoRefreshes(); return; } ensurePassengerApproachAutoRefresh(); ensureRiderMarketplaceAutoRefresh(); ensureAccountNoticeAutoRefreshes(); ensureMarketplaceRealtimeSubscription(); if (marketplaceRealtimeRefreshPendingReason && canRefreshMarketplace() && !marketRefreshInFlight) { const pendingReason = marketplaceRealtimeRefreshPendingReason; marketplaceRealtimeRefreshPendingReason = ""; scheduleMarketplaceRealtimeRefresh(pendingReason); } if (!canRefreshMarketplace() || marketRefreshInFlight || !marketplaceVisibleResumeIsStale()) return; void refreshMarketplace({ silent: true, reason: "visible_resume" }); } function marketplaceRealtimeUserSignature() { if (!hasSupabaseRuntime() || (!hasSignedIn("passenger") && !hasSignedIn("rider"))) return ""; return [ activeRole(), state.passenger?.id ?? "", state.rider?.id ?? "" ].join(":"); } function scheduleMarketplaceRealtimeRefresh(reason = "realtime") { if (document.hidden || marketRefreshInFlight) { marketplaceRealtimeRefreshPendingReason = reason; return; } if (marketplaceRealtimeRefreshTimer != null) { marketplaceRealtimeRefreshPendingReason = reason; return; } marketplaceRealtimeRefreshTimer = window.setTimeout(() => { marketplaceRealtimeRefreshTimer = null; const pendingReason = marketplaceRealtimeRefreshPendingReason || reason; marketplaceRealtimeRefreshPendingReason = ""; if (document.hidden || marketRefreshInFlight) { marketplaceRealtimeRefreshPendingReason = pendingReason; return; } if (!canRefreshMarketplace()) { marketplaceRealtimeRefreshPendingReason = pendingReason; return; } void refreshMarketplace({ silent: true, reason: pendingReason }); }, marketplaceRealtimeRefreshDebounceMs); } function forceMarketplaceRefreshSoon(reason = "forced") { if (document.hidden) { marketplaceRealtimeRefreshPendingReason = reason; return; } if (marketRefreshInFlight) { marketplaceRealtimeRefreshPendingReason = reason; return; } if (!canRefreshMarketplace()) return; void refreshMarketplace({ silent: true, reason }); } function clearMarketplaceRealtimeReconnectTimer() { if (marketplaceRealtimeReconnectTimer == null) return; window.clearTimeout(marketplaceRealtimeReconnectTimer); marketplaceRealtimeReconnectTimer = null; } function removeMarketplaceRealtimeChannel(channel = marketplaceRealtimeChannel) { if (!channel || !supabaseClient?.removeChannel) return; try { supabaseClient.removeChannel(channel); } catch (error) { logClientWarning("Marketplace realtime channel could not be removed.", error); } } function scheduleMarketplaceRealtimeReconnect(status = "reconnect", expectedSignature = marketplaceRealtimeSignature) { if (marketplaceRealtimeReconnectTimer != null) return; const reconnectSignature = expectedSignature; marketplaceRealtimeReconnectTimer = window.setTimeout(() => { marketplaceRealtimeReconnectTimer = null; if (!reconnectSignature || reconnectSignature !== marketplaceRealtimeSignature || reconnectSignature !== marketplaceRealtimeUserSignature()) { stopMarketplaceRealtimeSubscription(); ensureMarketplaceRealtimeSubscription(); return; } const channel = marketplaceRealtimeChannel; marketplaceRealtimeChannel = null; marketplaceRealtimeSignature = ""; removeMarketplaceRealtimeChannel(channel); ensureMarketplaceRealtimeSubscription(); scheduleMarketplaceRealtimeRefresh(`realtime_${status.toLowerCase()}`); }, marketplaceRealtimeReconnectDelayMs); } function stopMarketplaceRealtimeSubscription() { if (marketplaceRealtimeRefreshTimer != null) { window.clearTimeout(marketplaceRealtimeRefreshTimer); marketplaceRealtimeRefreshTimer = null; } clearMarketplaceRealtimeReconnectTimer(); marketplaceRealtimeRefreshPendingReason = ""; removeMarketplaceRealtimeChannel(); marketplaceRealtimeChannel = null; marketplaceRealtimeSignature = ""; } function ensureMarketplaceRealtimeSubscription() { const signature = marketplaceRealtimeUserSignature(); if (!signature || !supabaseClient?.channel || !platformFeatureEnabled("marketplace_realtime_enabled")) { stopMarketplaceRealtimeSubscription(); return; } void loadPlatformFeatureFlagsFromSupabase().then(() => { if (!platformFeatureEnabled("marketplace_realtime_enabled")) stopMarketplaceRealtimeSubscription(); }); if (marketplaceRealtimeChannel && marketplaceRealtimeSignature === signature) return; stopMarketplaceRealtimeSubscription(); marketplaceRealtimeSignature = signature; const channelSignature = signature; marketplaceRealtimeChannel = supabaseClient .channel(`waka-marketplace-${signature}-${Date.now()}`) .on("postgres_changes", { event: "*", schema: "public", table: "marketplace_refresh_events" }, () => scheduleMarketplaceRealtimeRefresh("marketplace_refresh_events")) .on("postgres_changes", { event: "*", schema: "public", table: "ride_offers" }, () => scheduleMarketplaceRealtimeRefresh("ride_offers")) .on("postgres_changes", { event: "*", schema: "public", table: "ride_chats" }, () => scheduleMarketplaceRealtimeRefresh("ride_chats")) .on("postgres_changes", { event: "*", schema: "public", table: "ride_route_changes" }, () => scheduleMarketplaceRealtimeRefresh("ride_route_changes")) .on("postgres_changes", { event: "*", schema: "public", table: "admin_notifications" }, () => scheduleMarketplaceRealtimeRefresh("admin_notifications")) .subscribe((status) => { if (channelSignature !== marketplaceRealtimeSignature) return; if (status === "SUBSCRIBED") { clearMarketplaceRealtimeReconnectTimer(); scheduleMarketplaceRealtimeRefresh("realtime_subscribed"); return; } if (["CHANNEL_ERROR", "TIMED_OUT", "CLOSED"].includes(status)) { logClientWarning(`Marketplace realtime subscription status: ${status}`); scheduleMarketplaceRealtimeReconnect(status, channelSignature); } }); } function mapsCoordinate(point) { const gps = normalizeGpsPoint(point); return gps ? `${gps.latitude},${gps.longitude}` : null; } function compactLocationQuery(parts) { return parts .map((part) => String(part ?? "").trim()) .filter(Boolean) .join(", "); } function pickupMapsDestination(request) { return mapsCoordinate(requestPickupGps(request)) ?? compactLocationQuery([request?.pickupDescription, request?.pickupArea, request?.city, request?.country]); } function pickupAddressLooksPreciseForNavigation(value) { const text = String(value ?? "").trim(); if (!text || pickupUsesCurrentLocationText(text) || pickupUsesGpsFallbackText(text)) return false; if (/^-?\d{1,3}(?:\.\d+)?,\s*-?\d{1,3}(?:\.\d+)?$/.test(text)) return true; const streetToken = /\b(?:road|rd|street|st|avenue|ave|drive|dr|lane|ln|boulevard|blvd|court|ct|circle|cir|highway|hwy|way|place|pl|terrace|ter|pike|parkway|pkwy|route|rte)\b/i.test(text); return /\d/.test(text) && (streetToken || text.includes(",")); } function requestHasPrecisePickupNavigation(request) { if (!request) return false; if (requestPickupGps(request)) return true; return pickupAddressLooksPreciseForNavigation(request.pickupFormattedAddress) || pickupAddressLooksPreciseForNavigation(request.pickupDescription); } function destinationMapsQuery(request) { return mapsCoordinate({ latitude: request?.destinationLatitude, longitude: request?.destinationLongitude }) ?? compactLocationQuery([ request?.destinationFormattedAddress || request?.destination, request?.destinationArea, request?.city, request?.country ]); } function rideStopIndex(request) { const stops = normalizeRideStops(request?.rideStops); return Math.min(stops.length, Math.max(0, Number(request?.currentStopIndex ?? 0) || 0)); } function nextRideLeg(request) { const stops = normalizeRideStops(request?.rideStops); const index = rideStopIndex(request); if (!["arrived", "in_progress"].includes(request?.status)) { return { type: "pickup", label: "Pickup", destination: pickupMapsDestination(request), remainingStops: stops.length }; } if (index < stops.length) { return { type: "stop", index, label: `Stop ${index + 1}`, destination: stops[index], remainingStops: stops.length - index }; } return { type: "destination", label: "Destination", destination: destinationMapsQuery(request), remainingStops: 0 }; } function googleMapsSearchUrl(query) { if (!query) return ""; return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(query)}`; } const wakaAndroidRuntimeStorageKey = "waka-android-runtime-context-v1"; function wakaAndroidPackageFromUrl() { return new URLSearchParams(window.location.search).get("androidPackage") || ""; } function rememberWakaAndroidPackage(value = wakaAndroidPackageFromUrl()) { const normalized = String(value || "").trim(); if (!normalized) return ""; try { sessionStorage.setItem(wakaAndroidRuntimeStorageKey, normalized); localStorage.setItem(wakaAndroidRuntimeStorageKey, normalized); } catch { // Android WebView storage can be unavailable during early startup; URL detection still covers this page load. } return normalized; } function rememberedWakaAndroidPackage() { try { return sessionStorage.getItem(wakaAndroidRuntimeStorageKey) || localStorage.getItem(wakaAndroidRuntimeStorageKey) || ""; } catch { return ""; } } function currentWakaAndroidPackage() { return rememberWakaAndroidPackage(wakaAndroidPackageFromUrl()) || rememberedWakaAndroidPackage(); } function isAndroidRuntime() { const params = new URLSearchParams(window.location.search); return params.has("androidAppVersion") || Boolean(currentWakaAndroidPackage()) || /Android/i.test(navigator.userAgent || ""); } function isWakaAndroidAppRuntime() { const params = new URLSearchParams(window.location.search); return Boolean(currentWakaAndroidPackage() || params.has("androidAppVersion") || window.WakaAndroid || wakaAndroidBridge()); } function wakaAndroidNavigationBridgeAvailable() { const bridge = wakaAndroidBridge(); return Boolean(bridge && typeof bridge.openNavigation === "function"); } function wakaAndroidNavigationUrl(provider, destination, origin = null, waypoints = []) { if (!destination || !wakaAndroidNavigationBridgeAvailable()) return ""; const params = new URLSearchParams({ provider: normalizeRiderNavigationPreference(provider), destination }); if (origin) params.set("origin", origin); const cleanWaypoints = normalizeRideStops(waypoints); if (cleanWaypoints.length) params.set("waypoints", cleanWaypoints.join("|")); return `waka-nav://navigate?${params.toString()}`; } function androidPackageIntentUrl(fallbackUrl, packageName) { if (!fallbackUrl || !packageName) return fallbackUrl || ""; try { const parsed = new URL(fallbackUrl); const target = `${parsed.host}${parsed.pathname}${parsed.search}${parsed.hash}`; const scheme = parsed.protocol.replace(":", ""); return `intent://${target}#Intent;scheme=${scheme};package=${packageName};S.browser_fallback_url=${encodeURIComponent(fallbackUrl)};end`; } catch { return fallbackUrl; } } function googleMapsWebDirectionsUrl(destination, origin = null, waypoints = []) { if (!destination) return ""; const params = new URLSearchParams({ api: "1", destination, travelmode: "driving", dir_action: "navigate" }); if (origin) params.set("origin", origin); const cleanWaypoints = normalizeRideStops(waypoints); if (cleanWaypoints.length) params.set("waypoints", cleanWaypoints.join("|")); return `https://www.google.com/maps/dir/?${params.toString()}`; } function googleMapsAndroidIntentUrl(destination, origin = null, waypoints = []) { const fallbackUrl = googleMapsWebDirectionsUrl(destination, origin, waypoints); return androidPackageIntentUrl(fallbackUrl, "com.google.android.apps.maps"); } function googleMapsTurnByTurnUrl(destination) { if (!destination) return ""; if (wakaAndroidNavigationBridgeAvailable()) return wakaAndroidNavigationUrl("google_maps", destination); if (isAndroidRuntime()) return googleMapsAndroidIntentUrl(destination); return googleMapsWebDirectionsUrl(destination); } function googleMapsDirectionsUrl(destination, origin = null, waypoints = []) { if (!destination) return ""; if (wakaAndroidNavigationBridgeAvailable()) return wakaAndroidNavigationUrl("google_maps", destination, origin, waypoints); if (isAndroidRuntime()) return googleMapsAndroidIntentUrl(destination, origin, waypoints); return googleMapsWebDirectionsUrl(destination, origin, waypoints); } function riderPickupNavigationUrl(request, rider = currentRiderRecord()) { const destination = pickupMapsDestination(request); if (riderNavigationPreference(rider) === "waze") return wazeNavigationUrl(destination); return googleMapsDirectionsUrl(destination); } function wazeNavigationUrl(destination) { if (wakaAndroidNavigationBridgeAvailable()) return wakaAndroidNavigationUrl("waze", destination); return wazeExternalNavigationUrl(destination); } function wazeExternalNavigationUrl(destination) { if (!destination) return ""; const normalized = String(destination).replace(/\s+/g, ""); const coordinatePattern = /^-?\d+(?:\.\d+)?,-?\d+(?:\.\d+)?$/; const params = new URLSearchParams({ navigate: "yes" }); if (coordinatePattern.test(normalized)) { params.set("ll", normalized); } else { params.set("q", destination); } const query = params.toString(); const fallbackUrl = `https://waze.com/ul?${query}`; if (isAndroidRuntime()) return androidPackageIntentUrl(fallbackUrl, "com.waze"); return fallbackUrl; } function wakaAndroidBridge() { return window.WakaAndroid || null; } function wakaAndroidNavigationUrlFromExternalUrl(url) { if (!wakaAndroidNavigationBridgeAvailable()) return ""; if (!url || /^waka-nav:/i.test(url)) return url || ""; try { if (/^google\.navigation:/i.test(url)) { const rawQuery = String(url).replace(/^google\.navigation:/i, ""); const params = new URLSearchParams(rawQuery); return wakaAndroidNavigationUrl("google_maps", params.get("q") || ""); } const parsed = new URL(url); if (/waze\.com$/i.test(parsed.hostname) && parsed.pathname.startsWith("/ul")) { return wakaAndroidNavigationUrl("waze", parsed.searchParams.get("q") || parsed.searchParams.get("ll") || ""); } if (/google\.com$/i.test(parsed.hostname) && parsed.pathname.startsWith("/maps/")) { return wakaAndroidNavigationUrl( "google_maps", parsed.searchParams.get("destination") || parsed.searchParams.get("query") || "" ); } } catch { return ""; } return ""; } function externalNavigationUrlFromWakaAndroidUrl(url) { if (!url || !/^waka-nav:/i.test(url)) return ""; try { const parsed = new URL(url); const provider = normalizeRiderNavigationPreference(parsed.searchParams.get("provider")); const destination = parsed.searchParams.get("destination") || ""; const origin = parsed.searchParams.get("origin") || null; const waypoints = normalizeRideStops((parsed.searchParams.get("waypoints") || "").split("|")); if (provider === "waze") return wazeExternalNavigationUrl(destination); return isAndroidRuntime() ? googleMapsAndroidIntentUrl(destination, origin, waypoints) : googleMapsWebDirectionsUrl(destination, origin, waypoints); } catch { return ""; } } function openWakaAndroidNavigationUrl(url) { if (!url) return false; try { const parsed = new URL(url); if (parsed.protocol !== "waka-nav:") return false; const bridge = wakaAndroidBridge(); if (!bridge || typeof bridge.openNavigation !== "function") return false; bridge.openNavigation( parsed.searchParams.get("provider") || "google_maps", parsed.searchParams.get("destination") || "", parsed.searchParams.get("origin") || "", parsed.searchParams.get("waypoints") || "" ); return true; } catch (error) { logClientWarning("Android navigation bridge failed.", error); return false; } } function riderPickupWazeUrl(request) { return wazeNavigationUrl(pickupMapsDestination(request)); } function pickupMapUrl(request) { return googleMapsSearchUrl(pickupMapsDestination(request)); } function destinationMapUrl(request, origin = null) { return googleMapsDirectionsUrl(destinationMapsQuery(request), origin, request?.rideStops); } function nextRideLegNavigationUrl(request, rider = currentRiderRecord()) { const leg = nextRideLeg(request); if (!leg.destination) return ""; if (riderNavigationPreference(rider) === "waze") return wazeNavigationUrl(leg.destination); return googleMapsDirectionsUrl(leg.destination); } function riderDestinationNavigationUrl(request, rider = currentRiderRecord()) { const destination = destinationMapsQuery(request); if (riderNavigationPreference(rider) === "waze") return wazeNavigationUrl(destination); return destinationMapUrl(request, pickupMapsDestination(request)); } function navigationUrlCanOpenSameWindow(url) { return /^waka-nav:/i.test(url) || /^intent:/i.test(url) || /^google\.navigation:/i.test(url) || /^https:\/\/(?:www\.)?google\.com\/maps\/dir\//i.test(url) || /^waze:/i.test(url) || /^https:\/\/waze\.com\/ul/i.test(url); } function openNavigationUrl(url, { auto = false } = {}) { if (!url) return false; try { const wakaUrl = wakaAndroidNavigationUrlFromExternalUrl(url); if (wakaUrl && openWakaAndroidNavigationUrl(wakaUrl)) return true; const externalWakaUrl = externalNavigationUrlFromWakaAndroidUrl(url); if (externalWakaUrl) { if (auto && navigationUrlCanOpenSameWindow(externalWakaUrl)) { window.location.assign(externalWakaUrl); return true; } if (!auto) { const opened = window.open(externalWakaUrl, "_blank", "noopener,noreferrer"); return Boolean(opened); } return false; } if (/^waka-nav:/i.test(url)) { if (isWakaAndroidAppRuntime()) { window.location.assign(url); return true; } return false; } if (auto && navigationUrlCanOpenSameWindow(url)) { window.location.assign(url); return true; } if (auto) return false; const opened = window.open(url, "_blank", "noopener,noreferrer"); if (opened) return true; if (!auto || navigationUrlCanOpenSameWindow(url)) { window.location.assign(url); return true; } } catch (error) { logClientWarning("Navigation app could not open.", error); } return false; } function offerPickupDistanceSnapshot(request, rider) { const model = pickupProximityModel(request, rider); if (!model) return {}; const source = model.source === "GPS/PostGIS" ? "postgis" : model.source === "GPS" ? "gps" : "area_estimate"; return { pickupDistanceMeters: Math.round(model.distanceKm * 1000), distanceSource: source }; } function confirmationChip(request) { if (!isScheduledRequest(request)) return null; const status = request.riderConfirmationStatus; if (status === "requested") return "Rider confirmation requested"; if (status === "confirmed") return `Rider confirmed ${formatDateTime(request.riderConfirmedAt)}`; if (status === "declined") return "Rider cannot keep plan"; if (status === "released") return "Rider released"; return request.status === "matched" ? "Confirmation not requested" : "Awaiting rider selection"; } function riderBaseReadyForRequests(rider = currentRiderRecord()) { return Boolean(rider && hasSignedIn("rider") && rider.status === "approved" && isSubscriptionActive(rider) && riderComplianceReady(rider) && paymentAccountReady("rider", rider)); } function riderCanSeeRequests(rider = currentRiderRecord()) { return Boolean(riderAvailabilityIsActivated() && riderBaseReadyForRequests(rider) && riderCurrentFreshGps(rider)); } function roleCanSeeRequest(request) { if (!request) return false; if (activeRole() === "passenger") { return requestBelongsToPassenger(request); } if (activeRole() === "rider") { const rider = currentRiderRecord(); const ownMatchedRequest = requestHasRiderMatch(request); const ownActiveRequest = ownMatchedRequest && ["matched", "arrived", "in_progress"].includes(request.status); if (ownActiveRequest && riderPrePickupCancellationClearedForCurrentRider(request, rider)) return false; if (ownActiveRequest) return true; if (!riderCanSeeRequests(rider) || request.status !== "open") return false; if (riderRequestDismissedByCurrentRider(request, rider)) return false; const vehicleMatches = requestMatchesRiderVehicle(request, rider); const gpsDistanceKm = gpsDistanceKmForRequest(request, rider); const isNearEnough = gpsDistanceKm != null ? riderWithinGpsProximity(request, rider) : riderWithinRequestProximity(request, rider); const destinationAllowed = requestDestinationMatchesDailyRegions(request, rider); const notBusyElsewhere = riderCanReviewAnotherImmediateRequest(request, rider); return vehicleMatches && isNearEnough && destinationAllowed && notBusyElsewhere; } return false; } function visibleRequestsForRole() { const { country, city } = activeMarketLocation(); return state.requests .filter((item) => ( activeRole() === "rider" && requestIsActiveForCurrentRider(item) ) || (item.country === country && item.city === city)) .filter((item) => activeRole() !== "passenger" || passengerRideRequestVisibleInActiveBoard(item)) .filter(requestMatchesVehicleFilter) .filter(roleCanSeeRequest) .filter(requestMatchesRiderMarketplaceDestinationFilter) .sort((a, b) => { if (activeRole() === "rider") { const rider = currentRiderRecord(); const aIsOwnActive = requestHasRiderMatch(a) && ["matched", "arrived", "in_progress"].includes(a.status) ? 0 : 1; const bIsOwnActive = requestHasRiderMatch(b) && ["matched", "arrived", "in_progress"].includes(b.status) ? 0 : 1; if (aIsOwnActive !== bIsOwnActive) return aIsOwnActive - bIsOwnActive; const aDestinationPreference = riderDestinationFilterSortValue(a); const bDestinationPreference = riderDestinationFilterSortValue(b); if (aDestinationPreference !== bDestinationPreference) return aDestinationPreference - bDestinationPreference; const aPickupEta = pickupProximitySortValue(a, rider); const bPickupEta = pickupProximitySortValue(b, rider); if (aPickupEta !== bPickupEta) return aPickupEta - bPickupEta; const fareDifference = Number(b.fareOffer ?? 0) - Number(a.fareOffer ?? 0); if (fareDifference !== 0) return fareDifference; } return new Date(b.createdAt) - new Date(a.createdAt); }); } function riderAlertKey(request, rider = currentRiderRecord()) { return [rider?.id, request?.id, request?.status, Number(request?.fareOffer ?? 0)].filter(Boolean).join(":"); } function riderVisibleAlertedKeys() { return new Set(Array.isArray(state.riderNearbyRequestAlertedKeys) ? state.riderNearbyRequestAlertedKeys : []); } function riderDismissedRequestKey(request, rider = currentRiderRecord()) { if (!request?.id || !rider?.id) return ""; return [rider.id, request.id, String(Number(request.fareOffer ?? 0))].join(":"); } function riderDismissedRequestKeys() { return new Set(Array.isArray(state.riderDismissedRequestKeys) ? state.riderDismissedRequestKeys : []); } function riderLocalStateIdentity(rider = currentRiderRecord()) { return rider?.id ?? rider?.supabaseUserId ?? state.sessions?.rider?.userId ?? ""; } function riderPrePickupCancellationClearKey(request, rider = currentRiderRecord()) { const riderId = riderLocalStateIdentity(rider); if (!request?.id || !riderId) return ""; return `${riderId}:${request.id}`; } function riderClearedPrePickupCancellationKeys() { return new Set(Array.isArray(state.riderClearedPrePickupCancellationKeys) ? state.riderClearedPrePickupCancellationKeys : []); } function riderPrePickupCancellationClearedForCurrentRider(request, rider = currentRiderRecord()) { const key = riderPrePickupCancellationClearKey(request, rider); return Boolean(key && riderClearedPrePickupCancellationKeys().has(key)); } function rememberRiderPrePickupCancellationClear(request, rider = currentRiderRecord()) { const key = riderPrePickupCancellationClearKey(request, rider); if (!key) return; state.riderClearedPrePickupCancellationKeys = [...riderClearedPrePickupCancellationKeys(), key].slice(-100); } function riderRequestDismissedByCurrentRider(request, rider = currentRiderRecord()) { const key = riderDismissedRequestKey(request, rider); return Boolean(key && riderDismissedRequestKeys().has(key)); } function riderRequestReopenedForCurrentRider(request, rider = currentRiderRecord()) { if (!request?.id || !rider?.id || request.status !== "open") return false; const currentFare = Number(request.fareOffer ?? 0); const prefix = `${rider.id}:${request.id}:`; return [...riderDismissedRequestKeys()].some((key) => { if (!key.startsWith(prefix)) return false; const dismissedFare = Number(key.slice(prefix.length)); return Number.isFinite(dismissedFare) && dismissedFare < currentFare; }); } function riderReopenedFareChip(request, rider = currentRiderRecord()) { return riderRequestReopenedForCurrentRider(request, rider) ? `Reopened at ${formatMoney(request.fareOffer, request.country)}` : null; } function rememberRiderDismissedRequest(request, rider = currentRiderRecord()) { const key = riderDismissedRequestKey(request, rider); if (!key) return; state.riderDismissedRequestKeys = [...riderDismissedRequestKeys(), key].slice(-300); } function riderHasAlreadyHandledMarketplaceRequest(request, rider = currentRiderRecord()) { if (!request?.id || !rider?.id) return false; if (riderRequestDismissedByCurrentRider(request, rider)) return true; if (riderRequestReopenedForCurrentRider(request, rider)) return true; return state.offers.some((offer) => offer.requestId === request.id && offer.riderId === rider.id); } function rememberRiderNearbyAlert(request, rider = currentRiderRecord()) { const key = riderAlertKey(request, rider); if (!key) return; state.riderNearbyRequestAlertedKeys = [...riderVisibleAlertedKeys(), key].slice(-200); saveState(); } function unreadRiderNearbyRequests(requests = visibleRequestsForRole()) { if (activeRole() !== "rider") return []; const rider = currentRiderRecord(); if (!riderCanSeeRequests(rider)) return []; const alerted = riderVisibleAlertedKeys(); return requests .filter((request) => request?.status === "open") .filter((request) => !riderHasAlreadyHandledMarketplaceRequest(request, rider)) .filter((request) => !alerted.has(riderAlertKey(request, rider))); } function nearbyRideAlertSummary(request) { const fare = formatMoney(request?.fareOffer, request?.country); const distance = proximityChip(request) ?? "pickup distance pending"; const stops = normalizeRideStops(request?.rideStops); const stopText = stops.length ? `, ${stops.length} stop${stops.length === 1 ? "" : "s"}` : ""; return `New ride nearby: ${fare}, ${distance}${stopText}.`; } const wakaNotificationVibrationPattern = [120, 60, 120, 60, 180]; function playNearbyRideCue() { try { if (navigator.vibrate) navigator.vibrate(wakaNotificationVibrationPattern); } catch (error) { logClientWarning("Nearby ride vibration cue was not available.", error); } } function workspaceNotificationUrl(role, page, requestId = "") { const workspaceRole = role === "rider" ? "rider" : "passenger"; const workspacePage = page || (workspaceRole === "rider" ? "requests" : "trips"); const url = new URL(`/${workspaceRole}`, window.location.origin); url.searchParams.set(workspaceRole === "rider" ? "riderPage" : "passengerPage", workspacePage); if (requestId) url.searchParams.set("requestId", requestId); return url.toString(); } const notificationPreferenceOptions = [ { key: "all", label: "Allow notifications" }, { key: "ride", label: "Ride updates" }, { key: "chat", label: "Messages" }, { key: "fare", label: "Fare and offers" }, { key: "admin", label: "Waka notices" } ]; function notificationPreferenceType(notice = {}) { const text = `${notice.eventType || ""} ${notice.id || ""} ${notice.title || ""} ${notice.body || ""}`.toLowerCase(); if (/chat|message|ride_chat_message/.test(text)) return "chat"; if (/fare|offer|counter|boost|increased|rider_counter_offer|passenger_fare_increased/.test(text)) return "fare"; if (!notice.requestId || /admin|broadcast|notice|support|approval|correction|checkr|eligibility/.test(text)) return "admin"; return "ride"; } function notificationPreferenceEnabled(role, key) { const type = role === "rider" ? "rider" : "passenger"; const normalizedKey = notificationPreferenceOptions.some((option) => option.key === key) ? key : "ride"; const preferences = state.notificationPreferences?.[type] ?? normalizeNotificationPreferenceSet(); if (normalizedKey === "all") return preferences.all !== false; if (preferences.all === false) return false; return preferences[normalizedKey] !== false; } function setNotificationPreference(role, key, enabled) { const type = role === "rider" ? "rider" : "passenger"; if (!notificationPreferenceOptions.some((option) => option.key === key)) return; state.notificationPreferences ||= normalizeNotificationPreferences(); state.notificationPreferences[type] ||= normalizeNotificationPreferenceSet(); state.notificationPreferences[type][key] = enabled !== false; saveState(); renderAll(); } function noticeDeliveryAllowedByPreference(role, notice) { return notificationPreferenceEnabled(role, notificationPreferenceType(notice)); } const queuedPhoneDeliveryProcessKeys = new Set(); const phoneDeliveryProcessableRideEvents = new Set([ "passenger_fare_increased", "rider_counter_offer", "passenger_rejected_offer", "rider_offer_withdrawn", "ride_matched", "ride_reopened", "ride_cancelled", "ride_chat_message", "ride_arrived", "ride_started", "ride_stop_arrived", "ride_completed", "route_change_requested", "route_change_accepted", "route_change_declined", "scheduled_ride_reminder", "scheduled_ride_confirmation_needed" ]); function processQueuedPhoneDeliveryForNotice(role, notice) { if (!notice?.requestId || !hasSupabaseRuntime() || typeof processRideRequestPushDelivery !== "function") return false; if (!noticeDeliveryAllowedByPreference(role, notice)) return false; const eventType = noticePopupSemanticEvent(notice); if (!phoneDeliveryProcessableRideEvents.has(eventType)) return false; const key = [role, notice.requestId, eventType, notice.id || notice.createdAt || ""].join(":"); if (queuedPhoneDeliveryProcessKeys.has(key)) return false; queuedPhoneDeliveryProcessKeys.add(key); Promise.resolve(processRideRequestPushDelivery(notice.requestId, { eventTypes: [eventType] })) .catch((error) => logClientWarning("Queued phone notification delivery could not be processed.", error)) .finally(() => { window.setTimeout(() => queuedPhoneDeliveryProcessKeys.delete(key), 15000); }); return true; } async function showRideTransactionPhoneNotification({ role = "rider", page = "", request = null, requestId = "", title = "Waka update", body = "", tag = "", cue = true } = {}) { const resolvedRequestId = requestId || request?.id || ""; const workspaceRole = role === "passenger" ? "passenger" : "rider"; if (!notificationPreferenceEnabled(workspaceRole, notificationPreferenceType({ id: tag, title, body, requestId: resolvedRequestId, eventType: tag }))) return; if (cue) playNearbyRideCue(); if (typeof Notification === "undefined" || Notification.permission !== "granted") return; const notificationUrl = workspaceNotificationUrl(workspaceRole, page, resolvedRequestId); const displayTitle = wakaGoodDialogBrand; const displayBody = title && title !== displayTitle ? `${title}${body ? `\n${body}` : ""}` : body; const options = { body: displayBody, icon: "./icons/icon-192.png", badge: "./icons/icon-192.png", tag: tag || `waka-${workspaceRole}-ride-${resolvedRequestId || Date.now()}`, renotify: true, silent: false, vibrate: wakaNotificationVibrationPattern, data: { url: notificationUrl, requestId: resolvedRequestId, role: workspaceRole } }; try { const registration = "serviceWorker" in navigator ? await navigator.serviceWorker.ready : null; if (registration?.showNotification) { await registration.showNotification(displayTitle, options); return; } const notice = new Notification(displayTitle, options); notice.onclick = () => { window.focus(); window.location.assign(notificationUrl); }; } catch (error) { logClientWarning("Ride transaction phone notification was not available.", error); } } async function showRiderNearbyPhoneNotification(request) { if (!request) return; const title = "New nearby ride request"; const body = nearbyRideAlertSummary(request); await showRideTransactionPhoneNotification({ role: "rider", page: "requests", request, title, body, tag: `waka-ride-request-${request.id}` }); } function dismissRiderNearbyAlert(request) { rememberRiderNearbyAlert(request); const panel = document.querySelector(".nearby-ride-alert"); if (panel) panel.remove(); riderNearbyAlertActiveId = null; } function focusRiderRequestView(request, { refresh = true, replace = true } = {}) { if (!request) return; document.querySelector(".nearby-ride-alert")?.remove(); clearRiderDecisionQueueForRequest(request.id); clearRequestMarketplaceFareChange(request.id); state.activeTab = "rider"; state.showRoleEntry = false; state.riderPage = "requests"; state.selectedRequestId = request.id; riderNearbyAlertActiveId = null; if (typeof updateRiderWorkspaceRoute === "function") updateRiderWorkspaceRoute("requests", { replace, requestId: request.id }); saveState(); renderAll(); window.setTimeout(() => { document.querySelector("#riderRequestDetailPanel, #marketPanel") ?.scrollIntoView({ behavior: "smooth", block: "start" }); }, 0); if (refresh) void refreshMarketplace({ silent: true }); } function openRiderMarketplaceView({ refresh = true, replace = true } = {}) { state.activeTab = "rider"; state.showRoleEntry = false; state.riderPage = "requests"; state.selectedRequestId = null; riderNearbyAlertActiveId = null; if (typeof updateRiderWorkspaceRoute === "function") updateRiderWorkspaceRoute("requests", { replace, requestId: "" }); saveState(); renderAll(); window.setTimeout(() => { document.querySelector("#requestsBoard, #marketPanel") ?.scrollIntoView({ behavior: "smooth", block: "start" }); }, 0); if (refresh) void refreshMarketplace({ silent: true }); } function focusPassengerRequestView(request, { refresh = true, replace = true } = {}) { if (!request) return; state.activeTab = "passenger"; state.showRoleEntry = false; state.passengerPage = "trips"; state.selectedRequestId = request.id; if (typeof updatePassengerWorkspaceRoute === "function") updatePassengerWorkspaceRoute("trips", { replace, requestId: request.id }); saveState(); renderAll(); window.setTimeout(() => { document.querySelector("#requestList .market-card.selected, #marketPanel") ?.scrollIntoView({ behavior: "smooth", block: "start" }); }, 0); if (refresh) void refreshMarketplace({ silent: true }); } function focusRideNoticeView(type, request, { refresh = false, replace = true } = {}) { if (!request) return false; if (type === "passenger") { if (!requestBelongsToPassenger(request)) return false; focusPassengerRequestView(request, { refresh, replace }); return true; } if (type === "rider") { if (!state.rider?.id) return false; focusRiderRequestView(request, { refresh, replace }); return true; } return false; } function riderWorkloadMode() { return ["normal", "focus"].includes(state.riderWorkloadMode) ? state.riderWorkloadMode : "normal"; } function setRiderWorkloadMode(mode) { state.riderWorkloadMode = mode === "focus" ? "focus" : "normal"; saveState(); renderAll(); } function riderFocusModeActive() { return riderWorkloadMode() === "focus"; } function riderDecisionQueueItems() { const riderId = state.rider?.id; const requestMap = stateLookupIndexes().requestMap; return (state.riderDecisionQueue ?? []) .filter((item) => item?.requestId && (!item.riderId || item.riderId === riderId)) .filter((item) => requestMap.has(item.requestId)) .sort((a, b) => new Date(b.createdAt ?? 0) - new Date(a.createdAt ?? 0)) .slice(0, 8); } function riderDecisionQueueKey(request, eventType = "ride_update") { return [ "rider-decision", state.rider?.id, request?.id, eventType, Number(request?.fareOffer ?? 0) ].filter(Boolean).join(":"); } function queueRiderDecisionUpdate(title, body, requestId, eventType = "ride_update") { if (!state.rider?.id || !requestId) return null; const request = stateLookupIndexes().requestMap.get(requestId) ?? null; const item = { id: riderDecisionQueueKey(request ?? { id: requestId }, eventType), riderId: state.rider.id, requestId, title, body, eventType, fareOffer: request ? Number(request.fareOffer ?? 0) : null, createdAt: new Date().toISOString() }; state.riderDecisionQueue = upsertById(state.riderDecisionQueue ?? [], item).slice(0, 25); saveState(); return item; } function clearRiderDecisionQueueForRequest(requestId) { if (!requestId || !Array.isArray(state.riderDecisionQueue)) return; state.riderDecisionQueue = state.riderDecisionQueue.filter((item) => item.requestId !== requestId); } function dismissRiderDecisionQueueItem(itemId) { if (!itemId) return; state.riderDecisionQueue = (state.riderDecisionQueue ?? []).filter((item) => item.id !== itemId); saveState(); renderAll(); } function viewRiderDecisionQueueItem(itemId) { const item = (state.riderDecisionQueue ?? []).find((entry) => entry.id === itemId); const request = item?.requestId ? stateLookupIndexes().requestMap.get(item.requestId) : null; if (!item || !request) { dismissRiderDecisionQueueItem(itemId); return; } clearRiderDecisionQueueForRequest(item.requestId); focusRiderRequestView(request, { refresh: true, replace: true }); } function riderHasActiveRequestDecisionInProgress(targetRequestId = "") { if (activeRole() !== "rider" || typeof riderWorkspacePage !== "function") return false; if (riderWorkspacePage() !== "requests") return false; const current = selectedRequest(); if (!current?.id || current.id === targetRequestId) return false; return Boolean(current.status === "open" && riderCanShowOfferControls(currentRiderRecord(), current)); } function riderShouldQueueRideNotice(targetRequestId = "") { if (activeRole() !== "rider" || typeof riderWorkspacePage !== "function") return false; if (riderWorkspacePage() !== "requests") return false; if (riderHasActiveRequestDecisionInProgress(targetRequestId)) return true; return riderFocusModeActive(); } function shouldAutoFocusRideNotice(type, request) { if (!request) return false; if (type !== "rider") return true; return !riderShouldQueueRideNotice(request.id); } const riderRouteChangeDecisionRetryKeys = new Set(); function queueRiderRouteChangeDecisionRetry(requestOrId, notice) { const requestId = typeof requestOrId === "string" ? requestOrId : requestOrId?.id ?? notice?.requestId ?? ""; if (!requestId) return false; const key = [requestId, notice?.id || notice?.createdAt || notice?.actionUrl || "route-change"].filter(Boolean).join(":"); if (riderRouteChangeDecisionRetryKeys.has(key)) return true; riderRouteChangeDecisionRetryKeys.add(key); window.setTimeout(async () => { try { if (canRefreshMarketplace() && !marketRefreshInFlight) { await refreshMarketplace({ silent: true, reason: "route_change_notice_retry" }); } else { scheduleMarketplaceRealtimeRefresh("route_change_notice_retry"); } const refreshedRequest = stateLookupIndexes().requestMap.get(requestId) ?? (typeof requestOrId === "string" ? null : requestOrId); if (showRiderRouteChangeDecisionForRequest(refreshedRequest)) { rememberNoticePopup(notice); } } finally { window.setTimeout(() => riderRouteChangeDecisionRetryKeys.delete(key), 5000); } }, 250); return true; } function showRiderRouteChangeDecisionForRequest(request, change = pendingRouteChangeForRequest(request), { render = true } = {}) { if (!request?.id || !change?.id || !state.rider?.id) return false; if (!riderIdentityMatches(selectedRiderIdForRequest(request)) || change.status !== "pending") return false; state.activeTab = "rider"; state.showRoleEntry = false; state.riderPage = "requests"; state.selectedRequestId = request.id; document.querySelector(".nearby-ride-alert")?.remove(); clearRiderDecisionQueueForRequest(request.id); clearRequestMarketplaceFareChange(request.id); if (typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute("requests", { replace: true, requestId: request.id }); } saveState(); if (render && typeof renderAll === "function") renderAll(); window.setTimeout(() => { if (pendingRouteChangeForRequest(stateLookupIndexes().requestMap.get(request.id) ?? request)?.id === change.id) { renderRiderRouteChangeDecisionModal(); } }, 0); return true; } function reconcileRiderPendingRouteChangeDecision({ render = false } = {}) { if (activeRole() !== "rider" || !state.rider?.id) return false; const activeRequests = state.requests .filter((request) => requestIsActiveForCurrentRider(request) && pendingRouteChangeForRequest(request)) .sort((left, right) => (left.id === state.selectedRequestId ? -1 : 0) - (right.id === state.selectedRequestId ? -1 : 0)); const request = activeRequests[0] ?? null; if (!request) return false; return showRiderRouteChangeDecisionForRequest(request, pendingRouteChangeForRequest(request), { render }); } function viewRiderNearbyAlert(request) { if (!request) return; rememberRiderNearbyAlert(request); focusRiderRequestView(request, { refresh: true, replace: true }); } function showRiderNearbyRequestAlert(request) { if (!request || riderNearbyAlertActiveId === request.id) return; document.querySelector(".nearby-ride-alert")?.remove(); riderNearbyAlertActiveId = request.id; rememberRiderNearbyAlert(request); if (!notificationPreferenceEnabled("rider", "ride")) { riderNearbyAlertActiveId = null; return; } const panel = document.createElement("section"); panel.className = "nearby-ride-alert"; panel.setAttribute("role", "dialog"); panel.setAttribute("aria-live", "assertive"); const title = document.createElement("strong"); title.textContent = "New nearby ride"; const detail = document.createElement("p"); detail.textContent = nearbyRideAlertSummary(request); const meta = document.createElement("small"); meta.textContent = `${requestPickupTownText(request)} to ${requestDestinationDisplayText(request)}. ${riderDestinationScopeLabel()}.`; const actions = document.createElement("div"); actions.className = "review-actions"; const view = document.createElement("button"); view.type = "button"; view.className = "secondary-action"; view.textContent = "View request"; view.addEventListener("click", () => viewRiderNearbyAlert(request)); const dismiss = document.createElement("button"); dismiss.type = "button"; dismiss.className = "ghost-action"; dismiss.textContent = "Dismiss"; dismiss.addEventListener("click", () => dismissRiderNearbyAlert(request)); actions.append(view, dismiss); panel.append(title, detail, meta, actions); document.body.append(panel); playNearbyRideCue(); if (!hasSupabaseRuntime()) void showRiderNearbyPhoneNotification(request); } function notifyRiderAboutNearbyRequests(requests = visibleRequestsForRole()) { const [request] = unreadRiderNearbyRequests(requests); if (!request) return; if (riderShouldQueueRideNotice(request.id)) { queueRiderDecisionUpdate("New nearby ride", nearbyRideAlertSummary(request), request.id, "nearby_request"); rememberRiderNearbyAlert(request); return; } if (!riderHasActiveRequestDecisionInProgress(request.id)) { openRiderMarketplaceView({ refresh: false, replace: true }); } showRiderNearbyRequestAlert(request); } function visibleOffersForRole(request) { if (!request || !roleCanSeeRequest(request)) return []; const offers = offersForRequest(request.id); if (activeRole() === "rider") { return offers.filter(offerBelongsToRider); } if (activeRole() === "passenger" && request.status !== "open") return []; const rejected = new Set(state.rejectedOfferIds ?? []); return sortOffersForPassenger(offers.filter((offer) => !rejected.has(offer.id)), request); } function canChatOnRequest(request) { if (!request || !rideLifecycleChatStatuses.includes(request.status)) return false; if (activeRole() === "passenger") return requestBelongsToPassenger(request); if (activeRole() === "rider") return riderIdentityMatches(selectedRiderIdForRequest(request)); return false; } const riderOfferExpiryMs = 10 * 60 * 1000; function offerAgeMs(offer) { const createdAt = new Date(offer?.createdAt ?? 0).getTime(); if (!createdAt || Number.isNaN(createdAt)) return 0; return Math.max(0, Date.now() - createdAt); } function offerIsExpired(offer, request = selectedRequest()) { if (!offer?.id || !request || request.status !== "open") return false; if (request.selectedOfferId === offer.id) return false; return offerAgeMs(offer) > riderOfferExpiryMs; } function offerExpiryChip(offer, request = selectedRequest()) { if (!offer?.createdAt || !request || request.status !== "open") return null; const ageMs = offerAgeMs(offer); if (offerIsExpired(offer, request)) return "Expired offer"; const remainingMinutes = Math.max(1, Math.ceil((riderOfferExpiryMs - ageMs) / 60000)); return `Expires in ${remainingMinutes} min`; } function offerStatusChip(offer, request = selectedRequest()) { if (offerIsExpired(offer, request)) return "Expired"; if (offer?.type === "accepted") return "Accepted passenger fare"; const delta = Number(offer?.fare ?? 0) - Number(request?.fareOffer ?? 0); if (Number.isFinite(delta) && delta > 0) return "Counter-offer"; if (Number.isFinite(delta) && delta < 0) return "Lower fare offer"; return "Matches current fare"; } function offerFareDifference(offer, request) { return Math.abs(Number(offer?.fare ?? 0) - Number(request?.fareOffer ?? 0)); } function offerFareChangeTrail(offer) { return fareHistoryTrail(offer, offer?.fare, offer?.createdAt); } function sortOffersForPassenger(offers, request) { return [...offers].sort((a, b) => { const fareDifference = Number(a.fare ?? 0) - Number(b.fare ?? 0); if (fareDifference !== 0) return fareDifference; return new Date(a.createdAt ?? 0) - new Date(b.createdAt ?? 0); }); } function offerFareDeltaChip(offer, request) { if (activeRole() !== "passenger" || !request) return null; return fareChangeChipFromTrail(offerFareChangeTrail(offer), request.country); } function passengerOfferFareChangeChip(request) { if (activeRole() !== "passenger" || !request || request.status !== "open" || !requestBelongsToPassenger(request)) return null; const changedOffers = offersForRequest(request.id) .map((offer) => { const trail = offerFareChangeTrail(offer); return { offer, trail, change: fareChangeFromTrail(trail) }; }) .filter((item) => item.change) .sort((a, b) => new Date(b.change.changedAt ?? b.offer.createdAt ?? 0).getTime() - new Date(a.change.changedAt ?? a.offer.createdAt ?? 0).getTime()); const latest = changedOffers[0]; if (!latest) return null; return fareChangeChipFromTrail(latest.trail, request.country, "Rider fare "); } function canRefreshMarketplace() { return Boolean(hasSupabaseRuntime() && activeRole() !== "admin" && (hasSignedIn("passenger") || hasSignedIn("rider"))); } function areaProximityRpcMissing(error) { return /waka_area_distance_km|waka_city_span_km/i.test(error.message ?? String(error)); } function adminDirectoryRpcMissing(error) { return /schema cache|Could not find the function|function .* does not exist|404/i.test(error.message ?? String(error)); } function riderMarketplaceRpcBody(rider) { return { p_pickup_areas: pickupAreasWithinRiderRadius(rider), p_limit: riderMarketplacePageSize, p_offset: 0 }; } async function fetchRiderMarketplaceRpcRows(rider) { const body = riderMarketplaceRpcBody(rider); if (!supabaseClient) { return withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/rider_marketplace_requests", { method: "POST", body }), "Loading rider marketplace requests", optionalSupabaseRequestTimeoutMs ); } const { data, error } = await withSupabaseTimeout( supabaseClient.rpc("rider_marketplace_requests", body), "Loading rider marketplace requests", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return data ?? []; } async function fetchRiderMarketplaceGpsRpcRows(rider) { const body = riderMarketplaceRpcBody(rider); if (!supabaseClient) { return withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/rider_marketplace_requests_gps", { method: "POST", body }), "Loading GPS rider marketplace requests", optionalSupabaseRequestTimeoutMs ); } const { data, error } = await withSupabaseTimeout( supabaseClient.rpc("rider_marketplace_requests_gps", body), "Loading GPS rider marketplace requests", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return data ?? []; } async function fetchPassengerApproachRows() { if (!supabaseClient) { return withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/passenger_active_ride_approach", { method: "POST", body: {} }), "Loading passenger ride approach", optionalSupabaseRequestTimeoutMs ); } const { data, error } = await withSupabaseTimeout( supabaseClient.rpc("passenger_active_ride_approach"), "Loading passenger ride approach", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return data ?? []; } async function loadPassengerApproachFromSupabase() { if (passengerApproachRpcUnavailable || !hasSignedIn("passenger")) return false; try { const rows = await fetchPassengerApproachRows(); lastPassengerApproachSource = "passenger_active_ride_approach RPC"; const approachMap = new Map(rows.map((row) => { const mapped = mapPassengerApproachFromDatabase(row); return [mapped.requestId, mapped]; })); if (!approachMap.size) return false; state.requests = state.requests.map((request) => { const approach = approachMap.get(request.id); return approach ? { ...request, ...approach } : request; }); return true; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; passengerApproachRpcUnavailable = true; logClientWarning("Passenger active ride approach RPC is not installed yet. Falling back to offer distance snapshots.", error); return false; } } async function fetchActiveRideContactRows() { if (!supabaseClient) { return withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/active_ride_contacts", { method: "POST", body: {} }), "Loading active ride contacts", optionalSupabaseRequestTimeoutMs ); } const { data, error } = await withSupabaseTimeout( supabaseClient.rpc("active_ride_contacts"), "Loading active ride contacts", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return data ?? []; } async function loadActiveRideContactsFromSupabase() { if (activeRideContactRpcUnavailable || (!hasSignedIn("passenger") && !hasSignedIn("rider"))) return false; try { const rows = await fetchActiveRideContactRows(); const contactMap = new Map(rows.map((row) => { const mapped = mapActiveRideContactFromDatabase(row); return [mapped.requestId, mapped]; })); if (!contactMap.size) return false; state.requests = state.requests.map((request) => { const contact = contactMap.get(request.id); return contact ? { ...request, ...contact } : request; }); return true; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; activeRideContactRpcUnavailable = true; logClientWarning("Active ride contact RPC is not installed yet. Text chat still works after rider selection.", error); return false; } } function emptyMarketplaceTableResult() { return { data: [], warning: null, count: 0, limit: 0, offset: 0 }; } function uniqueMarketplaceIds(values = []) { return [...new Set(values.filter(Boolean))]; } function currentMarketplaceUserIds() { return uniqueMarketplaceIds([ state.passenger?.id, state.passenger?.supabaseUserId, state.sessions?.passenger?.userId, state.rider?.id, state.rider?.supabaseUserId, state.sessions?.rider?.userId ]); } const accountNotificationRefreshIntervalMs = 1000; const accountNotificationRefreshState = { passenger: { at: 0, promise: null, accountId: "", hydrated: false, startedAt: Date.now() }, rider: { at: 0, promise: null, accountId: "", hydrated: false, startedAt: Date.now() } }; let pushSubscriptionRpcUnavailable = false; function runtimeTableLoadOptions(table, limits = marketplaceSyncLoadLimits, page = 0) { const limit = limits[table] ?? null; return { count: true, limit, offset: limit ? Math.max(0, page) * limit : 0 }; } function runtimeTableCountFromContentRange(contentRange, fallbackCount) { const match = String(contentRange ?? "").match(/\/(\d+|\*)$/); if (!match || match[1] === "*") return fallbackCount; return Number(match[1]); } function applyRuntimeRestFilters(params, filters = []) { filters.forEach((filter) => { if (!filter?.column || filter.value === undefined || filter.value === null || filter.value === "") return; if (filter.operator === "in") { params.set(filter.column, `in.(${filter.value.join(",")})`); return; } params.set(filter.column, `${filter.operator ?? "eq"}.${filter.value}`); }); } function applyRuntimeSupabaseFilters(query, filters = []) { return filters.reduce((nextQuery, filter) => { if (!filter?.column || filter.value === undefined || filter.value === null || filter.value === "") return nextQuery; if (filter.operator === "ilike") return nextQuery.ilike(filter.column, filter.value); if (filter.operator === "in") return nextQuery.in(filter.column, filter.value); return nextQuery.eq(filter.column, filter.value); }, query); } async function selectRuntimeTable(table, select = "*", orderColumn = null, options = {}) { const limit = Number.isFinite(options.limit) ? options.limit : null; const offset = Number.isFinite(options.offset) ? Math.max(0, options.offset) : 0; const shouldCount = Boolean(options.count); const filters = options.filters ?? []; if (!supabaseClient && supabaseRestSession?.access_token) { const params = new URLSearchParams(); params.set("select", select); if (orderColumn) params.set("order", `${orderColumn}.desc`); if (limit) params.set("limit", `${limit}`); if (offset) params.set("offset", `${offset}`); applyRuntimeRestFilters(params, filters); const response = await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/${table}?${params.toString()}`, { headers: shouldCount ? { Prefer: "count=exact" } : {}, returnResponse: true }), `Loading ${table} records`, optionalSupabaseRequestTimeoutMs ); const data = response.data ?? []; return { data, warning: null, count: shouldCount ? runtimeTableCountFromContentRange(response.headers.get("content-range"), data.length) : data.length, limit, offset }; } let query = shouldCount ? supabaseClient.from(table).select(select, { count: "exact" }) : supabaseClient.from(table).select(select); query = applyRuntimeSupabaseFilters(query, filters); if (orderColumn) query = query.order(orderColumn, { ascending: false }); if (limit) { query = query.range(offset, offset + limit - 1); } else if (offset) { query = query.range(offset, offset + 999); } const { data, error, count } = await withSupabaseTimeout( query, `Loading ${table} records`, optionalSupabaseRequestTimeoutMs ); if (error) throw error; return { data: data ?? [], warning: null, count: shouldCount ? count ?? data?.length ?? 0 : data?.length ?? 0, limit, offset }; } function marketplaceTableOptions(table, filters = []) { return { ...runtimeTableLoadOptions(table, marketplaceSyncLoadLimits), filters: filters.filter(Boolean) }; } function marketplaceEqFilter(column, value) { return value ? { column, value } : null; } function marketplaceInFilter(column, values = []) { const scopedValues = uniqueMarketplaceIds(values); return scopedValues.length ? { column, operator: "in", value: scopedValues } : null; } async function selectScopedMarketplaceTable(table, orderColumn, filters = []) { const scopedFilters = filters.filter(Boolean); if (!scopedFilters.length) return emptyMarketplaceTableResult(); return selectRuntimeTable(table, "*", orderColumn, marketplaceTableOptions(table, scopedFilters)); } function pushNotificationsSupported() { return Boolean("Notification" in window && "serviceWorker" in navigator && "PushManager" in window); } function pushNotificationPublicKey() { return String(appConfig.pushNotificationPublicKey || window.WAKA_PUSH_PUBLIC_KEY || "").trim(); } function pushStatusElement(type) { return type === "rider" ? els.riderPushStatus : els.passengerPushStatus; } function pushButtonElement(type) { return type === "rider" ? els.riderEnablePush : els.passengerEnablePush; } function base64UrlToUint8Array(value) { const padding = "=".repeat((4 - (value.length % 4)) % 4); const base64 = `${value}${padding}`.replace(/-/g, "+").replace(/_/g, "/"); const raw = window.atob(base64); return Uint8Array.from([...raw].map((character) => character.charCodeAt(0))); } function pushSubscriptionRecordFromBrowser(type, subscription) { const json = subscription.toJSON(); const account = type === "rider" ? state.rider : state.passenger; return { id: makeId("push"), userId: account?.id, role: type, endpoint: json.endpoint, p256dh: json.keys?.p256dh, auth: json.keys?.auth, userAgent: navigator.userAgent, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; } async function savePushSubscriptionToSupabase(record) { if (!record?.endpoint || !record?.p256dh || !record?.auth) throw new Error("Push subscription is missing browser keys."); if (!hasSupabaseRuntime()) return record; if (!pushSubscriptionRpcUnavailable) { try { await callSupabaseRpc( "register_push_subscription", { p_endpoint: record.endpoint, p_p256dh: record.p256dh, p_auth: record.auth, p_user_agent: record.userAgent }, "Saving phone notification permission", supabaseProfileSaveTimeoutMs ); return record; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; pushSubscriptionRpcUnavailable = true; logClientWarning("Push subscription RPC is not installed yet. Falling back to direct table upsert.", error); } } assertClientFallbackAllowed("Push notification subscription", "supabase-notification-delivery.sql"); const payload = { user_id: record.userId, role: record.role, endpoint: record.endpoint, p256dh: record.p256dh, auth: record.auth, user_agent: record.userAgent, updated_at: record.updatedAt }; if (!supabaseClient) { await withSupabaseTimeout( supabaseRestRequest("/rest/v1/push_subscriptions?on_conflict=endpoint", { method: "POST", body: payload, headers: { Prefer: "resolution=merge-duplicates,return=minimal" } }), "Saving phone notification permission", supabaseProfileSaveTimeoutMs ); return record; } const { error } = await withSupabaseTimeout( supabaseClient .from("push_subscriptions") .upsert(payload, { onConflict: "endpoint" }), "Saving phone notification permission", supabaseProfileSaveTimeoutMs ); if (error) throw error; return record; } function updatePushNotificationControls(type) { const button = pushButtonElement(type); const status = pushStatusElement(type); if (!button || !status) return; const signedIn = hasSignedIn(type); button.hidden = !signedIn; status.hidden = !signedIn; if (!signedIn) return; if (!pushNotificationsSupported()) { button.disabled = true; status.textContent = "This browser does not support phone push notifications."; return; } const hasPushKey = Boolean(pushNotificationPublicKey()); const muted = !notificationPreferenceEnabled(type, "all"); button.disabled = Notification.permission === "denied" || muted; button.textContent = muted ? "Notifications muted" : Notification.permission === "granted" ? "Phone notifications enabled" : "Enable phone notifications"; status.textContent = muted ? "Notifications are muted for this account on this device. Turn on Allow notifications below to receive ride, fare, message, or Waka notices." : Notification.permission === "granted" ? hasPushKey ? "Phone push notifications are allowed on this device." : "Ride request popups are allowed while Waka is open on this device. Add pushNotificationPublicKey for closed-app push." : Notification.permission === "denied" ? "Phone notifications are blocked in this browser. Change site settings to allow them." : hasPushKey ? "Enable phone notifications to receive ride request popups and admin broadcasts on this device." : "Enable phone notifications for ride request popups while Waka is open. Closed-app push needs pushNotificationPublicKey."; } async function enableAccountPushNotifications(type) { const status = pushStatusElement(type); const button = pushButtonElement(type); if (!hasSignedIn(type)) { if (status) status.textContent = "Sign in before enabling phone notifications."; return; } if (!pushNotificationsSupported()) { if (status) status.textContent = "This browser does not support phone push notifications."; return; } try { if (button) button.disabled = true; if (status) status.textContent = "Requesting phone notification permission..."; const permission = await Notification.requestPermission(); if (permission !== "granted") { if (status) status.textContent = "Phone notification permission was not granted."; return; } const publicKey = pushNotificationPublicKey(); if (!publicKey) { if (status) status.textContent = "Ride request popups are enabled while Waka is open. Add pushNotificationPublicKey for closed-app push."; return; } const registration = await navigator.serviceWorker.ready; const existing = await registration.pushManager.getSubscription(); const subscription = existing ?? await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: base64UrlToUint8Array(publicKey) }); const saved = await savePushSubscriptionToSupabase(pushSubscriptionRecordFromBrowser(type, subscription)); state.pushSubscriptions = upsertById(state.pushSubscriptions, saved); saveState(); if (status) status.textContent = "Phone push notifications are enabled for Waka notices on this device."; } catch (error) { if (status) status.textContent = `Could not enable phone notifications: ${error.message}`; } finally { updatePushNotificationControls(type); } } function noticePopupAlreadyShown(notice) { const shown = state.notificationPopupIds ?? []; return noticePopupDeliveryKeys(notice).some((key) => shown.includes(key)); } function noticePopupSemanticEvent(notice) { const text = `${notice?.eventType || ""} ${notice?.id || ""} ${notice?.title || ""} ${notice?.body || ""}`.toLowerCase(); if (/nearby_ride_request|nearby_request|new nearby ride|new scheduled ride/.test(text)) return "nearby_ride_request"; if (/passenger_fare_increased|passenger.*(increased|updated).*fare|\bfare-/.test(text)) return "passenger_fare_increased"; if (/rider_counter_offer|new rider (counter-)?offer|rider.*(counter|updated).*fare|\boffer-/.test(text)) return "rider_counter_offer"; if (/passenger_rejected_offer|passenger left negotiation|declined this offer|rejected-offer/.test(text)) return "passenger_rejected_offer"; if (/rider_offer_withdrawn|rider left negotiation|withdrew their offer|offer-withdrawn/.test(text)) return "rider_offer_withdrawn"; if (/ride_matched|ride matched|\bmatched-/.test(text)) return "ride_matched"; if (/ride_reopened|rider_cancelled_before_pickup|rider_canceled_before_pickup|cancelled_before_pickup|canceled_before_pickup|rider cancelled|rider canceled|open again|reopened/.test(text)) return "ride_reopened"; if (/ride_cancelled|ride canceled|ride cancelled|\bcancelled-|\bcanceled-/.test(text)) return "ride_cancelled"; if (/route_change_requested|route-change-requested|route change request|requested a destination change|requested an added stop/.test(text)) return "route_change_requested"; if (/route_change_accepted|route-change-accepted|accepted route change|route change accepted|rider accepted route change/.test(text)) return "route_change_accepted"; if (/route_change_declined|route-change-declined|declined route change|route change declined|rider declined route change/.test(text)) return "route_change_declined"; if (/ride_arrived|rider arrived|\barrived-/.test(text)) return "ride_arrived"; if (/ride_started|ride started|\bstarted-/.test(text)) return "ride_started"; if (/ride_stop_arrived|added stop|\bstop-/.test(text)) return "ride_stop_arrived"; if (/ride_completed|ride completed|\bcompleted-/.test(text)) return "ride_completed"; if (/ride_chat_message|new ride message|\bchat-/.test(text)) return "ride_chat_message"; return String(notice?.eventType || notice?.title || "").trim().toLowerCase().replace(/\s+/g, "_"); } function noticePopupDeliveryKey(notice) { return noticePopupDeliveryKeys(notice)[0] ?? ""; } function noticePopupFareAmountToken(notice) { const text = `${notice?.title || ""} ${notice?.body || ""}`; const dollarMatches = [...text.matchAll(/\$\s*(\d+(?:\.\d{1,2})?)/g)]; if (dollarMatches.length) return dollarMatches[dollarMatches.length - 1][1].replace(/[^\d.]/g, ""); const fareMatch = text.match(/\b(?:fare|offer|offered|asks|asked|counter(?:ed)?|updated)\D{0,32}(\d+(?:\.\d{1,2})?)/i); return fareMatch?.[1]?.replace(/[^\d.]/g, "") || ""; } function noticePopupDeliveryKeys(notice) { if (!notice?.recipientRole || !notice?.requestId) return []; const event = noticePopupSemanticEvent(notice); if (!event) return []; const identityKey = notice.id ? [notice.id] : []; if (event === "ride_chat_message") { return [["notice-delivered", notice.recipientRole, notice.requestId, event, notice.id || notice.createdAt || Date.now()].filter(Boolean).join(":"), ...identityKey].filter(Boolean); } if (/^route_change_/.test(event)) { return [["notice-delivered", notice.recipientRole, notice.requestId, event, notice.id || notice.createdAt || notice.actionUrl || Date.now()].filter(Boolean).join(":"), ...identityKey].filter(Boolean); } const text = `${notice?.title || ""} ${notice?.body || ""}`; const amount = /(fare|offer)/.test(event) ? noticePopupFareAmountToken(notice) : ""; const actor = /(fare|offer)/.test(event) ? noticePopupActorToken(notice) : ""; const semanticKey = ["notice-delivered", notice.recipientRole, notice.requestId, event, actor, amount].filter(Boolean).join(":"); const keys = [ semanticKey, ...identityKey ]; if (/(fare|offer)/.test(event)) { keys.push(["notice-delivered", notice.recipientRole, notice.requestId, event, amount].filter(Boolean).join(":")); } return [...new Set(keys.filter(Boolean))]; } function upsertNotificationByDeliveryKey(items, notice) { const keys = new Set(noticePopupDeliveryKeys(notice)); if (!keys.size) return upsertById(items, notice); return [ notice, ...items.filter((item) => item.id !== notice.id && !noticePopupDeliveryKeys(item).some((key) => keys.has(key))) ]; } function uniqueNotificationsByDeliveryKey(notifications = []) { const seen = new Set(); return notifications.filter((notice) => { const keys = noticePopupDeliveryKeys(notice); const primaryKey = keys[0] || notice?.id || ""; if (!primaryKey) return true; if (seen.has(primaryKey) || keys.some((key) => seen.has(key))) return false; keys.forEach((key) => seen.add(key)); return true; }); } function noticePopupActorToken(notice) { if (notice?.createdBy) return `actor-${String(notice.createdBy).toLowerCase()}`; const text = `${notice?.title || ""} ${notice?.body || ""}`; const explicitActor = text.match(/\b(rider|passenger)\s+([A-Za-z0-9][A-Za-z0-9'.-]*)/i); if (explicitActor) return `${explicitActor[1].toLowerCase()}-${explicitActor[2].toLowerCase()}`; if (/passenger/i.test(text)) return "passenger"; if (/rider/i.test(text)) return "rider"; return ""; } function rideNoticePopupAllowed(type, notice, request) { if (!notice?.requestId) return true; const marker = `${notice.eventType || ""} ${notice.id || ""} ${notice.title || ""}`.toLowerCase(); const rideEndedMarker = /(cancel|cancelled|reject|rejected|withdrawn|left|no[-_\s]?longer[-_\s]?available|rider_offer_withdrawn|passenger_rejected_offer)/.test(marker); if (!request) return rideEndedMarker; if (request.status === "cancelled") return /(cancel|cancelled|ride_cancelled)/.test(marker); if (request.status === "completed" && !/(completed|ride_completed)/.test(marker)) return false; if (/(chat|ride_chat_message|matched|ride_matched)/.test(marker)) { if (!["matched", "arrived", "in_progress"].includes(request.status)) return false; if (type === "passenger") return requestBelongsToPassenger(request); if (type === "rider") return riderIdentityMatches(selectedRiderIdForRequest(request)); } if (/(nearby|offer|counter|fare|passenger_fare_increased|rider_counter_offer)/.test(marker)) { return request.status === "open"; } return true; } function addRideAccountNotice(type, title, body, requestId, eventKey = "", { autoFocus = true, createdBy = null, createdByRole = null } = {}) { const account = type === "rider" ? state.rider : state.passenger; if (!account?.id) return null; const notice = { id: eventKey ? `notice-${type}-${eventKey}` : makeId("notice"), recipientId: account.id, recipientRole: type, title, body, requestId, actionUrl: workspaceNotificationUrl(type, type === "rider" ? "requests" : "trips", requestId), eventType: eventKey ? eventKey.split("-")[0] : "", createdBy, createdByRole, createdAt: new Date().toISOString(), readAt: null }; if (noticeCreatedByCurrentAccount(type, notice)) return null; const alreadyDelivered = noticePopupAlreadyShown(notice); state.notifications = upsertNotificationByDeliveryKey(state.notifications, notice); const noticeRequest = requestId ? state.requests.find((request) => request.id === requestId) : null; const canDeliverNotice = rideNoticePopupAllowed(type, notice, noticeRequest); const autoFocusAllowed = Boolean(autoFocus && noticeRequest && shouldAutoFocusRideNotice(type, noticeRequest)); let queuedForRider = false; if (type === "rider" && autoFocus && noticeRequest && !autoFocusAllowed) { queueRiderDecisionUpdate(title, body, requestId, notice.eventType || "ride_update"); queuedForRider = true; } if (autoFocusAllowed) { focusRideNoticeView(type, noticeRequest, { refresh: false, replace: true }); } saveState(); const preferenceAllowed = noticeDeliveryAllowedByPreference(type, notice); let deliveredInline = false; if (queuedForRider) { rememberNoticePopup(notice); if (preferenceAllowed && !alreadyDelivered) { void showRideTransactionPhoneNotification({ role: type, page: type === "rider" ? "requests" : "trips", requestId, title, body, tag: `waka-${type}-${eventKey || requestId || Date.now()}` }); } return notice; } if (canDeliverNotice && preferenceAllowed && !alreadyDelivered && typeof showAccountNoticePopup === "function") { showAccountNoticePopup(type, notice, { autoFocusRide: false }); deliveredInline = noticePopupAlreadyShown(notice); } if (!canDeliverNotice || !preferenceAllowed || alreadyDelivered) return notice; rememberNoticePopup(notice); void showRideTransactionPhoneNotification({ role: type, page: type === "rider" ? "requests" : "trips", requestId, title, body, tag: `waka-${type}-${eventKey || requestId || Date.now()}` }); return notice; } function addRiderRideNotice(title, body, requestId, eventKey = "", options = {}) { return addRideAccountNotice("rider", title, body, requestId, eventKey, options); } function normalizeAccountNoticeForDelivery(type, notice, request = null) { if (type === "rider" && request && requestCancelledByCurrentRider(request) && /passenger cancelled/i.test(`${notice?.title || ""} ${notice?.body || ""}`)) { return { ...notice, title: "Ride cancelled", body: rideCancelledNoticeBodyForRider(request) }; } return notice; } function rememberNoticePopup(notice) { state.notificationPopupIds = [...new Set([ ...(state.notificationPopupIds ?? []), ...noticePopupDeliveryKeys(notice) ].filter(Boolean))].slice(-140); saveState(); } function visibleNoticePopupAlreadyShown(notice) { const keys = new Set(noticePopupDeliveryKeys(notice)); if (!keys.size) return false; return [...document.querySelectorAll(".notice-popup[data-notice-delivery-key]")] .some((node) => keys.has(node.dataset.noticeDeliveryKey)); } async function refreshRiderStateAfterAdminNotice(notice) { if (!hasSupabaseRuntime() || !hasSignedIn("rider")) return; const noticeText = `${notice?.title || ""} ${notice?.body || ""}`; if (!/(approved|approval|correction|checkr|background|eligibility|suspended|declined)/i.test(noticeText)) return; try { await hydrateProfileFromSupabase("rider"); await refreshPaymentAccountsFromSupabase("rider"); await loadMarketplaceFromSupabase({ includeAccountData: true }); renderAll(); } catch (error) { logClientWarning("Rider state could not be refreshed after admin notice.", error); } } function showAccountNoticePopup(type, notice, { autoFocusRide = false } = {}) { if (!notice?.id || noticePopupAlreadyShown(notice)) return; if (document.hidden) return; if (!noticeDeliveryAllowedByPreference(type, notice)) { rememberNoticePopup(notice); return; } if (type === "rider") void refreshRiderStateAfterAdminNotice(notice); const noticeText = `${notice.title} ${notice.body}`; const riderCorrectionNotice = type === "rider" && /correction/i.test(noticeText); const riderCheckrNotice = type === "rider" && /(checkr|background|eligibility)/i.test(noticeText); const noticeRequest = notice.requestId ? state.requests.find((request) => request.id === notice.requestId) : null; notice = normalizeAccountNoticeForDelivery(type, notice, noticeRequest); if (noticePopupAlreadyShown(notice) || visibleNoticePopupAlreadyShown(notice)) { rememberNoticePopup(notice); return; } if (!riderCorrectionNotice && !riderCheckrNotice && !rideNoticePopupAllowed(type, notice, noticeRequest)) return; rememberNoticePopup(notice); playNearbyRideCue(); const rideNotice = Boolean(noticeRequest && (type === "passenger" || type === "rider")); if (autoFocusRide && rideNotice) focusRideNoticeView(type, noticeRequest, { refresh: false, replace: true }); const node = document.createElement("aside"); node.className = `notice-popup${riderCorrectionNotice || riderCheckrNotice ? " notice-popup-actionable" : ""}`; node.dataset.noticeDeliveryKey = noticePopupDeliveryKey(notice); node.setAttribute("role", "status"); node.innerHTML = `
${escapeHtml(type === "rider" ? "Rider notice" : "Passenger notice")} ${escapeHtml(notice.title)}

${escapeHtml(notice.body)}

`; node.querySelector(".notice-popup-open")?.addEventListener("click", () => { if (type === "passenger" && rideNotice) focusPassengerRequestView(noticeRequest, { refresh: true, replace: true }); else if (type === "rider" && rideNotice) focusRiderRequestView(noticeRequest, { refresh: true, replace: true }); else if (type === "passenger") setPassengerWorkspacePage("notices"); if (type === "rider" && riderCorrectionNotice && typeof openRiderCorrectionForm === "function") openRiderCorrectionForm(); else if (type === "rider" && riderCheckrNotice && typeof setRiderWorkspacePage === "function") setRiderWorkspacePage("checks"); else if (type === "rider" && !rideNotice && typeof setRiderWorkspacePage === "function") setRiderWorkspacePage("notices"); else if (type === "rider" && !rideNotice) els.riderNoticePanel?.scrollIntoView({ behavior: "smooth", block: "start" }); node.remove(); }); node.querySelector(".notice-popup-close")?.addEventListener("click", () => node.remove()); document.body.append(node); if (!riderCorrectionNotice && !riderCheckrNotice) { window.setTimeout(() => node.remove(), 12000); } } function accountIdentityIdsForNoticeRole(type) { const account = type === "rider" ? state.rider : state.passenger; const session = type === "rider" ? state.sessions?.rider : state.sessions?.passenger; return uniqueMarketplaceIds([ account?.id, account?.supabaseUserId, session?.userId ]).map((id) => String(id)); } function noticeCreatedByCurrentAccount(type, notice) { const accountIds = accountIdentityIdsForNoticeRole(type); if (!accountIds.length || !notice?.createdBy) return false; const createdBy = String(notice.createdBy); const createdByRole = String(notice.createdByRole || "").toLowerCase(); if (createdByRole) { return createdByRole === type && accountIds.includes(createdBy); } return accountIds.includes(createdBy); } function deliverAccountNotificationNotice(type, notice, { autoFocusRide = false, deliverPhone = false, phoneTagPrefix = "account" } = {}) { if (!notice?.id || !hasSignedIn(type) || noticeCreatedByCurrentAccount(type, notice)) { return false; } const request = notice.requestId ? state.requests.find((item) => item.id === notice.requestId) : null; notice = normalizeAccountNoticeForDelivery(type, notice, request); if (deliverPhone) processQueuedPhoneDeliveryForNotice(type, notice); if (noticePopupAlreadyShown(notice)) return false; const semanticEvent = noticePopupSemanticEvent(notice); if (type === "passenger" && request && semanticEvent === "ride_reopened") { state.selectedRequestId = request.id; state.passengerPage = "trips"; if (typeof updatePassengerWorkspaceRoute === "function") updatePassengerWorkspaceRoute("trips", { replace: true, requestId: request.id }); showMandatoryPassengerRideReopenedNotice(request, notice.id || notice.createdAt || "", notice.createdBy || ""); if (typeof renderAll === "function") renderAll(); return true; } if (type === "rider" && semanticEvent === "route_change_requested") { if (!noticeDeliveryAllowedByPreference(type, notice)) { rememberNoticePopup(notice); return false; } const shown = request ? showRiderRouteChangeDecisionForRequest(request) : false; if (shown) { rememberNoticePopup(notice); return true; } queueRiderRouteChangeDecisionRetry(request ?? notice.requestId, notice); return true; } if (type === "rider" && request?.status === "open" && semanticEvent === "nearby_ride_request" && !document.hidden) { rememberNoticePopup(notice); return false; } if (!rideNoticePopupAllowed(type, notice, request)) return false; if (!noticeDeliveryAllowedByPreference(type, notice)) { rememberNoticePopup(notice); return false; } const autoFocusAllowed = Boolean(autoFocusRide && request && shouldAutoFocusRideNotice(type, request)); let queuedForRider = false; if (type === "rider" && autoFocusRide && request && !autoFocusAllowed) { queueRiderDecisionUpdate(notice.title, notice.body, notice.requestId, notice.eventType || "ride_update"); queuedForRider = true; } if (queuedForRider) { rememberNoticePopup(notice); if (deliverPhone) { void showRideTransactionPhoneNotification({ role: type, page: type === "rider" ? "requests" : "trips", request, requestId: notice.requestId, title: notice.title, body: notice.body, tag: `waka-${type}-${phoneTagPrefix}-${notice.id}` }); } return true; } showAccountNoticePopup(type, notice, { autoFocusRide: autoFocusAllowed }); if (deliverPhone) { rememberNoticePopup(notice); void showRideTransactionPhoneNotification({ role: type, page: type === "rider" ? "requests" : "trips", request, requestId: notice.requestId, title: notice.title, body: notice.body, tag: `waka-${type}-${phoneTagPrefix}-${notice.id}` }); return true; } if (noticePopupAlreadyShown(notice)) return true; return false; } async function refreshRideStateForAccountNotifications(notifications, reason = "account_notice_poll") { if (!notifications.some((notification) => notification.requestId) || !canRefreshMarketplace()) return; if (marketRefreshInFlight) { scheduleMarketplaceRealtimeRefresh(reason); return; } await refreshMarketplace({ silent: true, reason }); } async function refreshAccountNotificationsFromSupabase(type, options = {}) { if (!hasSupabaseRuntime() || !hasSignedIn(type)) return []; const account = type === "passenger" ? state.passenger : state.rider; const accountIds = accountIdentityIdsForNoticeRole(type); if (!accountIds.length) return []; const existingIds = new Set(currentAccountNotifications(type).map((notification) => notification.id)); const tracker = accountNotificationRefreshState[type] ?? accountNotificationRefreshState.passenger; if (tracker.accountId !== account.id) { tracker.accountId = account.id; tracker.hydrated = false; tracker.at = 0; tracker.startedAt = Date.now(); } const initialHydration = !tracker.hydrated; const now = Date.now(); if (!options.force && tracker.promise) return tracker.promise; if (!options.force && now - tracker.at < accountNotificationRefreshIntervalMs) { return currentAccountNotifications(type); } tracker.at = now; tracker.promise = selectScopedMarketplaceTable("admin_notifications", "created_at", [ marketplaceInFilter("recipient_id", accountIds), marketplaceEqFilter("recipient_role", type) ]) .then(async (result) => { const notifications = (result.data ?? []).map(mapAdminNotificationFromDatabase); state.notifications = state.notifications.filter((notification) => !(accountIds.includes(String(notification.recipientId)) && notification.recipientRole === type)); notifications.forEach((notification) => { state.notifications = upsertNotificationByDeliveryKey(state.notifications, notification); }); saveState(); tracker.hydrated = true; const newNotifications = uniqueNotificationsByDeliveryKey(notifications .filter((notification) => { if (options.force || existingIds.has(notification.id)) return false; if (!initialHydration) return true; const createdAt = new Date(notification.createdAt ?? 0).getTime(); return Number.isFinite(createdAt) && createdAt >= (tracker.startedAt ?? now) - 1000; }) .filter((notification) => !noticeCreatedByCurrentAccount(type, notification))) .slice(0, 5); if (options.refreshRide) { await refreshRideStateForAccountNotifications(newNotifications); } if (newNotifications.some((notification) => notification.requestId)) { forceMarketplaceRefreshSoon(`${type}_account_notice`); } newNotifications.forEach((notification) => { deliverAccountNotificationNotice(type, notification, { autoFocusRide: true, deliverPhone: Boolean(options.deliverPhone), phoneTagPrefix: "notice-poll" }); }); return currentAccountNotifications(type); }) .catch((error) => { logClientWarning(`${type} admin notices could not be refreshed.`, error); return currentAccountNotifications(type); }) .finally(() => { tracker.promise = null; }); return tracker.promise; } function mergeMarketplaceTableResults(results = []) { const rowsById = new Map(); const warnings = []; results.forEach((result) => { if (result?.warning) warnings.push(result.warning); (result?.data ?? []).forEach((row) => { rowsById.set(row.id ?? JSON.stringify(row), row); }); }); const rows = [...rowsById.values()]; return { data: rows, warning: warnings[0] ?? null, count: rows.length, limit: null, offset: 0 }; } async function selectAnyUserScopedMarketplaceTable(table, orderColumn, columns = []) { const userIds = currentMarketplaceUserIds(); if (!userIds.length || !columns.length) return emptyMarketplaceTableResult(); const results = await Promise.all(columns.map((column) => ( selectScopedMarketplaceTable(table, orderColumn, [marketplaceInFilter(column, userIds)]) ))); return mergeMarketplaceTableResults(results); } async function selectRiderCompletedMileageSegmentsForRequests(riderIds = [], requestIds = []) { const scopedRiderIds = uniqueMarketplaceIds(riderIds); const scopedRequestIds = uniqueMarketplaceIds(requestIds); if (!scopedRiderIds.length || !scopedRequestIds.length) return emptyMarketplaceTableResult(); try { return await selectScopedMarketplaceTable("insurance_telemetry_segments", "started_at", [ marketplaceInFilter("rider_id", scopedRiderIds), marketplaceInFilter("ride_request_id", scopedRequestIds), marketplaceEqFilter("status", "closed") ]); } catch (error) { logClientWarning("Completed rider mileage could not be loaded; marketplace refresh will continue.", error); return emptyMarketplaceTableResult(); } } async function selectRideRouteChangesForMarketplace() { try { return await selectAnyUserScopedMarketplaceTable("ride_route_changes", "created_at", ["passenger_id", "rider_id"]); } catch (error) { logClientWarning("Ride route changes could not be loaded; chat fallback will continue.", error); return emptyMarketplaceTableResult(); } } async function selectRideRouteChangesForRequests(requestIds = []) { const scopedRequestIds = uniqueMarketplaceIds(requestIds); if (!scopedRequestIds.length) return emptyMarketplaceTableResult(); try { return await selectScopedMarketplaceTable("ride_route_changes", "created_at", [marketplaceInFilter("ride_request_id", scopedRequestIds)]); } catch (error) { logClientWarning("Ride route changes could not be loaded by ride request; account scoped fallback will continue.", error); return emptyMarketplaceTableResult(); } } async function loadPassengerRideRequestsFromSupabase(passengerId = state.passenger?.id) { if (!hasSupabaseRuntime() || !passengerId) return []; const requestsResult = await selectScopedMarketplaceTable("ride_requests", "created_at", [ marketplaceEqFilter("passenger_id", passengerId) ]); const requestRows = requestsResult.data ?? []; const requestIds = uniqueMarketplaceIds(requestRows.map((request) => request.id)); const [offersResult, chatsResult, routeChangesResult, routeChangesByRequestResult] = await Promise.all([ selectScopedMarketplaceTable("ride_offers", "created_at", [marketplaceInFilter("ride_request_id", requestIds)]), selectScopedMarketplaceTable("ride_chats", "created_at", [marketplaceInFilter("ride_request_id", requestIds)]), selectRideRouteChangesForMarketplace(), selectRideRouteChangesForRequests(requestIds) ]); const offers = (offersResult.data ?? []).map(mapOfferFromDatabase); const offerMap = new Map(offers.map((offer) => [offer.id, offer])); const previousRequestMap = stateLookupIndexes().requestMap; const requests = requestRows.map((request) => { const mapped = mapRideRequestFromDatabase(request, new Map(), offerMap); return preserveRideRequestPickup(mapped, previousRequestMap.get(mapped.id)); }); const requestIdSet = new Set(requestIds); state.requests = [ ...state.requests.filter((request) => request.passengerId !== passengerId), ...requests ]; state.offers = [ ...state.offers.filter((offer) => !requestIdSet.has(offer.requestId)), ...offers ]; state.chats = [ ...state.chats.filter((message) => !requestIdSet.has(message.requestId)), ...(chatsResult.data ?? []).map(mapChatFromDatabase) ]; mergeMarketplaceTableResults([routeChangesResult, routeChangesByRequestResult]).data.map(mapRideRouteChangeFromDatabase).forEach((change) => { state.routeChangeRequests = upsertById(state.routeChangeRequests ?? [], change); }); if (typeof mergeRouteChangeEventsFromChats === "function") { mergeRouteChangeEventsFromChats(state.chats); } saveState(); return requests; } async function selectMarketplaceRequests() { const rider = activeRole() === "rider" ? currentRiderRecord() : null; if (riderCanSeeRequests(rider) && !gpsMatchingRpcUnavailable) { try { const rows = await fetchRiderMarketplaceGpsRpcRows(rider); lastMarketplaceSyncSource = "rider_marketplace_requests_gps RPC"; return { data: riderShowsAllNearbyPickups() ? rows : rows.filter((request) => requestDestinationMatchesDailyRegions(mapRideRequestFromDatabase(request), rider)), warning: null, count: rows[0]?.total_count ?? rows.length, limit: riderMarketplacePageSize, offset: 0, source: "rider_gps_rpc" }; } catch (error) { if (areaProximityRpcMissing(error)) areaProximityRpcUnavailable = true; if (!adminDirectoryRpcMissing(error)) throw error; gpsMatchingRpcUnavailable = true; logClientWarning( areaProximityRpcMissing(error) ? "Server-side area proximity helper is not installed yet. Falling back to local distance estimates." : "GPS/PostGIS rider marketplace RPC is not installed yet. Falling back to the GPS-gated area-distance marketplace RPC.", error ); } } if (riderCanSeeRequests(rider) && !riderMarketplaceRpcUnavailable) { try { const rows = await fetchRiderMarketplaceRpcRows(rider); lastMarketplaceSyncSource = "rider_marketplace_requests RPC"; return { data: riderShowsAllNearbyPickups() ? rows : rows.filter((request) => requestDestinationMatchesDailyRegions(mapRideRequestFromDatabase(request), rider)), warning: null, count: rows[0]?.total_count ?? rows.length, limit: riderMarketplacePageSize, offset: 0, source: "rider_rpc" }; } catch (error) { if (areaProximityRpcMissing(error)) areaProximityRpcUnavailable = true; if (!adminDirectoryRpcMissing(error)) throw error; riderMarketplaceRpcUnavailable = true; logClientWarning( areaProximityRpcMissing(error) ? "Server-side area proximity helper is not installed yet. Falling back to capped table reads." : "Rider marketplace RPC is not installed yet. Falling back to capped table reads.", error ); } } lastMarketplaceSyncSource = "capped table reads"; if (riderCanSeeRequests(rider)) { assertClientFallbackAllowed("Rider marketplace table read", "supabase-rider-marketplace-rpc.sql"); return selectScopedMarketplaceTable("ride_requests", "created_at", [ marketplaceEqFilter("status", "open"), marketplaceEqFilter("country", rider.country), marketplaceEqFilter("city", rider.city), marketplaceEqFilter("vehicle_preference", rider.vehicle) ]); } if (state.passenger?.id) { return selectScopedMarketplaceTable("ride_requests", "created_at", [ marketplaceEqFilter("passenger_id", state.passenger.id) ]); } return emptyMarketplaceTableResult(); } function rememberRiderDayPreferences(preferences) { preferences.forEach((preference) => { state.riderDayPreferences = upsertById( state.riderDayPreferences.filter((item) => !(item.riderId === preference.riderId && item.serviceDate === preference.serviceDate)), preference ); if (state.rider?.id === preference.riderId && preference.serviceDate === localDateKey()) { state.rider = { ...state.rider, dailyRegions: preference }; state.riderDestinationScope = preference.showAllNearbyPickups ? "all" : "preferred"; state.riders = upsertById(state.riders, state.rider); } }); } function pruneStaleRiderMarketplaceRequests(latestRequestIds = new Set(), rider = currentRiderRecord()) { if (activeRole() !== "rider" || !rider) return new Set(); const latestIds = latestRequestIds instanceof Set ? latestRequestIds : new Set(latestRequestIds); const removedIds = new Set(); state.requests = state.requests.filter((request) => { if (!request?.id || latestIds.has(request.id)) return true; if (requestBelongsToPassenger(request)) return true; if (requestIsActiveForCurrentRider(request, rider)) return true; if (request.country !== rider.country || request.city !== rider.city) return true; if (request.vehicle !== "car" || rider.vehicle !== "car") return true; removedIds.add(request.id); return false; }); if (removedIds.size) { state.offers = state.offers.filter((offer) => !removedIds.has(offer.requestId)); state.chats = state.chats.filter((message) => !removedIds.has(message.requestId)); if (removedIds.has(state.selectedRequestId)) state.selectedRequestId = null; } return removedIds; } function marketplaceStateSnapshot() { return { requests: new Map(state.requests.map((request) => [request.id, { status: request.status, fareOffer: Number(request.fareOffer ?? 0), selectedOfferId: request.selectedOfferId ?? null, selectedRiderId: selectedRiderIdForRequest(request), matchedAt: request.matchedAt ?? null, country: request.country, city: request.city, vehicle: request.vehicle, passengerId: request.passengerId ?? null, createdAt: request.createdAt ?? null }])), offerIds: new Set(state.offers.map((offer) => offer.id)), offers: new Map(state.offers.map((offer) => [offer.id, { requestId: offer.requestId, riderId: offer.riderId, fare: Number(offer.fare ?? 0), type: offer.type ?? "", createdAt: offer.createdAt ?? null }])), riderOfferRequestIds: new Set(state.offers .filter((offer) => riderIdentityMatches(offer.riderId)) .map((offer) => offer.requestId)), chatIds: new Set(state.chats.map((message) => message.id)), routeChanges: new Map((state.routeChangeRequests ?? []).map((change) => [change.id, { requestId: change.requestId, status: change.status, type: change.type, requestedAt: change.requestedAt ?? null, decidedAt: change.decidedAt ?? null }])), notificationIds: new Set(state.notifications.map((notification) => notification.id)) }; } function accountIdForNoticeRole(type) { return type === "rider" ? state.rider?.id : state.passenger?.id; } function incomingChatMessageForRole(message, type, request) { if (!message || message.sender === "system" || !request) return false; if (!["matched", "arrived", "in_progress"].includes(request.status)) return false; const accountId = accountIdForNoticeRole(type); if (!accountId) return false; if (message.sender === type) return false; if (message.senderId === accountId && !["passenger", "rider"].includes(message.sender)) return false; if (type === "passenger") return requestBelongsToPassenger(request); if (type === "rider") return selectedRiderIdForRequest(request) === accountId; return false; } function chatNoticeBodyForRole(type, message) { const sender = type === "rider" ? "Passenger" : "Rider"; const preview = String(message?.text ?? "").replace(/\s+/g, " ").trim(); const clipped = preview.length > 90 ? `${preview.slice(0, 87)}...` : preview; return clipped ? `${sender}: ${clipped}` : `${sender} sent a ride chat message.`; } function loadedNoticeIsFreshForPopup(notice) { const createdAt = new Date(notice?.createdAt ?? 0).getTime(); if (!Number.isFinite(createdAt) || createdAt <= 0) return false; return Date.now() - createdAt <= marketplaceLoadedNoticePopupMaxAgeMs; } function deliverLoadedAccountNotifications(type, previous) { if (!previous?.notificationIds || !hasSignedIn(type)) return; uniqueNotificationsByDeliveryKey(currentAccountNotifications(type) .filter((notice) => !previous.notificationIds.has(notice.id)) .filter(loadedNoticeIsFreshForPopup)) .slice(0, 5) .reverse() .forEach((notice) => { deliverAccountNotificationNotice(type, notice, { autoFocusRide: true, deliverPhone: true, phoneTagPrefix: "loaded" }); }); } function riderActiveRideNavigationUrl(request) { if (!request || !requestIsActiveForCurrentRider(request)) return ""; if (typeof riderPickupNavigationShouldWaitForDropoff === "function" && riderPickupNavigationShouldWaitForDropoff(request)) return ""; if (request.status === "matched") return riderPickupNavigationUrl(request); if (request.status === "in_progress") return nextRideLegNavigationUrl(request); return ""; } function riderActiveRideNavigationKey(request, prefix = "rider-active-nav") { const leg = nextRideLeg(request); const timestamp = request.status === "matched" ? request.matchedAt || request.selectedOfferId || "match" : request.status === "in_progress" ? request.startedAt || request.lastStopArrivedAt || request.updatedAt || "in-progress" : request.updatedAt || "ride"; return `${prefix}-${request.id}-${request.status}-${leg.type}-${leg.index ?? rideStopIndex(request)}-${timestamp}`; } function openRiderActiveRideNavigation(request, eventKey = "") { if (!request || !requestIsActiveForCurrentRider(request)) return false; const url = riderActiveRideNavigationUrl(request); if (!url) return false; const key = eventKey || riderActiveRideNavigationKey(request); const opened = openNavigationUrl(url, { auto: true }); if (opened) { state.notificationPopupIds = [...new Set([...(state.notificationPopupIds ?? []), key])].slice(-100); saveState(); } return opened; } function openRiderPickupNavigation(request, eventKey = "") { if (!request || request.status !== "matched") return false; return openRiderActiveRideNavigation(request, eventKey || riderActiveRideNavigationKey(request, "rider-pickup-nav")); } async function refreshMatchedRequestForPickupNavigation(requestId) { if (!requestId || !canRefreshMarketplace()) return state.requests.find((request) => request.id === requestId) ?? null; const startedAt = Date.now(); while (marketRefreshInFlight && Date.now() - startedAt < 3000) { await new Promise((resolve) => window.setTimeout(resolve, 120)); } if (!marketRefreshInFlight && typeof loadMarketplaceFromSupabase === "function") { await loadMarketplaceFromSupabase({ includeAccountData: true }); if (typeof renderAll === "function") renderAll(); } return state.requests.find((request) => request.id === requestId) ?? null; } async function openRiderPickupNavigationWhenPrecise(requestOrId, eventKey = "") { const requestId = typeof requestOrId === "string" ? requestOrId : requestOrId?.id; if (!requestId) return false; let request = typeof requestOrId === "string" ? state.requests.find((item) => item.id === requestId) : requestOrId; let navKey = eventKey || (request ? riderActiveRideNavigationKey(request, "rider-pickup-nav") : `rider-pickup-nav-${requestId}`); if (!shouldOpenRiderPickupNavigation(request, navKey)) return false; if (!requestHasPrecisePickupNavigation(request)) { request = await refreshMatchedRequestForPickupNavigation(requestId) ?? request; navKey = eventKey || riderActiveRideNavigationKey(request, "rider-pickup-nav"); } if (!requestHasPrecisePickupNavigation(request)) { logClientWarning("Skipped automatic rider pickup navigation until exact pickup GPS or street address is available.", { requestId }); return false; } if (!shouldOpenRiderPickupNavigation(request, navKey)) return false; return openRiderPickupNavigation(request, navKey); } function shouldOpenRiderActiveRideNavigation(request, eventKey) { return Boolean(request && eventKey && requestIsActiveForCurrentRider(request) && ["matched", "in_progress"].includes(request.status) && !(typeof riderPickupNavigationShouldWaitForDropoff === "function" && riderPickupNavigationShouldWaitForDropoff(request)) && !(state.notificationPopupIds ?? []).includes(eventKey)); } function shouldOpenRiderPickupNavigation(request, eventKey) { return Boolean(request && request.status === "matched" && shouldOpenRiderActiveRideNavigation(request, eventKey)); } function ensureRiderActiveRideNavigation() { if (activeRole() !== "rider" || !state.rider?.id) return false; const request = activeRideForRole(selectedRequest()); if (!request || !["matched", "in_progress"].includes(request.status)) return false; const key = riderActiveRideNavigationKey(request); if (!shouldOpenRiderActiveRideNavigation(request, key)) return false; return openRiderActiveRideNavigation(request, key); } function openQueuedRiderPickupAfterDropoff(completedRequestId = "") { if (activeRole() !== "rider" || !state.rider?.id) return false; const request = queuedRiderPickupAfterDropoff(completedRequestId); if (!request) return false; state.selectedRequestId = request.id; state.riderPage = "requests"; if (typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute("requests", { replace: true, requestId: request.id }); } saveState(); const navKey = riderActiveRideNavigationKey(request, "rider-pickup-nav-after-dropoff"); if (typeof openRiderPickupNavigationWhenPrecise === "function") { void openRiderPickupNavigationWhenPrecise(request, navKey); return true; } if (requestHasPrecisePickupNavigation(request) && shouldOpenRiderPickupNavigation(request, navKey)) { return openRiderPickupNavigation(request, navKey); } return false; } function showMandatoryPassengerRideReopenedNotice(request, eventKey = "", actorId = "") { if (!request?.id || activeRole() !== "passenger" || !state.passenger?.id) return; const title = "Rider cancelled before pickup"; const body = "The rider cancelled before pickup. Your ride request is still open in the marketplace for another rider to accept."; const canonicalNotice = { id: `notice-passenger-ride-reopened-${request.id}-${eventKey || request.releasedAt || "now"}`, recipientRole: "passenger", title, body, requestId: request.id, eventType: "ride_reopened", createdBy: actorId || null, createdByRole: actorId ? "rider" : null }; const key = `mandatory-passenger-ride-reopened-${request.id}-${actorId || "rider"}-${eventKey || request.releasedAt || "now"}`; if ((state.notificationPopupIds ?? []).includes(key) || noticePopupAlreadyShown(canonicalNotice) || visibleNoticePopupAlreadyShown(canonicalNotice)) return; rememberNoticePopup(canonicalNotice); state.notificationPopupIds = [...new Set([...(state.notificationPopupIds ?? []), key])].slice(-140); saveState(); if (document.hidden) { void showRideTransactionPhoneNotification({ role: "passenger", page: "trips", request, requestId: request.id, title, body, tag: `waka-passenger-${key}` }); return; } playNearbyRideCue(); const node = document.createElement("aside"); node.className = "notice-popup notice-popup-actionable"; node.dataset.noticeDeliveryKey = noticePopupDeliveryKey(canonicalNotice); node.setAttribute("role", "status"); node.innerHTML = `
Passenger notice ${escapeHtml(title)}

${escapeHtml(body)}

`; node.querySelector(".notice-popup-open")?.addEventListener("click", () => { focusPassengerRequestView(request, { refresh: true, replace: true }); node.remove(); }); node.querySelector(".notice-popup-close")?.addEventListener("click", () => node.remove()); document.body.append(node); window.setTimeout(() => node.remove(), 16000); } function notifyMarketplaceChanges(previous) { if (!previous) return; if (activeRole() === "passenger" && state.passenger?.id) { const passengerRequests = state.requests.filter((request) => requestBelongsToPassenger(request)); const passengerRequestIds = new Set(passengerRequests.map((request) => request.id)); passengerRequests.forEach((request) => { const before = previous.requests.get(request.id); const matchedNow = selectedRiderIdForRequest(request) && ["matched", "arrived", "in_progress"].includes(request.status); const wasMatched = before?.selectedRiderId && ["matched", "arrived", "in_progress"].includes(before.status); if (matchedNow && !wasMatched) { if (passengerInitiatedRideMatchRequestIds.has(request.id)) { focusPassengerRequestView(request, { refresh: false, replace: true }); return; } addRideAccountNotice( "passenger", `Rider ${selectedRiderFirstNameForRequest(request)} accepted your fare`, `Rider ${selectedRiderFirstNameForRequest(request)} accepted your fare. Track pickup from your ride card.`, request.id, `matched-${request.id}-${request.matchedAt || request.selectedOfferId || "now"}`, { createdBy: selectedRiderIdForRequest(request) || null, createdByRole: selectedRiderIdForRequest(request) ? "rider" : null } ); } if (before && before.status !== request.status) { if (request.status === "open" && requestReopenedAfterRiderCancellation(request, before)) { state.selectedRequestId = request.id; state.passengerPage = "trips"; if (typeof updatePassengerWorkspaceRoute === "function") updatePassengerWorkspaceRoute("trips", { replace: true, requestId: request.id }); const popupCountBeforeNotice = document.querySelectorAll(".notice-popup").length; addRideAccountNotice( "passenger", "Rider cancelled before pickup", "The rider cancelled before pickup. Your ride request is still open in the marketplace for another rider to accept.", request.id, `ride_reopened-${request.id}-${request.cancelledAt || request.releasedAt || "now"}`, { createdBy: before.selectedRiderId || null, createdByRole: before.selectedRiderId ? "rider" : null } ); if (document.querySelectorAll(".notice-popup").length <= popupCountBeforeNotice) { showMandatoryPassengerRideReopenedNotice(request, `${request.cancelledAt || request.releasedAt || "now"}`, before.selectedRiderId || ""); } } else if (request.status === "arrived") { addRideAccountNotice( "passenger", "Rider arrived at pickup", "Your rider has arrived at the pickup point. Meet the rider when you are ready.", request.id, `arrived-${request.id}-${request.arrivedAt || "now"}`, { createdBy: selectedRiderIdForRequest(request) || null, createdByRole: selectedRiderIdForRequest(request) ? "rider" : null } ); } else if (request.status === "in_progress") { addRideAccountNotice( "passenger", "Ride started", "Rider confirmed pickup. The ride is now in progress.", request.id, `started-${request.id}-${request.startedAt || "now"}`, { createdBy: selectedRiderIdForRequest(request) || null, createdByRole: selectedRiderIdForRequest(request) ? "rider" : null } ); } else if (request.status === "completed") { addRideAccountNotice( "passenger", "Ride completed", "Rider marked drop-off complete. Fare settlement is being processed.", request.id, `completed-${request.id}-${request.completedAt || "now"}`, { createdBy: selectedRiderIdForRequest(request) || null, createdByRole: selectedRiderIdForRequest(request) ? "rider" : null } ); } } }); state.offers.forEach((offer) => { const beforeOffer = previous.offers?.get(offer.id); const offerChanged = Boolean(beforeOffer && ( Number(beforeOffer.fare ?? 0) !== Number(offer.fare ?? 0) || beforeOffer.type !== (offer.type ?? "") || beforeOffer.createdAt !== (offer.createdAt ?? null) )); if (!passengerRequestIds.has(offer.requestId) || (beforeOffer && !offerChanged)) return; const request = state.requests.find((item) => item.id === offer.requestId); if (!request || request.status !== "open") return; const rider = stateLookupIndexes().riderMap.get(offer.riderId); const formattedFare = formatMoney(offer.fare, request.country); const riderFirstName = firstNameOnly(rider?.name, "Rider"); const title = `Rider ${riderFirstName} updated fare offer to ${formattedFare}`; addRideAccountNotice( "passenger", title, `Rider ${riderFirstName} updated fare offer to ${formattedFare}. Review the offer to accept or counter.`, request.id, `offer-${offer.id}-${offer.updatedAt || offer.createdAt || "updated"}-${offer.fare}`, { createdBy: offer.riderId || null, createdByRole: offer.riderId ? "rider" : null } ); }); state.chats.forEach((message) => { if (previous.chatIds.has(message.id)) return; const request = state.requests.find((item) => item.id === message.requestId); if (!incomingChatMessageForRole(message, "passenger", request)) return; addRideAccountNotice( "passenger", "New ride message", chatNoticeBodyForRole("passenger", message), request.id, `chat-${message.id}` ); }); (state.routeChangeRequests ?? []).forEach((change) => { const before = previous.routeChanges?.get(change.id); if (before?.status === change.status || !["accepted", "declined"].includes(change.status)) return; const request = state.requests.find((item) => item.id === change.requestId); if (!request || !requestBelongsToPassenger(request)) return; const label = routeChangeTypeLabel(change.type).toLowerCase(); const title = change.status === "accepted" ? "Rider accepted route change" : "Rider declined route change"; const body = change.status === "accepted" ? `The rider accepted the ${label}. The route and fare are updated.` : `The rider declined the ${label}. The agreed route and fare stay unchanged.`; addRideAccountNotice( "passenger", title, body, request.id, `route-change-${change.status}-${change.id}-${change.decidedAt || "now"}`, { createdBy: selectedRiderIdForRequest(request) || null, createdByRole: selectedRiderIdForRequest(request) ? "rider" : null } ); }); deliverLoadedAccountNotifications("passenger", previous); } if (activeRole() === "rider" && state.rider?.id) { const currentRequestIds = new Set(state.requests.map((request) => request.id)); state.requests.forEach((request) => { const before = previous.requests.get(request.id); const wasRelevantToRider = Boolean(before && ( riderIdentityMatches(before.selectedRiderId) || previous.riderOfferRequestIds?.has(request.id) || state.selectedRequestId === request.id )); if (wasRelevantToRider && before.status !== request.status && request.status === "open" && requestReopenedAfterRiderCancellation(request, before)) { if (state.selectedRequestId === request.id) state.selectedRequestId = null; return; } if (wasRelevantToRider && before.status !== request.status && request.status === "cancelled") { if (state.selectedRequestId === request.id) state.selectedRequestId = null; addRiderRideNotice( "Ride cancelled", rideCancelledNoticeBodyForRider(request), request.id, `cancelled-${request.id}-${request.cancelledAt || "now"}`, { createdBy: request.passengerId || null, createdByRole: request.passengerId ? "passenger" : null } ); return; } const matchedToRider = requestIsActiveForCurrentRider(request); const wasMatchedToRider = riderIdentityMatches(before?.selectedRiderId) && ["matched", "arrived", "in_progress"].includes(before.status); if (matchedToRider && !wasMatchedToRider) { const currentRide = riderInProgressImmediateRide(currentRiderRecord()); const queuedUntilDropoff = riderPickupNavigationShouldWaitForDropoff(request); if (queuedUntilDropoff && currentRide?.id && currentRide.id !== request.id) { state.selectedRequestId = currentRide.id; state.riderPage = "requests"; if (typeof updateRiderWorkspaceRoute === "function") updateRiderWorkspaceRoute("requests", { replace: true, requestId: currentRide.id }); } else if (!riderHasActiveRequestDecisionInProgress(request.id)) { state.selectedRequestId = request.id; state.riderPage = "requests"; if (typeof updateRiderWorkspaceRoute === "function") updateRiderWorkspaceRoute("requests", { replace: true, requestId: request.id }); } addRiderRideNotice( "Passenger accepted your offer", queuedUntilDropoff ? `Passenger accepted your offer. This next pickup is queued until the current ride is dropped off.` : `Passenger accepted your offer. Head to ${requestPickupDisplayText(request)} using your saved navigation preference.`, request.id, `matched-${request.id}-${request.matchedAt || request.selectedOfferId || "now"}`, { createdBy: request.passengerId || null, createdByRole: request.passengerId ? "passenger" : null } ); if (!queuedUntilDropoff) { const navKey = riderActiveRideNavigationKey(request, "rider-pickup-nav"); if (typeof openRiderPickupNavigationWhenPrecise === "function") { void openRiderPickupNavigationWhenPrecise(request, navKey); } else if (requestHasPrecisePickupNavigation(request) && shouldOpenRiderPickupNavigation(request, navKey)) { openRiderPickupNavigation(request, navKey); } } } else if (matchedToRider && before && before.status !== request.status && request.status === "in_progress") { const navKey = riderActiveRideNavigationKey(request, "rider-next-leg-nav"); if (shouldOpenRiderActiveRideNavigation(request, navKey)) openRiderActiveRideNavigation(request, navKey); } else if (request.status === "open" && before && Number(request.fareOffer ?? 0) > Number(before.fareOffer ?? 0)) { const ownOffer = state.offers.find((offer) => offer.requestId === request.id && riderIdentityMatches(offer.riderId)); if (ownOffer) { const formattedFare = formatMoney(request.fareOffer, request.country); const passengerFirstName = passengerFirstNameForRequest(request); addRiderRideNotice( `Passenger ${passengerFirstName} updated fare offer to ${formattedFare}`, `Passenger ${passengerFirstName} updated fare offer to ${formattedFare}. Review the request to accept or counter.`, request.id, `fare-${request.id}-${request.fareOffer}`, { createdBy: request.passengerId || null, createdByRole: request.passengerId ? "passenger" : null } ); } } }); (state.routeChangeRequests ?? []).forEach((change) => { const before = previous.routeChanges?.get(change.id); if (before?.status === change.status || change.status !== "pending") return; const request = state.requests.find((item) => item.id === change.requestId); if (!request || !riderIdentityMatches(selectedRiderIdForRequest(request)) || !["matched", "arrived", "in_progress"].includes(request.status)) return; showRiderRouteChangeDecisionForRequest(request, change, { render: false }); const label = routeChangeTypeLabel(change.type).toLowerCase(); const addOn = formatMoney(change.additionalFare, request.country); const total = formatMoney(change.totalFare, request.country); addRiderRideNotice( change.type === "add_stop" ? "Passenger requested an added stop" : "Passenger requested a destination change", `Review the ${label} now. Added fare: ${addOn}. New total if accepted: ${total}.`, request.id, `route-change-requested-${change.id}-${change.requestedAt || "now"}`, { autoFocus: false, createdBy: request.passengerId || null, createdByRole: request.passengerId ? "passenger" : null } ); }); previous.requests.forEach((before, requestId) => { if (currentRequestIds.has(requestId)) return; const sameMarketplace = before?.country === state.rider.country && before?.city === state.rider.city; const wasSelected = state.selectedRequestId === requestId; const wasRelevantToRider = riderIdentityMatches(before?.selectedRiderId) || previous.riderOfferRequestIds?.has(requestId) || (wasSelected && sameMarketplace); if (!wasRelevantToRider) return; if (state.selectedRequestId === requestId) state.selectedRequestId = null; state.offers = state.offers.filter((offer) => offer.requestId !== requestId); state.chats = state.chats.filter((message) => message.requestId !== requestId); addRiderRideNotice( "Ride request unavailable", "That passenger request is no longer available. It has been removed from your marketplace.", requestId, `no-longer-available-${requestId}`, { createdBy: before.passengerId || null, createdByRole: before.passengerId ? "passenger" : null } ); }); state.chats.forEach((message) => { if (previous.chatIds.has(message.id)) return; const request = state.requests.find((item) => item.id === message.requestId); if (!incomingChatMessageForRole(message, "rider", request)) return; addRiderRideNotice( "New ride message", chatNoticeBodyForRole("rider", message), request.id, `chat-${message.id}` ); }); deliverLoadedAccountNotifications("rider", previous); saveState(); } } async function loadMarketplaceFromSupabase({ includeAccountData = false } = {}) { if (!hasSupabaseRuntime() || (!hasSignedIn("passenger") && !hasSignedIn("rider"))) return; const userIds = currentMarketplaceUserIds(); const passengerIds = uniqueMarketplaceIds([state.passenger?.id, state.passenger?.supabaseUserId, state.sessions?.passenger?.userId]); const riderIds = riderIdentityIds(); const shouldLoadAccountData = Boolean(includeAccountData); const preloadedRiderDayPreferencesResult = riderIds.length ? await selectScopedMarketplaceTable("rider_day_preferences", "updated_at", [marketplaceInFilter("rider_id", riderIds)]) : { data: [] }; rememberRiderDayPreferences((preloadedRiderDayPreferencesResult.data ?? []).map((preference) => mapRiderDayPreferenceFromDatabase(preference))); const [requestsResult, notificationsResult, financeAdjustmentsResult, paymentAccountsResult, businessAccountsResult, riderDayPreferencesResult, rideRatingsResult, rideSettlementsResult, rideTipsResult, routeChangesResult, riderOwnOffersResult] = await Promise.all([ selectMarketplaceRequests(), selectScopedMarketplaceTable("admin_notifications", "created_at", [marketplaceInFilter("recipient_id", userIds)]), shouldLoadAccountData ? selectScopedMarketplaceTable("finance_adjustments", "created_at", [marketplaceInFilter("subject_id", userIds)]) : emptyMarketplaceTableResult(), shouldLoadAccountData ? selectScopedMarketplaceTable("payment_accounts", "updated_at", [marketplaceInFilter("user_id", userIds)]) : emptyMarketplaceTableResult(), shouldLoadAccountData ? selectScopedMarketplaceTable("business_accounts", "updated_at", [marketplaceInFilter("owner_id", passengerIds)]) : emptyMarketplaceTableResult(), Promise.resolve(preloadedRiderDayPreferencesResult), shouldLoadAccountData ? selectAnyUserScopedMarketplaceTable("ride_ratings", "created_at", ["reviewer_id"]) : emptyMarketplaceTableResult(), shouldLoadAccountData ? selectAnyUserScopedMarketplaceTable("ride_payment_settlements", "created_at", ["passenger_id", "rider_id"]) : emptyMarketplaceTableResult(), shouldLoadAccountData ? selectAnyUserScopedMarketplaceTable("ride_tips", "created_at", ["passenger_id", "rider_id"]) : emptyMarketplaceTableResult(), selectRideRouteChangesForMarketplace(), riderIds.length ? selectScopedMarketplaceTable("ride_offers", "created_at", [marketplaceInFilter("rider_id", riderIds)]) : emptyMarketplaceTableResult() ]); const riderOwnOfferRows = riderOwnOffersResult.data ?? []; const riderOwnOfferIds = uniqueMarketplaceIds(riderOwnOfferRows.map((offer) => offer.id)); const riderMatchedRequestsResult = riderOwnOfferIds.length ? await selectScopedMarketplaceTable("ride_requests", "created_at", [marketplaceInFilter("selected_offer_id", riderOwnOfferIds)]) : emptyMarketplaceTableResult(); const combinedRequestsResult = mergeMarketplaceTableResults([requestsResult, riderMatchedRequestsResult]); const requestIds = uniqueMarketplaceIds((combinedRequestsResult.data ?? []).map((request) => request.id)); const [offersResult, chatsResult, riderCompletedMileageSegmentsResult, routeChangesByRequestResult] = await Promise.all([ selectScopedMarketplaceTable("ride_offers", "created_at", [marketplaceInFilter("ride_request_id", requestIds)]), selectScopedMarketplaceTable("ride_chats", "created_at", [marketplaceInFilter("ride_request_id", requestIds)]), shouldLoadAccountData && riderIds.length ? selectRiderCompletedMileageSegmentsForRequests(riderIds, requestIds) : emptyMarketplaceTableResult(), selectRideRouteChangesForRequests(requestIds) ]); const offers = mergeMarketplaceTableResults([offersResult, riderOwnOffersResult]).data.map(mapOfferFromDatabase); const offerMap = new Map(offers.map((offer) => [offer.id, offer])); const previousRequestMap = stateLookupIndexes().requestMap; const requests = (combinedRequestsResult.data ?? []).map((request) => { const mapped = mapRideRequestFromDatabase(request, new Map(), offerMap); return preserveRideRequestPickup(mapped, previousRequestMap.get(mapped.id)); }); const chats = (chatsResult.data ?? []).map(mapChatFromDatabase); const notifications = (notificationsResult.data ?? []).map(mapAdminNotificationFromDatabase); const financeAdjustments = (financeAdjustmentsResult.data ?? []).map((adjustment) => mapFinanceAdjustmentFromDatabase(adjustment)); const paymentAccounts = (paymentAccountsResult.data ?? []).map((account) => mapPaymentAccountFromDatabase(account)); const businessAccounts = (businessAccountsResult.data ?? []).map((account) => mapBusinessAccountFromDatabase(account)); const businessAccountIds = uniqueMarketplaceIds(businessAccounts.map((account) => account.id)); const businessSubscriptionsResult = shouldLoadAccountData && businessAccountIds.length ? await selectScopedMarketplaceTable("business_subscriptions", "updated_at", [ marketplaceInFilter("business_account_id", businessAccountIds) ]) : emptyMarketplaceTableResult(); const businessSubscriptions = (businessSubscriptionsResult.data ?? []).map(mapBusinessSubscriptionFromDatabase); const riderDayPreferences = (riderDayPreferencesResult.data ?? []).map((preference) => mapRiderDayPreferenceFromDatabase(preference)); const rideRatings = (rideRatingsResult.data ?? []).map((rating) => mapRideRatingFromDatabase(rating)); const rideSettlements = (rideSettlementsResult.data ?? []).map((settlement) => mapRideSettlementFromDatabase(settlement)); const rideTips = (rideTipsResult.data ?? []).map((tip) => mapRideTipFromDatabase(tip)); const routeChanges = mergeMarketplaceTableResults([routeChangesResult, routeChangesByRequestResult]).data.map(mapRideRouteChangeFromDatabase); const riderCompletedMileageSegments = (riderCompletedMileageSegmentsResult.data ?? []).map(mapRiderCompletedMileageSegmentFromDatabase); pruneStaleRiderMarketplaceRequests(new Set(requests.map((request) => request.id))); requests.forEach((request) => { state.requests = upsertById(state.requests, preserveRideRequestPickup(request, stateLookupIndexes().requestMap.get(request.id))); }); const refreshedRequestIdSet = new Set(requestIds); if (shouldLoadAccountData && riderIds.length) { state.riderCompletedMileageSegments = (state.riderCompletedMileageSegments ?? []) .filter((segment) => !(riderIds.includes(segment.riderId) && refreshedRequestIdSet.has(segment.requestId))); } state.offers = state.offers.filter((offer) => !refreshedRequestIdSet.has(offer.requestId)); state.chats = state.chats.filter((message) => !refreshedRequestIdSet.has(message.requestId)); offers.forEach((offer) => { state.offers = upsertById(state.offers, offer); }); chats.forEach((message) => { state.chats = upsertById(state.chats, message); }); routeChanges.forEach((change) => { state.routeChangeRequests = upsertById(state.routeChangeRequests ?? [], change); }); if (typeof mergeRouteChangeEventsFromChats === "function") { mergeRouteChangeEventsFromChats(state.chats); } notifications.forEach((notification) => { state.notifications = upsertNotificationByDeliveryKey(state.notifications, notification); }); financeAdjustments.forEach((adjustment) => { state.financeAdjustments = upsertById(state.financeAdjustments, adjustment); }); paymentAccounts.forEach((account) => { state.paymentAccounts = upsertById( state.paymentAccounts.filter((item) => !(item.role === account.role && item.userId === account.userId)), account ); }); businessAccounts.forEach((account) => { state.businessAccounts = upsertById(state.businessAccounts, account); }); businessSubscriptions.forEach((subscription) => { state.businessSubscriptions = upsertById(state.businessSubscriptions, subscription); }); rememberRiderDayPreferences(riderDayPreferences); rideRatings.forEach((rating) => { state.rideRatings = upsertById(state.rideRatings, rating); }); rideSettlements.forEach((settlement) => { state.rideSettlements = upsertById(state.rideSettlements, settlement); }); rideTips.forEach((tip) => { state.rideTips = upsertById(state.rideTips, tip); }); riderCompletedMileageSegments.forEach((segment) => { state.riderCompletedMileageSegments = upsertById(state.riderCompletedMileageSegments ?? [], segment); }); if (shouldLoadAccountData && riderIds.length && typeof loadMyRiderRatingSummaryFromSupabase === "function") { await loadMyRiderRatingSummaryFromSupabase(); } await loadPassengerApproachFromSupabase(); await loadActiveRideContactsFromSupabase(); saveState(); } async function refreshMarketplace({ silent = false, reason = "manual" } = {}) { if (!canRefreshMarketplace() || marketRefreshInFlight) return; const previous = marketplaceStateSnapshot(); marketRefreshInFlight = true; els.refreshMarket.disabled = true; if (!silent) els.selectedSummary.textContent = "Refreshing shared marketplace from Supabase..."; try { await expireRiderLiveGpsIfNeeded(); await loadMarketplaceFromSupabase({ includeAccountData: !silent }); notifyMarketplaceChanges(previous); reconcileRiderPendingRouteChangeDecision({ render: false }); lastMarketRefreshAt = new Date(); if (!silent) { els.selectedSummary.textContent = `Marketplace refreshed ${lastMarketRefreshAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.`; } renderAll(); ensureAccountNoticeAutoRefreshes(); notifyRiderAboutNearbyRequests(); } catch (error) { if (!silent) els.selectedSummary.textContent = `Market refresh failed: ${error.message}`; } finally { marketRefreshInFlight = false; els.refreshMarket.disabled = false; ensurePassengerApproachAutoRefresh(); ensureRiderMarketplaceAutoRefresh(); ensureAccountNoticeAutoRefreshes(); ensureMarketplaceRealtimeSubscription(); if (marketplaceRealtimeRefreshPendingReason && !document.hidden && canRefreshMarketplace()) { const pendingReason = marketplaceRealtimeRefreshPendingReason; marketplaceRealtimeRefreshPendingReason = ""; scheduleMarketplaceRealtimeRefresh(pendingReason); } } } function clearSelectedRequestOutsideLocation(country, city) { const request = selectedRequest(); if (request && (request.country !== country || request.city !== city)) { state.selectedRequestId = null; } } // Lazy draggable background map for the passenger request and rider initialize workspaces. const workspaceMapTileSize = 256; const workspaceMapDefaultZoom = 12; const workspaceMapExactZoom = 15; const workspaceMapFallbackCenter = { latitude: 39.0458, longitude: -76.6413 }; const workspaceMapMaxRenderedTiles = 80; const workspaceMapDeviceGpsMinimumRenderMs = 15 * 1000; const workspaceMapTileUsageStorageKey = "waka-mapbox-tile-usage-v1"; const workspaceMapTileUsageDefaultSoftLimit = 150000; const workspaceMapTileUsageDefaultHardLimit = 190000; let workspaceMapInitialized = false; let workspaceMapAnimationFrame = 0; let workspaceMapResizeTimer = 0; let workspaceMapDragState = null; let workspaceMapDeviceGps = null; let workspaceMapDeviceGpsWatchId = null; let workspaceMapDeviceGpsContextKey = ""; let workspaceMapLastDeviceGpsRenderAt = 0; let workspaceMapTileUsageAlertedKey = ""; const workspaceMapLoadedTileKeys = new Set(); let workspaceMapState = { center: null, zoom: workspaceMapDefaultZoom, contextKey: "", modelKey: "", userPanned: false }; function workspaceMapElements() { return { root: document.querySelector("#workspaceBackgroundMap"), viewport: document.querySelector("#workspaceBackgroundMapViewport"), tiles: document.querySelector("#workspaceMapTiles"), routeLayer: document.querySelector("#workspaceMapRouteLayer"), markers: document.querySelector("#workspaceMapMarkers") }; } function workspaceMapToken() { return String(appConfig?.mapboxAccessToken || "").trim(); } function workspaceMapFlagEnabled(value, fallback = false) { if (typeof configFlagEnabled === "function") return configFlagEnabled(value ?? fallback); if (value == null) return Boolean(fallback); return value === true || value === "true" || value === 1 || value === "1"; } function workspaceMapTileMapsEnabled() { return workspaceMapFlagEnabled(appConfig?.mapboxTileMapsEnabled, true); } function workspaceMapTileUsageLimit(name, fallback) { const value = Number(appConfig?.[name] ?? fallback); return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : fallback; } function workspaceMapTileUsageMonthKey(now = new Date()) { return `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`; } function workspaceMapTileUsageRead() { const month = workspaceMapTileUsageMonthKey(); try { const stored = JSON.parse(localStorage.getItem(workspaceMapTileUsageStorageKey) || "{}"); if (stored?.month === month) { return { month, count: Math.max(0, Number(stored.count) || 0) }; } } catch {} return { month, count: 0 }; } function workspaceMapTileUsageWrite(record) { try { localStorage.setItem(workspaceMapTileUsageStorageKey, JSON.stringify({ month: record.month, count: Math.max(0, Number(record.count) || 0) })); } catch (error) { if (typeof logClientWarning === "function") logClientWarning("Mapbox tile usage estimate could not be saved.", error); } } function workspaceMapTileUsageSoftLimit() { return workspaceMapTileUsageLimit("mapboxTileRequestMonthlySoftLimit", workspaceMapTileUsageDefaultSoftLimit); } function workspaceMapTileUsageHardLimit() { return workspaceMapTileUsageLimit("mapboxTileRequestMonthlyHardLimit", workspaceMapTileUsageDefaultHardLimit); } function workspaceMapTileUsageHardLimitReached() { const hardLimit = workspaceMapTileUsageHardLimit(); return hardLimit > 0 && workspaceMapTileUsageRead().count >= hardLimit; } function workspaceMapTileUsageCanRequest(count = 1) { const hardLimit = workspaceMapTileUsageHardLimit(); if (hardLimit <= 0) return true; return workspaceMapTileUsageRead().count + count <= hardLimit; } function workspaceMapReportTileUsageLimit(record, limitType) { const alertKey = `${record.month}:${limitType}`; if (workspaceMapTileUsageAlertedKey === alertKey) return; workspaceMapTileUsageAlertedKey = alertKey; const softLimit = workspaceMapTileUsageSoftLimit(); const hardLimit = workspaceMapTileUsageHardLimit(); const detail = { month: record.month, estimatedTileRequests: record.count, softLimit, hardLimit, limitType }; if (typeof logClientWarning === "function") { logClientWarning(`Mapbox tile request estimate reached the configured ${limitType} limit.`, detail); } try { document.dispatchEvent(new CustomEvent("waka:mapbox-tile-usage-alert", { detail })); } catch {} } function workspaceMapRecordTileRequest(count = 1) { const record = workspaceMapTileUsageRead(); record.count += Math.max(0, Number(count) || 0); workspaceMapTileUsageWrite(record); const softLimit = workspaceMapTileUsageSoftLimit(); const hardLimit = workspaceMapTileUsageHardLimit(); if (hardLimit > 0 && record.count >= hardLimit) { workspaceMapReportTileUsageLimit(record, "hard"); } else if (softLimit > 0 && record.count >= softLimit) { workspaceMapReportTileUsageLimit(record, "soft"); } return record; } function workspaceMapTileMapsAvailable() { return workspaceMapTileMapsEnabled() && workspaceMapToken().length > 0 && !workspaceMapTileUsageHardLimitReached(); } function workspaceMapStylePath() { const style = String(appConfig?.mapboxStyleId || "mapbox/streets-v12").trim(); const parts = style.split("/").filter(Boolean); const owner = parts.length >= 2 ? parts[0] : "mapbox"; const styleId = parts.length >= 2 ? parts.slice(1).join("/") : "streets-v12"; return `${encodeURIComponent(owner)}/${encodeURIComponent(styleId)}`; } function workspaceMapPassengerHasMatchedRide() { if (typeof requestBelongsToPassenger !== "function") return false; const activeStatuses = new Set(["matched", "arrived", "in_progress", "completed"]); return (state.requests ?? []).some((request) => requestBelongsToPassenger(request) && activeStatuses.has(request?.status)); } function workspaceMapContext() { const role = typeof activeRole === "function" ? activeRole() : state.activeTab; if (role === "passenger" && typeof passengerWorkspacePage === "function" && passengerWorkspacePage() === "request" && state.sessions?.passenger && state.passenger && els.rideRequestForm && !els.rideRequestForm.hidden) { return { role: "passenger", page: "request" }; } if (role === "rider" && typeof riderWorkspacePage === "function" && riderWorkspacePage() === "initialize" && state.sessions?.rider && state.rider) { return { role: "rider", page: "initialize" }; } return null; } function workspaceMapPoint(point, label, type) { const gps = normalizeGpsPoint(point); if (!gps) return null; return { latitude: gps.latitude, longitude: gps.longitude, label, type }; } function workspaceMapDecodeGooglePolyline(value) { const encoded = String(value || ""); const points = []; let index = 0; let latitude = 0; let longitude = 0; const decodeValue = () => { let result = 0; let shift = 0; let byte = 0; do { if (index >= encoded.length) return null; byte = encoded.charCodeAt(index) - 63; index += 1; result |= (byte & 0x1f) << shift; shift += 5; } while (byte >= 0x20); return (result & 1) ? ~(result >> 1) : (result >> 1); }; while (index < encoded.length) { const latitudeDelta = decodeValue(); const longitudeDelta = decodeValue(); if (latitudeDelta == null || longitudeDelta == null) break; latitude += latitudeDelta; longitude += longitudeDelta; const point = workspaceMapPoint({ latitude: latitude / 1e5, longitude: longitude / 1e5 }, "", "route"); if (point) points.push(point); } return points; } function workspaceMapRoutePathPointsFromPolyline(value) { return workspaceMapDecodeGooglePolyline(value); } function workspaceMapDevicePoint(label = "Y", type = "device") { return workspaceMapPoint(workspaceMapDeviceGps, label, type); } function workspaceMapSyncDeviceGpsWatch(context) { const contextKey = context ? `${context.role}:${context.page}` : ""; const shouldWatch = Boolean(context && navigator.geolocation); if (!shouldWatch) { if (workspaceMapDeviceGpsWatchId != null && navigator.geolocation?.clearWatch) { navigator.geolocation.clearWatch(workspaceMapDeviceGpsWatchId); } workspaceMapDeviceGpsWatchId = null; workspaceMapDeviceGpsContextKey = ""; workspaceMapLastDeviceGpsRenderAt = 0; return; } if (workspaceMapDeviceGpsWatchId != null && workspaceMapDeviceGpsContextKey === contextKey) return; if (workspaceMapDeviceGpsWatchId != null && navigator.geolocation?.clearWatch) { navigator.geolocation.clearWatch(workspaceMapDeviceGpsWatchId); } workspaceMapDeviceGpsContextKey = contextKey; workspaceMapLastDeviceGpsRenderAt = 0; try { workspaceMapDeviceGpsWatchId = navigator.geolocation.watchPosition( (position) => { const point = typeof gpsPointFromPosition === "function" ? gpsPointFromPosition(position) : null; if (!point) return; workspaceMapDeviceGps = point; const now = Date.now(); if (workspaceMapLastDeviceGpsRenderAt && now - workspaceMapLastDeviceGpsRenderAt < workspaceMapDeviceGpsMinimumRenderMs) { return; } workspaceMapLastDeviceGpsRenderAt = now; workspaceMapState.userPanned = false; scheduleWorkspaceBackgroundMapRender(); }, () => {}, { enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 } ); } catch { workspaceMapDeviceGpsWatchId = null; workspaceMapDeviceGpsContextKey = ""; } } function workspaceMapTownCenter(country, city, name) { if (typeof launchGpsTownCenterForName === "function") { return launchGpsTownCenterForName(country, city, name); } return null; } function workspaceMapFirstTownCenter(country, city) { const first = typeof launchGpsTownCenters === "object" ? launchGpsTownCenters?.[country]?.[city]?.[0] : null; return workspaceMapPoint(first || workspaceMapFallbackCenter, "", "center"); } function workspaceMapPassengerModel() { const country = typeof selectedPassengerCountry === "function" ? selectedPassengerCountry() : state.passenger?.country; const city = state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(country); const pickupText = els.pickupDescription?.value ?? ""; const pickupGps = typeof passengerPickupGpsForFormChoice === "function" ? passengerPickupGpsForFormChoice() : null; const devicePoint = workspaceMapDevicePoint("Y", "device"); const pickupOrigin = typeof routeOriginForEstimate === "function" ? routeOriginForEstimate(country, city, els.pickupArea?.value, pickupText, pickupGps) : null; const pickup = workspaceMapPoint(pickupOrigin, "P", "pickup") || workspaceMapPoint(pickupGps, "P", "pickup") || workspaceMapPoint(workspaceMapTownCenter(country, city, els.pickupArea?.value), "P", "pickup"); const destinationText = String(els.destination?.value ?? "").trim(); const destinationPlace = typeof destinationPlaceForRoute === "function" ? destinationPlaceForRoute(destinationText) : null; const destination = destinationText ? workspaceMapPoint(destinationPlace, "D", "destination") || workspaceMapPoint(workspaceMapTownCenter(country, city, els.destinationArea?.value), "D", "destination") : null; const stopPoints = typeof normalizeRideStops === "function" && typeof stopRoutePoint === "function" && typeof rideStopsFormValue === "function" ? normalizeRideStops(rideStopsFormValue()).map((stop) => workspaceMapPoint(stopRoutePoint(stop), "", "stop")).filter(Boolean) : []; const markers = [devicePoint, pickup, ...stopPoints, destination].filter(Boolean); const routePoints = [pickup, ...stopPoints, destination].filter(Boolean); const fallback = workspaceMapFirstTownCenter(country, city); const exactCenter = devicePoint || pickup; const guidanceKey = typeof routeGuidanceInputKey === "function" ? routeGuidanceInputKey( country, city, els.pickupArea?.value, els.destinationArea?.value, pickupText, destinationText, typeof rideStopsFormValue === "function" ? rideStopsFormValue() : [], pickupGps, destinationPlace ) : ""; const guidance = guidanceKey && typeof cachedConfirmedFareGuidanceForKey === "function" ? cachedConfirmedFareGuidanceForKey(guidanceKey) : null; const routePathPoints = workspaceMapRoutePathPointsFromPolyline(guidance?.routePolyline); return { contextKey: "passenger:request", modelKey: [devicePoint, ...routePoints, ...routePathPoints].filter(Boolean).map((point) => `${point.type}:${point.latitude.toFixed(5)},${point.longitude.toFixed(5)}`).join("|") || `${country}:${city}:${els.pickupArea?.value || ""}:${els.destinationArea?.value || ""}`, center: exactCenter || workspaceMapCenterForPoints(routePathPoints.length ? routePathPoints : routePoints.length ? routePoints : markers.length ? markers : [fallback]), markers: markers.length ? markers : [fallback], routePoints, routePathPoints, zoom: exactCenter ? workspaceMapExactZoom : 13 }; } function rideRequestRoutePreviewModel() { const country = typeof selectedPassengerCountry === "function" ? selectedPassengerCountry() : state.passenger?.country; const city = state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(country); const pickupText = els.pickupDescription?.value ?? ""; const pickupGps = typeof passengerPickupGpsForFormChoice === "function" ? passengerPickupGpsForFormChoice() : null; const pickupOrigin = typeof routeOriginForEstimate === "function" ? routeOriginForEstimate(country, city, els.pickupArea?.value, pickupText, pickupGps) : null; const pickup = workspaceMapPoint(pickupOrigin, "P", "pickup") || workspaceMapPoint(pickupGps, "P", "pickup") || workspaceMapPoint(workspaceMapTownCenter(country, city, els.pickupArea?.value), "P", "pickup"); const destinationText = String(els.destination?.value ?? "").trim(); const destinationPlace = destinationText && typeof destinationPlaceForRoute === "function" ? destinationPlaceForRoute(destinationText) : null; const destination = destinationText ? workspaceMapPoint(destinationPlace, "D", "destination") || workspaceMapPoint(workspaceMapTownCenter(country, city, els.destinationArea?.value), "D", "destination") : null; const stopPoints = typeof normalizeRideStops === "function" && typeof stopRoutePoint === "function" && typeof rideStopsFormValue === "function" ? normalizeRideStops(rideStopsFormValue()).map((stop) => workspaceMapPoint(stopRoutePoint(stop), "", "stop")).filter(Boolean) : []; const fallback = workspaceMapFirstTownCenter(country, city); const markers = [pickup, ...stopPoints, destination].filter(Boolean); const routePoints = [pickup, ...stopPoints, destination].filter(Boolean); const guidanceKey = typeof routeGuidanceInputKey === "function" ? routeGuidanceInputKey( country, city, els.pickupArea?.value, els.destinationArea?.value, pickupText, destinationText, typeof rideStopsFormValue === "function" ? rideStopsFormValue() : [], pickupGps, destinationPlace ) : ""; const guidance = guidanceKey && typeof cachedConfirmedFareGuidanceForKey === "function" ? cachedConfirmedFareGuidanceForKey(guidanceKey) : null; const routePathPoints = workspaceMapRoutePathPointsFromPolyline(guidance?.routePolyline); const points = routePathPoints.length ? routePathPoints : routePoints.length ? routePoints : markers.length ? markers : [fallback]; return { center: workspaceMapCenterForPoints(points), markers: markers.length ? markers : [fallback], routePoints, routePathPoints, zoom: routePoints.length >= 2 ? 13 : 12 }; } function workspaceMapRiderModel() { const rider = typeof currentRiderRecord === "function" ? currentRiderRecord() : state.rider; const country = rider?.country ?? (typeof selectedRiderCountry === "function" ? selectedRiderCountry() : state.rider?.country); const city = rider?.city ?? (typeof selectedRiderCity === "function" ? selectedRiderCity() : state.rider?.city); const devicePoint = workspaceMapPoint( (typeof riderCurrentGps === "function" ? riderCurrentGps(rider) : null) || workspaceMapDeviceGps, "R", "rider" ); const riderPoint = devicePoint || workspaceMapPoint(workspaceMapTownCenter(country, city, rider?.area ?? els.riderActiveArea?.value), "R", "rider") || workspaceMapFirstTownCenter(country, city); const requestMarkers = (state.requests || []) .filter((request) => request?.status === "open" && request?.country === country && request?.city === city) .map((request) => workspaceMapPoint( typeof requestPickupGps === "function" ? requestPickupGps(request) : null, "$", "request" )) .filter(Boolean) .slice(0, 10); const markers = [riderPoint, ...requestMarkers].filter(Boolean); return { contextKey: "rider:initialize", modelKey: markers.map((point) => `${point.type}:${point.latitude.toFixed(5)},${point.longitude.toFixed(5)}`).join("|") || `${country}:${city}:${rider?.area || ""}`, center: devicePoint || riderPoint, markers, routePoints: [], zoom: devicePoint ? workspaceMapExactZoom : Number(appConfig?.riderInitializeMapZoom) || 13 }; } function workspaceMapModel(context = workspaceMapContext()) { if (!context) return null; return context.role === "rider" ? workspaceMapRiderModel() : workspaceMapPassengerModel(); } function workspaceMapCenterForPoints(points) { const valid = points.filter(Boolean); if (!valid.length) return workspaceMapFallbackCenter; const sum = valid.reduce((total, point) => ({ latitude: total.latitude + point.latitude, longitude: total.longitude + point.longitude }), { latitude: 0, longitude: 0 }); return { latitude: sum.latitude / valid.length, longitude: sum.longitude / valid.length }; } function workspaceMapClampLatitude(latitude) { return Math.max(-85.05112878, Math.min(85.05112878, Number(latitude) || 0)); } function workspaceMapWorldPoint(point, zoom) { const latitude = workspaceMapClampLatitude(point?.latitude); const longitude = Number(point?.longitude) || 0; const sin = Math.sin((latitude * Math.PI) / 180); const scale = workspaceMapTileSize * (2 ** zoom); return { x: ((longitude + 180) / 360) * scale, y: (0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI)) * scale }; } function workspaceMapLatLngFromWorld(world, zoom) { const scale = workspaceMapTileSize * (2 ** zoom); const longitude = (world.x / scale) * 360 - 180; const n = Math.PI - (2 * Math.PI * world.y) / scale; const latitude = (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))); return { latitude: workspaceMapClampLatitude(latitude), longitude: ((longitude + 540) % 360) - 180 }; } function workspaceMapZoomForPoints(points, viewport, fallbackZoom) { const valid = points.filter(Boolean); if (valid.length < 2 || !viewport?.clientWidth || !viewport?.clientHeight) return fallbackZoom; for (let zoom = 15; zoom >= 8; zoom -= 1) { const pixels = valid.map((point) => workspaceMapWorldPoint(point, zoom)); const xs = pixels.map((point) => point.x); const ys = pixels.map((point) => point.y); const spanX = Math.max(...xs) - Math.min(...xs); const spanY = Math.max(...ys) - Math.min(...ys); if (spanX <= viewport.clientWidth * 0.68 && spanY <= viewport.clientHeight * 0.55) return zoom; } return 8; } function workspaceMapTileUrl(zoom, x, y) { const token = workspaceMapToken(); if (!token) return ""; return `https://api.mapbox.com/styles/v1/${workspaceMapStylePath()}/tiles/${workspaceMapTileSize}/${zoom}/${x}/${y}@2x?access_token=${encodeURIComponent(token)}`; } function workspaceMapRenderTiles(elements, centerWorld, zoom, options = {}) { const { root, viewport, tiles } = elements; const token = options.useMapboxTiles === false || !workspaceMapTileMapsAvailable() ? "" : workspaceMapToken(); root.classList.toggle("workspace-map-fallback", !token); if (!token) { if (tiles.childElementCount) tiles.replaceChildren(); return; } const width = viewport.clientWidth || window.innerWidth; const height = viewport.clientHeight || window.innerHeight; const leftWorld = centerWorld.x - width / 2; const topWorld = centerWorld.y - height / 2; const minX = Math.floor(leftWorld / workspaceMapTileSize) - 1; const maxX = Math.floor((leftWorld + width) / workspaceMapTileSize) + 1; const minY = Math.floor(topWorld / workspaceMapTileSize) - 1; const maxY = Math.floor((topWorld + height) / workspaceMapTileSize) + 1; const maxTile = 2 ** zoom; const existingTiles = new Map(Array.from(tiles.children) .filter((node) => node instanceof HTMLImageElement && node.dataset.tileKey) .map((node) => [node.dataset.tileKey, node])); const fragment = document.createDocumentFragment(); let rendered = 0; for (let tileY = minY; tileY <= maxY; tileY += 1) { if (tileY < 0 || tileY >= maxTile) continue; for (let tileX = minX; tileX <= maxX; tileX += 1) { if (rendered >= workspaceMapMaxRenderedTiles) break; const wrappedX = ((tileX % maxTile) + maxTile) % maxTile; const tileKey = `${zoom}:${wrappedX}:${tileY}`; let image = existingTiles.get(tileKey); if (!image) { if (!workspaceMapTileUsageCanRequest(1)) { workspaceMapReportTileUsageLimit(workspaceMapTileUsageRead(), "hard"); continue; } image = document.createElement("img"); image.className = "workspace-map-tile"; image.dataset.tileKey = tileKey; image.alt = ""; image.decoding = "async"; image.loading = "lazy"; image.draggable = false; image.src = workspaceMapTileUrl(zoom, wrappedX, tileY); workspaceMapRecordTileRequest(1); if (workspaceMapLoadedTileKeys.has(tileKey)) image.classList.add("loaded"); image.addEventListener("load", () => { workspaceMapLoadedTileKeys.add(tileKey); image.classList.add("loaded"); }, { once: true }); } image.style.left = `${Math.round(tileX * workspaceMapTileSize - leftWorld)}px`; image.style.top = `${Math.round(tileY * workspaceMapTileSize - topWorld)}px`; fragment.append(image); rendered += 1; } } tiles.replaceChildren(fragment); } function workspaceMapScreenPoint(point, centerWorld, zoom, viewport) { const world = workspaceMapWorldPoint(point, zoom); return { x: world.x - centerWorld.x + viewport.clientWidth / 2, y: world.y - centerWorld.y + viewport.clientHeight / 2 }; } function workspaceMapRenderOverlay(elements, model, centerWorld, zoom) { const { viewport, routeLayer, markers } = elements; const width = viewport.clientWidth || window.innerWidth; const height = viewport.clientHeight || window.innerHeight; routeLayer.setAttribute("viewBox", `0 0 ${Math.max(1, width)} ${Math.max(1, height)}`); routeLayer.replaceChildren(); markers.replaceChildren(); const routeLinePoints = workspaceMapRouteDisplayPoints(model); if (routeLinePoints.length >= 2) { const points = routeLinePoints .map((point) => workspaceMapScreenPoint(point, centerWorld, zoom, viewport)) .map((point) => `${point.x.toFixed(1)},${point.y.toFixed(1)}`) .join(" "); const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline"); polyline.setAttribute("class", "workspace-map-route-line"); polyline.setAttribute("points", points); routeLayer.append(polyline); } for (const marker of model.markers || []) { const point = workspaceMapScreenPoint(marker, centerWorld, zoom, viewport); const node = document.createElement("div"); node.className = `workspace-map-marker ${marker.type || ""}`.trim(); node.style.left = `${point.x}px`; node.style.top = `${point.y}px`; node.textContent = marker.label || ""; markers.append(node); } } function workspaceMapRouteDisplayPoints(model) { return (model?.routePathPoints?.length ? model.routePathPoints : model?.routePoints || []).filter(Boolean); } function renderInlineWorkspaceMap(container, model, options = {}) { if (!container || !model) return; let tiles = container.querySelector(".workspace-map-tiles"); let routeLayer = container.querySelector(".workspace-map-route-layer"); let markers = container.querySelector(".workspace-map-markers"); if (!tiles) { tiles = document.createElement("div"); tiles.className = "workspace-map-tiles"; container.append(tiles); } if (!routeLayer) { routeLayer = document.createElementNS("http://www.w3.org/2000/svg", "svg"); routeLayer.setAttribute("class", "workspace-map-route-layer"); routeLayer.setAttribute("preserveAspectRatio", "none"); routeLayer.setAttribute("aria-hidden", "true"); container.append(routeLayer); } if (!markers) { markers = document.createElement("div"); markers.className = "workspace-map-markers"; container.append(markers); } const routeLinePoints = workspaceMapRouteDisplayPoints(model); const points = (routeLinePoints.length ? routeLinePoints : model.markers || []).filter(Boolean); const viewport = container; const zoom = options.zoom ?? workspaceMapZoomForPoints(points, viewport, model.zoom || workspaceMapExactZoom); const center = model.center || workspaceMapCenterForPoints(points); const centerWorld = workspaceMapWorldPoint(center, zoom); workspaceMapRenderTiles( { root: container, viewport, tiles, routeLayer, markers }, centerWorld, zoom, { useMapboxTiles: options.useMapboxTiles } ); workspaceMapRenderOverlay({ viewport, routeLayer, markers }, model, centerWorld, zoom); } function rideRequestRoutePreviewReadyForTiles(model) { const hasPickup = Boolean( String(els.pickupDescription?.value ?? "").trim() || els.pickupUseCurrentLocation?.checked || (typeof passengerPickupGpsForFormChoice === "function" && passengerPickupGpsForFormChoice()) ); const hasDestination = Boolean(String(els.destination?.value ?? "").trim()); return Boolean(hasPickup && hasDestination && model?.routePoints?.length >= 2); } function rideRequestRoutePreviewUseMapboxTiles(model) { return Boolean( workspaceMapFlagEnabled(appConfig?.passengerRequestTileMapEnabled, false) && rideRequestRoutePreviewReadyForTiles(model) && workspaceMapTileMapsAvailable() ); } function renderRideRequestRoutePreview() { const container = els.rideRequestRoutePreview; if (!container) return; container.hidden = false; const visible = Boolean( els.rideRequestForm && !els.rideRequestForm.hidden && typeof activeRole === "function" && activeRole() === "passenger" && typeof passengerWorkspacePage === "function" && passengerWorkspacePage() === "request" ); if (!visible) return; const model = rideRequestRoutePreviewModel(); window.requestAnimationFrame(() => renderInlineWorkspaceMap(container, model, { useMapboxTiles: rideRequestRoutePreviewUseMapboxTiles(model) })); } function renderWorkspaceBackgroundMap() { const elements = workspaceMapElements(); if (!elements.root || !elements.viewport || !elements.tiles || !elements.routeLayer || !elements.markers) return; initializeWorkspaceBackgroundMap(); const context = workspaceMapContext(); workspaceMapSyncDeviceGpsWatch(context); const model = workspaceMapModel(context); if (!context || !model) { elements.root.hidden = true; document.body.classList.remove("workspace-map-active"); workspaceMapState.contextKey = ""; workspaceMapState.userPanned = false; return; } elements.root.hidden = false; document.body.classList.add("workspace-map-active"); const contextChanged = model.contextKey !== workspaceMapState.contextKey; const modelChanged = model.modelKey !== workspaceMapState.modelKey; const viewport = elements.viewport; const zoom = workspaceMapZoomForPoints(workspaceMapRouteDisplayPoints(model), viewport, model.zoom || workspaceMapDefaultZoom); if (contextChanged || modelChanged || !workspaceMapState.center || !workspaceMapState.userPanned) { workspaceMapState.center = model.center; workspaceMapState.zoom = zoom; workspaceMapState.contextKey = model.contextKey; workspaceMapState.modelKey = model.modelKey; workspaceMapState.userPanned = false; } const centerWorld = workspaceMapWorldPoint(workspaceMapState.center, workspaceMapState.zoom); const useMapboxTiles = context.role === "passenger" && context.page === "request" ? rideRequestRoutePreviewUseMapboxTiles(model) : context.role !== "passenger"; workspaceMapRenderTiles(elements, centerWorld, workspaceMapState.zoom, { useMapboxTiles }); workspaceMapRenderOverlay(elements, model, centerWorld, workspaceMapState.zoom); } function scheduleWorkspaceBackgroundMapRender() { if (workspaceMapAnimationFrame) return; workspaceMapAnimationFrame = window.requestAnimationFrame(() => { workspaceMapAnimationFrame = 0; renderWorkspaceBackgroundMap(); }); } function workspaceMapPointerDown(event) { const elements = workspaceMapElements(); if (!elements.viewport || !workspaceMapState.center) return; if (event.pointerType === "mouse" && event.button !== 0) return; event.preventDefault(); elements.viewport.classList.add("dragging"); elements.viewport.setPointerCapture?.(event.pointerId); workspaceMapDragState = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, startWorld: workspaceMapWorldPoint(workspaceMapState.center, workspaceMapState.zoom) }; } function workspaceMapPointerMove(event) { if (!workspaceMapDragState || workspaceMapDragState.pointerId !== event.pointerId) return; event.preventDefault(); const dx = event.clientX - workspaceMapDragState.startX; const dy = event.clientY - workspaceMapDragState.startY; workspaceMapState.center = workspaceMapLatLngFromWorld({ x: workspaceMapDragState.startWorld.x - dx, y: workspaceMapDragState.startWorld.y - dy }, workspaceMapState.zoom); workspaceMapState.userPanned = true; scheduleWorkspaceBackgroundMapRender(); } function workspaceMapPointerEnd(event) { if (!workspaceMapDragState || workspaceMapDragState.pointerId !== event.pointerId) return; const elements = workspaceMapElements(); elements.viewport?.classList.remove("dragging"); elements.viewport?.releasePointerCapture?.(event.pointerId); workspaceMapDragState = null; } function workspaceMapWheel(event) { const context = workspaceMapContext(); if (!context) return; event.preventDefault(); const delta = event.deltaY > 0 ? -1 : 1; workspaceMapState.zoom = Math.max(8, Math.min(16, (workspaceMapState.zoom || workspaceMapDefaultZoom) + delta)); workspaceMapState.userPanned = true; scheduleWorkspaceBackgroundMapRender(); } function initializeWorkspaceBackgroundMap() { if (workspaceMapInitialized) return; const elements = workspaceMapElements(); if (!elements.viewport) return; workspaceMapInitialized = true; elements.viewport.addEventListener("pointerdown", workspaceMapPointerDown); elements.viewport.addEventListener("pointermove", workspaceMapPointerMove); elements.viewport.addEventListener("pointerup", workspaceMapPointerEnd); elements.viewport.addEventListener("pointercancel", workspaceMapPointerEnd); elements.viewport.addEventListener("wheel", workspaceMapWheel, { passive: false }); const schedulePassengerMapRenders = () => { scheduleWorkspaceBackgroundMapRender(); if (typeof renderRideRequestRoutePreview === "function") renderRideRequestRoutePreview(); }; ["passengerCountry", "passengerCity", "pickupArea", "destinationArea", "pickupDescription", "destination", "rideStops", "pickupUseCurrentLocation", "rideTiming", "vehiclePreference"].forEach((id) => { document.querySelector(`#${id}`)?.addEventListener("input", schedulePassengerMapRenders); document.querySelector(`#${id}`)?.addEventListener("change", () => { workspaceMapState.userPanned = false; schedulePassengerMapRenders(); }); }); window.addEventListener("resize", () => { window.clearTimeout(workspaceMapResizeTimer); workspaceMapResizeTimer = window.setTimeout(() => { workspaceMapState.userPanned = false; renderWorkspaceBackgroundMap(); }, 120); }); } // Payment preferences, subscriptions, business billing, settlements, tips, ratings, and tax access helpers. const subscriptionFee = riderMonthlySubscriptionFee; const trialDays = 30; const pendingPaymentSetupStorageKey = "waka-pending-payment-setup-v1"; const pendingPaymentSetupMaxAgeMs = 24 * 60 * 60 * 1000; let pendingPaymentSetupPollTimer = null; const riderSubscriptionPlans = { monthly: { label: "Monthly Waka Rider Access", amount: riderMonthlySubscriptionFee, days: riderMonthlyAccessDays, description: `$${riderMonthlySubscriptionFee} upfront for ${riderMonthlyAccessDays} days, with $0 Waka ride commission` }, weekly: { label: "Weekly Waka Rider Access", amount: riderWeeklySubscriptionFee, days: riderWeeklyAccessDays, description: `$${riderWeeklySubscriptionFee} upfront for ${riderWeeklyAccessDays} days, with $0 Waka ride commission` } }; let subscriptionPaymentRpcUnavailable = { submit: false, verify: false, decline: false }; let lastSubscriptionPaymentSource = "not used"; let paymentAccountRpcUnavailable = false; let lastPaymentAccountSource = "not used"; let businessAccountRpcUnavailable = false; let lastBusinessAccountSource = "not used"; let riderDayRegionsRpcUnavailable = false; let lastRiderDayRegionsSource = "not used"; function agreedFareBaseForRequest(request) { return Number(request?.agreedFare ?? offersForRequest(request?.id).find((offer) => offer.id === request?.selectedOfferId)?.fare ?? request?.fareOffer ?? 0); } function acceptedRouteChangeFareForRequest(request) { return Math.max(0, Number(request?.acceptedRouteChangeFare ?? request?.accepted_route_change_fare ?? 0) || 0); } function agreedFareForRequest(request) { return agreedFareBaseForRequest(request) + acceptedRouteChangeFareForRequest(request); } function passengerCancellationFeeEstimate(request, atTime = Date.now()) { if (!request || !["matched", "arrived"].includes(request.status) || !selectedRiderIdForRequest(request)) { return { amount: 0, currency: moneyCurrencyForCountry(request?.country), elapsedMinutes: 0, status: "not_applicable" }; } const matchedAt = request.matchedAt ? new Date(request.matchedAt).getTime() : null; const elapsedMinutes = matchedAt && Number.isFinite(matchedAt) ? Math.max(0, Math.ceil((atTime - matchedAt) / 60000)) : 0; if (request.status === "matched" && elapsedMinutes < passengerCancellationFeeConfig.graceMinutes) { return { amount: 0, currency: moneyCurrencyForCountry(request.country), elapsedMinutes, status: "grace_period" }; } const fare = Math.max(0, agreedFareForRequest(request)); const base = request.status === "arrived" ? passengerCancellationFeeConfig.arrivedBaseUsd : passengerCancellationFeeConfig.matchedBaseUsd; const cap = Math.max(base, Math.ceil(fare * passengerCancellationFeeConfig.capFareRatio)); const amount = Math.min(cap, base + elapsedMinutes * passengerCancellationFeeConfig.perMinuteUsd); return { amount, currency: moneyCurrencyForCountry(request.country), elapsedMinutes, status: amount > 0 ? "pending_charge" : "not_applicable" }; } function inProgressCancellationCompensationEstimate(request, atTime = Date.now()) { if (!request || request.status !== "in_progress" || !selectedRiderIdForRequest(request)) { return { amount: 0, currency: moneyCurrencyForCountry(request?.country), elapsedMinutes: 0, status: "not_applicable", fareRatio: 0 }; } const startedAt = request.startedAt ? new Date(request.startedAt).getTime() : null; const elapsedMinutes = startedAt && Number.isFinite(startedAt) ? Math.max(1, Math.ceil((atTime - startedAt) / 60000)) : 1; const fare = Math.max(0, agreedFareForRequest(request)); if (fare <= 0) { return { amount: 0, currency: moneyCurrencyForCountry(request.country), elapsedMinutes, status: "not_applicable", fareRatio: 0 }; } const estimatedMinutes = Math.max( 1, Number(request.estimatedTravelMinutes || inProgressCancellationCompensationConfig.fallbackEstimatedMinutes) || inProgressCancellationCompensationConfig.fallbackEstimatedMinutes ); const elapsedRatio = elapsedMinutes / estimatedMinutes; const fareRatio = Math.min( inProgressCancellationCompensationConfig.maximumFareRatio, Math.max(inProgressCancellationCompensationConfig.minimumFareRatio, elapsedRatio) ); const minimumAmount = Math.max(minimumFareOffer(request.country), Math.ceil(fare * inProgressCancellationCompensationConfig.minimumFareRatio)); const maximumAmount = Math.max(minimumAmount, Math.ceil(fare * inProgressCancellationCompensationConfig.maximumFareRatio)); const amount = Math.min(maximumAmount, Math.max(minimumAmount, Math.ceil(fare * fareRatio))); return { amount, currency: moneyCurrencyForCountry(request.country), elapsedMinutes, status: amount > 0 ? "pending_charge" : "not_applicable", fareRatio }; } function riderCancellationNoPassengerChargeEstimate(request) { return { amount: 0, currency: moneyCurrencyForCountry(request?.country), elapsedMinutes: 0, status: "waived", fareRatio: 0 }; } function rideCancellationCompensationEstimate(request, atTime = Date.now(), actorRole = typeof activeRole === "function" ? activeRole() : "") { if (actorRole === "rider") return riderCancellationNoPassengerChargeEstimate(request); if (request?.status === "in_progress") return inProgressCancellationCompensationEstimate(request, atTime); return passengerCancellationFeeEstimate(request, atTime); } function cancellationFeeText(request) { const amount = Number(request?.cancellationFeeAmount ?? 0); if (amount > 0) { return `Passenger cancellation fee: ${formatMoney(amount, request.country)} (${request.cancellationFeeStatus ?? "pending"}).`; } if (typeof activeRole === "function" && activeRole() === "rider" && ["matched", "arrived", "in_progress"].includes(request?.status)) { return ""; } if (request?.status === "in_progress") { const estimate = inProgressCancellationCompensationEstimate(request); if (estimate.amount > 0) return `If this ride is cancelled now, Waka will charge a partial fare of ${formatMoney(estimate.amount, request.country)} for about ${estimate.elapsedMinutes} minute${estimate.elapsedMinutes === 1 ? "" : "s"} after pickup.`; } const estimate = passengerCancellationFeeEstimate(request); if (estimate.amount > 0) return `Passenger cancellation now may charge ${formatMoney(estimate.amount, request.country)} for rider time.`; if (estimate.status === "grace_period") return "Passenger cancellation is still inside the short no-fee grace window."; return ""; } function dollarsToCents(amount) { return Math.max(0, Math.round(Number(amount || 0) * 100)); } function centsToDollars(cents) { return Number(cents || 0) / 100; } function formatMoneyCents(cents, country = defaultLaunchCountry()) { if (moneyCurrencyForCountry(country) !== "USD") return formatMoney(Math.round(centsToDollars(cents)), country); return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(centsToDollars(cents)); } function stripeProcessingFeeCents(amountCents) { return Math.max(0, Math.ceil(Number(amountCents || 0) * stripeProcessingFeeRate + stripeProcessingFixedUsd * 100)); } function riderTrialHasEnded(rider) { if (!rider?.trialEndsAt) return true; return new Date(rider.trialEndsAt).getTime() < Date.now(); } function selectedRiderForRequest(request) { const riderId = selectedRiderIdForRequest(request); if (!riderId) return null; return state.riders.find((rider) => rider.id === riderId) ?? null; } function riderFacilitationFeeCents(fareCents, rider) { return riderTrialHasEnded(rider) ? Math.ceil(Number(fareCents || 0) * riderFacilitationFeeRate) : 0; } function businessRideServiceFeeCents(request, fareCents) { if (!request?.businessAccountId) return 0; const account = businessAccountForRequest(request); if (businessAccountWaivesRideServiceFee(account)) return 0; return Math.ceil(Number(fareCents || 0) * businessRideServiceFeeRate); } function rideFinancialBreakdown(request, tipAmount = 0) { const rider = selectedRiderForRequest(request); const fareCents = dollarsToCents(agreedFareForRequest(request)); const tipCents = dollarsToCents(tipAmount); const grossCents = fareCents + tipCents; const stripeFeeCents = directRidePaymentMode() ? 0 : stripeProcessingFeeCents(grossCents); const facilitationFeeCents = riderFacilitationFeeCents(fareCents, rider); const businessServiceFeeCents = businessRideServiceFeeCents(request, fareCents); const riderPayoutCents = Math.max(0, grossCents - stripeFeeCents - facilitationFeeCents); return { fareCents, tipCents, grossCents, stripeFeeCents, facilitationFeeCents, businessServiceFeeCents, riderPayoutCents, facilitationFeeWaived: facilitationFeeCents === 0, rider }; } function rideFinancialSummary(request) { if (!request || request.status !== "completed") return ""; const breakdown = rideFinancialBreakdown(request, totalTipAmountForRequest(request.id)); const businessFeeText = breakdown.businessServiceFeeCents ? ` Business account service fee charged separately to the business: ${formatMoneyCents(breakdown.businessServiceFeeCents, request.country)}.` : ""; if (directRidePaymentMode()) { return `Direct payment ride: passenger pays the rider by cash or mobile money. Waka commission is tracked through the rider wallet after any free period.${businessFeeText}`; } return `Rider payout estimate: ${formatMoneyCents(breakdown.riderPayoutCents, request.country)} after Stripe fee ${formatMoneyCents(breakdown.stripeFeeCents, request.country)}. Waka ride fee: $0; rider keeps the rest of the fare.${businessFeeText}`; } function paymentFromDatabase(value) { return { cash: "cash", mtn_money: "mtn", orange_money: "orange", agree_before_ride: "decide", online_card: "online_card", online_wallet: "online_wallet" }[value] ?? "decide"; } function paymentToDatabase(value) { return { cash: "cash", mtn: "mtn_money", orange: "orange_money", decide: "agree_before_ride", online_card: "online_card", online_wallet: "online_wallet" }[value] ?? "agree_before_ride"; } function paymentLabel(value) { return { cash: "Cash", mtn: "MTN Mobile Money", orange: "Orange Money", decide: "Agree before ride", online_card: "Card or online payment", online_wallet: "Online wallet or bank transfer" }[value] ?? "Agree before ride"; } function requiresOnlineRidePayment(country) { return Boolean(country && !africanRidePaymentCountries.has(country)); } function ridePaymentOptionsForCountry(country) { if (requiresOnlineRidePayment(country)) { return [ { value: "online_card", label: "Card or online payment" }, { value: "online_wallet", label: "Online wallet or bank transfer" } ]; } return [ { value: "cash", label: "Cash in hand" }, { value: "mtn", label: "MTN Mobile Money" }, { value: "orange", label: "Orange Money" }, { value: "decide", label: "Agree with rider before ride" } ]; } function validPaymentPreferenceForCountry(value, country) { const options = ridePaymentOptionsForCountry(country); return options.some((option) => option.value === value) ? value : options[0]?.value ?? "decide"; } function enabledLaunchCountries() { const configured = Array.isArray(appConfig.enabledLaunchCountries) ? appConfig.enabledLaunchCountries : []; const validConfigured = configured .map((country) => String(country ?? "").trim()) .filter((country) => country && countryCities[country]); if (validConfigured.length) return [...new Set(validConfigured)]; const firstLaunchCountry = String(appConfig.firstLaunchCountry ?? "").trim(); if (firstLaunchCountry && countryCities[firstLaunchCountry]) return [firstLaunchCountry]; return Object.keys(countryCities); } function onlineRidePaymentMarketCount() { return enabledLaunchCountries().filter((country) => requiresOnlineRidePayment(country)).length; } function ridePaymentProviderSupportsOnline(provider = appConfig.paymentProvider) { return productionOnlineRidePaymentProviderPattern.test(String(provider ?? "")); } function directRidePaymentMode() { return !ridePaymentProviderSupportsOnline() && enabledLaunchCountries().every((country) => !requiresOnlineRidePayment(country)); } function currentAccountNotifications(type) { const account = type === "passenger" ? state.passenger : state.rider; if (!account?.id) return []; const accountIds = typeof accountIdentityIdsForNoticeRole === "function" ? accountIdentityIdsForNoticeRole(type) : [account.id].filter(Boolean).map((id) => String(id)); const adminNotices = state.notifications .filter((notification) => accountIds.includes(String(notification.recipientId)) && notification.recipientRole === type) .map((notification) => ({ ...notification, noticeKind: "admin_notice" })); const financeNotices = financeAdjustmentsForAccount(type, account.id).map((adjustment) => ({ id: `finance-${adjustment.id}`, title: financeAdjustmentUserTitle(adjustment), body: financeAdjustmentUserBody(adjustment), recipientId: account.id, recipientRole: type, deliveryChannels: ["in_app"], createdAt: adjustment.processedAt || adjustment.updatedAt || adjustment.createdAt, noticeKind: "finance_adjustment" })); return [...adminNotices, ...financeNotices] .sort((a, b) => new Date(b.createdAt ?? 0) - new Date(a.createdAt ?? 0)); } function financeAdjustmentRecords() { return state.financeAdjustments ?? []; } function financeAdjustmentsForAccount(role, accountId) { if (!accountId) return []; return financeAdjustmentRecords() .filter((adjustment) => { const roleMatches = adjustment.subjectRole === role || (role === "passenger" && adjustment.subjectRole === "business"); return adjustment.subjectId === accountId && roleMatches && adjustment.visibleToUser !== false; }) .sort((a, b) => new Date(b.processedAt ?? b.updatedAt ?? b.createdAt ?? 0) - new Date(a.processedAt ?? a.updatedAt ?? a.createdAt ?? 0)); } function financeAdjustmentUserTitle(adjustment) { const labels = { passenger_refund: "Ride refund", partial_passenger_refund: "Partial ride refund", business_service_fee_refund: "Business ride fee refund", rider_subscription_refund: "Rider access refund", business_subscription_refund: "Business subscription refund", passenger_credit: "Passenger credit", passenger_debit: "Passenger account adjustment", rider_bonus: "Rider bonus", rider_debit: "Rider account adjustment", manual_correction: "Account correction" }; return labels[adjustment.adjustmentType] || "Finance update"; } function financeAdjustmentUserBody(adjustment) { const amount = formatMoneyCents(adjustment.amountCents || 0); const status = String(adjustment.status || "recorded").replace(/_/g, " "); const reason = adjustment.reason ? ` Reason: ${adjustment.reason}` : ""; return `${amount} ${financeAdjustmentUserTitle(adjustment).toLowerCase()} is ${status}.${reason}`; } function riderPaymentRequests(riderId = state.rider?.id) { if (!riderId) return []; return state.paymentRequests.filter((request) => request.riderId === riderId).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); } function pendingPaymentRequestForRider(riderId = state.rider?.id) { return riderPaymentRequests(riderId).find((request) => request.status === "pending") ?? null; } function paymentAccountRecords() { return state.paymentAccounts; } function paymentAccountFor(role, userId) { if (!userId) return null; return paymentAccountRecords() .filter((account) => account.role === role && account.userId === userId) .sort((a, b) => new Date(b.updatedAt ?? b.createdAt ?? 0) - new Date(a.updatedAt ?? a.createdAt ?? 0))[0] ?? null; } async function refreshPaymentAccountsFromSupabase(role) { if (!hasSupabaseRuntime()) return false; const roles = role ? [role] : ["passenger", "rider"]; let refreshed = false; for (const accountRole of roles) { const account = accountRole === "passenger" ? state.passenger : currentRiderRecord(); const userId = account?.id; if (!userId || !hasSignedIn(accountRole)) continue; const rows = supabaseClient ? await withSupabaseTimeout( supabaseClient .from("payment_accounts") .select("*") .eq("user_id", userId) .eq("role", accountRole) .order("updated_at", { ascending: false }), `Loading ${accountRole} payment account`, optionalSupabaseRequestTimeoutMs ).then(({ data, error }) => { if (error) throw error; return data ?? []; }) : await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/payment_accounts?select=*&user_id=eq.${encodeURIComponent(userId)}&role=eq.${encodeURIComponent(accountRole)}&order=updated_at.desc`, { accessToken: supabaseRestSession?.access_token }), `Loading ${accountRole} payment account`, optionalSupabaseRequestTimeoutMs ); rows.map((row) => mapPaymentAccountFromDatabase(row, new Map([[userId, { full_name: account?.name, email: account?.email }]]))) .forEach((paymentAccount) => { state.paymentAccounts = upsertById( state.paymentAccounts.filter((item) => !(item.role === paymentAccount.role && item.userId === paymentAccount.userId)), paymentAccount ); refreshed = true; }); } if (refreshed) saveState(); return refreshed; } function paymentAccountReady(role, account) { const userId = account?.id; if (!userId) return false; if (directRidePaymentMode()) return true; if (paymentSetupRelaxedForTesting()) return true; return Boolean(paymentAccountFor(role, userId)?.status === "linked"); } function stagingPaymentAccountForTesting(role, account) { const existing = paymentAccountFor(role, account?.id); const isRider = role === "rider"; const now = new Date().toISOString(); return { id: existing?.id ?? makeId("payacct"), userId: account.id, userName: account.name, role, provider: isRider ? "stripe-connect-test" : "stripe-test", accountType: isRider ? "test_payout_account" : "test_card", accountHolder: account.name || (isRider ? "Waka rider" : "Waka passenger"), accountLast4: isRider ? "0000" : "4242", institutionName: isRider ? "Stripe Connect staging payout" : "Stripe test mode", reference: `staging-${isRider ? "rider-payout" : "test"}-${account.id}`, status: "linked", createdAt: existing?.createdAt ?? now, updatedAt: now }; } async function ensureStagingPaymentAccountForTesting(role, account, { localFallback = true } = {}) { if (!paymentSetupRelaxedForTesting() || !account?.id || !hasSignedIn(role)) return paymentAccountFor(role, account?.id); const existing = paymentAccountFor(role, account.id); if (existing?.status === "linked") return existing; const stagingAccount = stagingPaymentAccountForTesting(role, account); let savedAccount = stagingAccount; try { savedAccount = await savePaymentAccountToSupabase(stagingAccount); } catch (error) { if (!localFallback) throw error; logClientWarning(`Staging ${role} payment account could not be saved to Supabase; keeping it local for pilot testing.`, error); } state.paymentAccounts = upsertById( state.paymentAccounts.filter((item) => !(item.role === role && item.userId === account.id)), savedAccount ); saveState(); return savedAccount; } function paymentAccountSummary(role, account) { const paymentAccount = paymentAccountFor(role, account?.id); if (!account) return "Sign in before payment setup."; if (directRidePaymentMode()) { return role === "rider" ? "Cameroon direct-payment mode is active. Rider wallet and commission obligations are handled by Waka operations after the free period." : "Cameroon direct-payment mode is active. Passenger pays the matched rider by cash, MTN Mobile Money, or Orange Money."; } if (paymentSetupRelaxedForTesting()) { return role === "rider" ? "Staging payout setup is relaxed for testing. Stripe Connect is still required before production." : "Staging payment setup is relaxed for testing. A real payment method is still required before production."; } if (role === "rider" && !paymentAccount) return "Stripe payout account is not connected yet."; if (!paymentAccount) return "Stripe payment setup required. Waka does not collect card or bank credentials."; const ending = paymentAccount.accountLast4 && paymentAccount.accountLast4 !== "0000" ? ` ending ${paymentAccount.accountLast4}` : ""; if (role === "rider") { return paymentAccount.status === "linked" ? `Stripe payout account${ending} is linked.` : "Stripe payout setup is not complete yet. Finish Stripe Connect onboarding before receiving ride requests."; } return `${paymentAccount.provider} ${paymentAccount.accountType}${ending} is ${paymentAccount.status}.`; } function paymentSetupRelaxedForTesting() { return configFlagEnabled(appConfig.relaxPaymentSetupForTesting) && /\b(staging|pilot|test|preview)\b/i.test(String(appConfig.projectName || "")); } function paymentSetupConfirmFunctionName() { return String(appConfig.paymentSetupConfirmFunctionName || "payment-method-setup-confirm").trim() || "payment-method-setup-confirm"; } function paymentSetupReturnParams() { const params = new URLSearchParams(window.location.search); const payment = String(params.get("payment") || "").toLowerCase(); if (!payment) return null; const path = window.location.pathname.toLowerCase(); const role = path.includes("rider") ? "rider" : "passenger"; return { payment, sessionId: String(params.get("session_id") || "").trim(), role }; } function normalizePaymentSetupRole(role) { return role === "rider" ? "rider" : "passenger"; } function readPendingPaymentSetup() { try { const pending = JSON.parse(localStorage.getItem(pendingPaymentSetupStorageKey)); if (!pending || typeof pending !== "object") return null; const sessionId = String(pending.sessionId || "").trim(); const role = normalizePaymentSetupRole(pending.role); const createdAtMs = new Date(pending.createdAt || 0).getTime(); if (!sessionId || !Number.isFinite(createdAtMs) || Date.now() - createdAtMs > pendingPaymentSetupMaxAgeMs) { localStorage.removeItem(pendingPaymentSetupStorageKey); return null; } return { payment: "success", sessionId, role, pending: true }; } catch { try { localStorage.removeItem(pendingPaymentSetupStorageKey); } catch { // Storage can be unavailable; the URL return can still finish setup. } return null; } } function rememberPendingPaymentSetup(role, sessionId) { const normalizedSessionId = String(sessionId || "").trim(); if (!normalizedSessionId) return; try { localStorage.setItem(pendingPaymentSetupStorageKey, JSON.stringify({ role: normalizePaymentSetupRole(role), sessionId: normalizedSessionId, createdAt: new Date().toISOString() })); } catch { // If storage is blocked, Waka can still confirm while the return URL is present. } } function clearPendingPaymentSetup(sessionId = "") { try { if (!sessionId) { localStorage.removeItem(pendingPaymentSetupStorageKey); stopPendingPaymentSetupPolling(); return; } const pending = JSON.parse(localStorage.getItem(pendingPaymentSetupStorageKey)); if (!pending || String(pending.sessionId || "") === sessionId) { localStorage.removeItem(pendingPaymentSetupStorageKey); stopPendingPaymentSetupPolling(); } } catch { // Nothing else to clear. } } function stopPendingPaymentSetupPolling() { if (!pendingPaymentSetupPollTimer) return; window.clearInterval(pendingPaymentSetupPollTimer); pendingPaymentSetupPollTimer = null; } function paymentSetupStillInProgressError(error) { return /\bnot complete yet\b|\bdid not save a payment method\b|\bsetup intent\b/i.test(String(error?.message || error || "")); } function schedulePendingPaymentSetupConfirmation({ immediate = false } = {}) { if (!readPendingPaymentSetup()) { stopPendingPaymentSetupPolling(); return; } if (immediate) { window.setTimeout(() => { void handlePaymentSetupReturnFromLocation({ fromPendingCheck: true }); }, 0); } if (pendingPaymentSetupPollTimer) return; let attempts = 0; pendingPaymentSetupPollTimer = window.setInterval(() => { attempts += 1; if (!readPendingPaymentSetup() || attempts > 60) { stopPendingPaymentSetupPolling(); return; } void handlePaymentSetupReturnFromLocation({ fromPendingCheck: true }); }, 5000); } function paymentStatusElement(role) { return role === "rider" ? els.riderPaymentStatus : els.passengerPaymentStatus; } function clearPaymentSetupReturnParams() { const params = new URLSearchParams(window.location.search); params.delete("payment"); params.delete("session_id"); const nextQuery = params.toString(); const nextUrl = `${window.location.pathname}${nextQuery ? `?${nextQuery}` : ""}${window.location.hash || ""}`; window.history.replaceState({}, "", nextUrl); } function subscriptionCheckoutReturnParams() { const params = new URLSearchParams(window.location.search); const subscription = String(params.get("subscription") || "").trim().toLowerCase(); if (!subscription) return null; if (!["success", "cancelled", "business_success", "business_cancelled"].includes(subscription)) return null; return { kind: subscription.startsWith("business_") ? "business" : "rider", status: subscription.includes("cancelled") ? "cancelled" : "success" }; } function clearSubscriptionCheckoutReturnParams() { const params = new URLSearchParams(window.location.search); params.delete("subscription"); const nextQuery = params.toString(); const nextUrl = `${window.location.pathname}${nextQuery ? `?${nextQuery}` : ""}${window.location.hash || ""}`; window.history.replaceState({}, "", nextUrl); } function subscriptionCheckoutReturnMessage({ kind, status }) { if (kind === "business") { return status === "success" ? "Business Partner checkout completed. Waka is refreshing the monthly billing state; billing starts after any active free business month." : "Business Partner checkout was cancelled. Verified businesses can still use the free month and Starter can continue with the 10% completed-ride fee model after that."; } return status === "success" ? "Rider access checkout completed. Waka is refreshing Stripe confirmation; access will show active as soon as the webhook confirms payment." : "Rider access checkout was cancelled. Rider marketplace access stays unchanged until subscription payment succeeds."; } function applySubscriptionCheckoutReturnRoute({ kind }) { if (kind === "business") { state.activeTab = "passenger"; state.passengerPage = "business"; if (!hasSignedIn("passenger")) state.accountMode.passenger = "signin"; if (typeof updatePassengerWorkspaceRoute === "function") { updatePassengerWorkspaceRoute("business", { replace: true }); } return; } state.activeTab = "rider"; state.riderPage = "checks"; if (!hasSignedIn("rider")) state.accountMode.rider = "signin"; if (typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute("checks", { replace: true }); } } async function handleSubscriptionCheckoutReturnFromLocation() { const checkoutReturn = subscriptionCheckoutReturnParams(); if (!checkoutReturn) return false; applySubscriptionCheckoutReturnRoute(checkoutReturn); clearSubscriptionCheckoutReturnParams(); saveState(); await loadMarketplaceFromSupabase({ includeAccountData: true }).catch((error) => { logClientWarning("Marketplace refresh after subscription checkout return was skipped.", error); }); renderAll(); const statusElement = checkoutReturn.kind === "business" ? els.businessAccountStatus : els.subscriptionPaymentStatus; if (statusElement) statusElement.textContent = subscriptionCheckoutReturnMessage(checkoutReturn); return true; } async function ensureSignedInForPaymentReturn(role) { if (hasSignedIn(role)) return true; const user = await getSupabaseUser().catch(() => null); if (!user) return false; const profile = supabaseClient ? await supabaseClient .from("profiles") .select("*") .eq("id", user.id) .maybeSingle() .then(({ data, error }) => { if (error) throw error; return data; }) : await selectProfileRest(user.id, "*", supabaseRestSession?.access_token); if (!profile || profile.role !== role) return false; applySignedInProfile(role, profile, user); state.activeTab = role; if (role === "passenger") state.passengerPage = "payment"; saveState(); return true; } async function confirmPaymentMethodSetup(sessionId, role) { const token = await currentSupabaseAccessToken(); if (!token) throw new Error("Sign in to finish linking the Stripe payment method."); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${paymentSetupConfirmFunctionName()}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ sessionId, role }) }), "Confirming Stripe payment setup", supabaseProfileSaveTimeoutMs ); const payload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(payload?.error || "Stripe payment setup could not be confirmed."); return payload.paymentAccount; } async function handlePaymentSetupReturnFromLocation({ fromPendingCheck = false } = {}) { const locationReturn = paymentSetupReturnParams(); if (locationReturn?.payment === "success" && locationReturn.sessionId) { rememberPendingPaymentSetup(locationReturn.role, locationReturn.sessionId); } const paymentReturn = locationReturn ?? readPendingPaymentSetup(); if (!paymentReturn) return false; const { payment, sessionId, role } = paymentReturn; const status = paymentStatusElement(role); const signedInAccount = role === "passenger" ? state.passenger : currentRiderRecord(); if (!locationReturn && payment === "success" && paymentAccountReady(role, signedInAccount)) { clearPendingPaymentSetup(sessionId); if (status) status.textContent = paymentAccountSummary(role, signedInAccount); if (role === "passenger") state.passengerPage = "request"; saveState(); renderAll(); return true; } state.activeTab = role; if (role === "passenger") state.passengerPage = "payment"; if (payment === "cancelled") { if (status) status.textContent = "Stripe card setup was cancelled. No passenger payment method was linked."; clearPendingPaymentSetup(sessionId); clearPaymentSetupReturnParams(); saveState(); return true; } if (payment !== "success") return false; if (!await ensureSignedInForPaymentReturn(role)) { state.accountMode[role] = "signin"; if (status) status.textContent = "Sign in to finish linking the Stripe payment method."; if (locationReturn) clearPaymentSetupReturnParams(); saveState(); if (!fromPendingCheck) renderAll(); return true; } if (!sessionId) { if (status) status.textContent = "Stripe returned without a setup session id. Open Stripe setup again so Waka can link the saved card immediately."; saveState(); return true; } try { if (status) status.textContent = "Confirming saved Stripe payment method..."; const row = await confirmPaymentMethodSetup(sessionId, role); const account = role === "passenger" ? state.passenger : currentRiderRecord(); const savedAccount = mapPaymentAccountFromDatabase(row, new Map([[account?.id, { full_name: account?.name }]])); state.paymentAccounts = upsertById( state.paymentAccounts.filter((item) => !(item.role === role && item.userId === account?.id)), savedAccount ); await refreshPaymentAccountsFromSupabase(role); await loadMarketplaceFromSupabase({ includeAccountData: true }).catch((marketplaceError) => { logClientWarning("Marketplace refresh after Stripe setup was skipped.", marketplaceError); }); clearPendingPaymentSetup(sessionId); clearPaymentSetupReturnParams(); if (role === "passenger") state.passengerPage = "request"; saveState(); renderAll(); const refreshedStatus = paymentStatusElement(role); if (refreshedStatus) refreshedStatus.textContent = paymentAccountSummary(role, account); if (role === "passenger" && els.passengerRideGate) { els.passengerRideGate.textContent = "Passenger payment method is ready. You can request rides."; } return true; } catch (error) { await refreshPaymentAccountsFromSupabase(role).catch(() => false); const account = role === "passenger" ? state.passenger : currentRiderRecord(); if (paymentAccountReady(role, account)) { clearPendingPaymentSetup(sessionId); if (role === "passenger") state.passengerPage = "request"; saveState(); renderAll(); const refreshedStatus = paymentStatusElement(role); if (refreshedStatus) refreshedStatus.textContent = paymentAccountSummary(role, account); return true; } if (paymentSetupStillInProgressError(error)) { if (status) status.textContent = "Finish Stripe card setup in the secure tab, then return here. Waka will link the saved card automatically."; schedulePendingPaymentSetupConfirmation(); saveState(); return true; } if (status) status.textContent = `Stripe payment setup could not be linked yet: ${error.message}`; saveState(); return true; } } function selectedSubscriptionPlanKey() { const value = els.subscriptionPlan?.value || "monthly"; return riderSubscriptionPlans[value] ? value : "monthly"; } function selectedSubscriptionRenewalMode() { return els.subscriptionRenewalMode?.value === "automatic" ? "automatic" : "manual"; } function selectedSubscriptionPlan() { return riderSubscriptionPlans[selectedSubscriptionPlanKey()] ?? riderSubscriptionPlans.monthly; } function riderPlanSummary() { if (directRidePaymentMode()) { return `After the ${trialDays}-day commission-free period, the first 5 completed rides per day remain free and ride 6+ creates a 10% Waka commission obligation tracked through the rider wallet. Minimum wallet target: 5,000 FCFA.`; } const plans = Object.values(riderSubscriptionPlans) .map((plan) => plan.description) .join("; "); return `After the ${trialDays}-day free trial: ${plans}. Riders can renew manually before expiry or choose automatic provider renewal. Waka takes $0 from each ride fare; rider payout is fare minus Stripe/payment processing only.`; } const businessPlanLabels = { [businessStarterPlanCode]: `Starter - ${businessFreeTrialDays}-day free month, then ${Math.round(businessRideServiceFeeRate * 100)}% per completed ride`, [businessPartnerPlanCode]: `Partner - ${businessFreeTrialDays}-day free month, then ${formatMoney(businessPartnerMonthlySubscriptionFee)}/month with no ${Math.round(businessRideServiceFeeRate * 100)}% ride fee` }; function normalizeBusinessPlanCode(value) { return value === businessPartnerPlanCode ? businessPartnerPlanCode : businessStarterPlanCode; } function businessPlanLabel(value) { return businessPlanLabels[normalizeBusinessPlanCode(value)]; } function normalizeBusinessAccountStatus(value) { const status = String(value || "pending_review").toLowerCase(); if (status === "pending") return "pending_review"; return ["pending_review", "active", "past_due", "suspended", "cancelled", "rejected"].includes(status) ? status : "pending_review"; } function businessVerificationLabel(status) { return { pending_review: "Pending Waka verification", verified: "Verified by Waka", rejected: "Rejected", suspended: "Suspended" }[status] ?? status; } function businessAccountRecords() { return state.businessAccounts; } function businessSubscriptionRecords() { return state.businessSubscriptions; } function passengerBusinessAccounts(passenger = state.passenger) { if (!passenger?.id) return []; return businessAccountRecords() .filter((account) => account.ownerId === passenger.id) .sort((a, b) => new Date(b.createdAt ?? 0) - new Date(a.createdAt ?? 0)); } function businessSubscriptionFor(accountId) { if (!accountId) return null; return businessSubscriptionRecords() .filter((subscription) => subscription.businessAccountId === accountId) .sort((a, b) => new Date(b.updatedAt ?? b.createdAt ?? 0) - new Date(a.updatedAt ?? a.createdAt ?? 0))[0] ?? null; } function businessSubscriptionIsActive(subscription, now = new Date()) { if (!subscription || subscription.status !== "active") return false; const paidUntil = new Date(subscription.paidUntil || ""); return Number.isFinite(paidUntil.getTime()) && paidUntil.getTime() > now.getTime(); } function businessFreeTrialEndsAt(account) { const endsAt = new Date(account?.freeTrialEndsAt || ""); return Number.isFinite(endsAt.getTime()) ? endsAt : null; } function businessFreeTrialIsActive(account, now = new Date()) { const endsAt = businessFreeTrialEndsAt(account); return Boolean(endsAt && endsAt.getTime() > now.getTime()); } function businessFreeTrialText(account) { const endsAt = businessFreeTrialEndsAt(account); if (!endsAt) return `${businessFreeTrialDays}-day free month starts after Waka verification.`; const daysLeft = Math.max(0, Math.ceil((endsAt.getTime() - Date.now()) / (24 * 60 * 60 * 1000))); return daysLeft > 0 ? `Free business month active until ${formatDate(endsAt)} (${pluralDays(daysLeft)} left).` : `Free business month ended on ${formatDate(endsAt)}.`; } function businessAccountForRequest(request) { if (!request?.businessAccountId) return null; return businessAccountRecords().find((account) => account.id === request.businessAccountId) ?? null; } function businessAccountWaivesRideServiceFee(account) { if (businessFreeTrialIsActive(account)) return true; return normalizeBusinessPlanCode(account?.planCode) === businessPartnerPlanCode; } function businessAccountCanRequest(account) { if (!account) return false; if (normalizeBusinessAccountStatus(account.status) !== "active") return false; if ((account.verificationStatus || "pending_review") !== "verified") return false; if (businessFreeTrialIsActive(account)) return true; if (normalizeBusinessPlanCode(account.planCode) === businessPartnerPlanCode) { return businessSubscriptionIsActive(businessSubscriptionFor(account.id)); } return true; } function businessAccountSummary(account) { const serviceFee = `${Math.round(businessRideServiceFeeRate * 100)}% business ride service fee`; if (!account) return `Business accounts require Waka verification before ride billing. Waka gives verified businesses one free month; after that Starter adds a ${serviceFee}, or Partner is ${formatMoney(businessPartnerMonthlySubscriptionFee)}/month with no per-ride Waka business fee.`; const status = normalizeBusinessAccountStatus(account.status); const verification = account.verificationStatus || "pending_review"; const planCode = normalizeBusinessPlanCode(account.planCode); const subscription = businessSubscriptionFor(account.id); if (status === "rejected" || verification === "rejected") return `${account.businessName} was not approved for business ride billing.`; if (status === "suspended") return `${account.businessName} is suspended and cannot publish business rides.`; if (status !== "active" || verification !== "verified") return `${account.businessName} is waiting for Waka verification before the ${businessFreeTrialDays}-day free business month starts.`; const trialText = businessFreeTrialText(account); if (planCode === businessPartnerPlanCode) { if (businessFreeTrialIsActive(account)) return `${account.businessName} is verified on Business Partner. ${trialText} The monthly Partner charge starts after the free month and waives the ${Math.round(businessRideServiceFeeRate * 100)}% Waka ride fee.`; if (businessSubscriptionIsActive(subscription)) return `${account.businessName} is verified on Business Partner. The ${formatMoney(businessPartnerMonthlySubscriptionFee)}/month plan waives the ${Math.round(businessRideServiceFeeRate * 100)}% Waka ride fee.`; return `${account.businessName} is verified for Business Partner. ${trialText} Start the ${formatMoney(businessPartnerMonthlySubscriptionFee)}/month checkout to keep business rides active after the free month.`; } return `${account.businessName} is verified on Business Starter. ${trialText} After the free month, completed business rides add a ${serviceFee} paid to Waka, separate from the rider fare.`; } function localDateKey(date = new Date()) { const local = new Date(date.getTime() - date.getTimezoneOffset() * 60000); return local.toISOString().slice(0, 10); } function riderDayPreferenceRecords() { return state.riderDayPreferences; } function riderDayPreferenceFor(rider = currentRiderRecord(), dateKey = localDateKey()) { if (!rider?.id) return null; const localPreferenceDate = rider.dailyRegions?.serviceDate ?? rider.dailyRegions?.date; const localPreference = localPreferenceDate === dateKey ? rider.dailyRegions : null; return riderDayPreferenceRecords() .find((item) => item.riderId === rider.id && item.serviceDate === dateKey) ?? localPreference; } function riderDailyDestinationRegions(rider = currentRiderRecord()) { const preference = riderDayPreferenceFor(rider); return Array.isArray(preference?.regions) ? preference.regions.filter(Boolean) : []; } function riderDailyRegionsReady(rider = currentRiderRecord()) { return riderDailyDestinationRegions(rider).length > 0; } function riderShowsAllNearbyPickups(rider = currentRiderRecord()) { if (rider) return true; const preference = riderDayPreferenceFor(rider); return preference?.showAllNearbyPickups === true || state.riderDestinationScope === "all"; } function riderDestinationScopeLabel() { return "all nearby pickups"; } function riderDailyRegionUpdatesUsed(rider = currentRiderRecord()) { return Number(riderDayPreferenceFor(rider)?.updatesUsed ?? 0); } function riderDailyRegionUpdatesRemaining(rider = currentRiderRecord()) { return Math.max(0, 2 - riderDailyRegionUpdatesUsed(rider)); } function taxDocumentRecords() { return state.taxDocuments; } function taxIdentityReferenceRecords() { return state.taxIdentityReferences; } function taxIdentityForRider(riderId = state.rider?.id) { if (!riderId) return null; return taxIdentityReferenceRecords() .filter((record) => record.riderId === riderId) .sort((a, b) => new Date(b.updatedAt ?? b.createdAt ?? 0) - new Date(a.updatedAt ?? a.createdAt ?? 0))[0] ?? null; } function taxIdentityStatusText(reference) { if (!reference) return "Not started"; const status = String(reference.status || "pending").replace(/_/g, " "); const last4 = reference.tinLast4 ? ` Last four: ${reference.tinLast4}.` : " Full tax identifier is not stored in Waka."; return `${reference.provider || appConfig.taxOnboardingProvider || "Provider"}: ${status}.${last4}`; } function taxDocumentsForRider(riderId = state.rider?.id) { if (!riderId) return []; return taxDocumentRecords() .filter((document) => document.riderId === riderId) .sort((a, b) => { const yearDelta = Number(b.taxYear) - Number(a.taxYear); if (yearDelta) return yearDelta; return new Date(b.availableAt ?? b.issuedAt ?? b.updatedAt ?? b.createdAt ?? 0) - new Date(a.availableAt ?? a.issuedAt ?? a.updatedAt ?? a.createdAt ?? 0); }); } function rideRatingRecords() { return state.rideRatings; } const riderRatingCategoryDefinitions = [ { key: "overall", label: "Overall", field: "score", percentField: "overallPercent" }, { key: "safety", label: "Safety", field: "safetyScore", percentField: "safetyPercent" }, { key: "punctuality", label: "Pickup timing", field: "punctualityScore", percentField: "punctualityPercent" }, { key: "communication", label: "Communication", field: "communicationScore", percentField: "communicationPercent" }, { key: "vehicle", label: "Vehicle", field: "vehicleScore", percentField: "vehiclePercent" } ]; function rideSettlementRecords() { return state.rideSettlements; } function rideTipRecords() { return state.rideTips; } function totalTipAmountForRequest(requestId) { return rideTipRecords() .filter((tip) => tip.requestId === requestId && !["failed", "refunded"].includes(tip.status)) .reduce((total, tip) => total + Number(tip.amount || 0), 0); } function passengerTipForRequest(requestId, passengerId = state.passenger?.id) { if (!requestId || !passengerId) return null; return rideTipRecords().find((tip) => tip.requestId === requestId && tip.passengerId === passengerId) ?? null; } function ratingsForRider(riderId) { if (!riderId) return []; return rideRatingRecords().filter((rating) => rating.ratedUserId === riderId); } function riderRatingAggregateForRider(riderId) { if (!riderId || state.adminSession?.source === "supabase") return null; const summary = state.riderRatingSummary; if (!summary || summary.riderId !== riderId || !Number(summary.ratingCount)) return null; return summary; } function ratingPercentFromAverage(average) { return Number.isFinite(average) ? Math.round((average / 5) * 100) : null; } function averageScoreForRatings(ratings, field) { const values = ratings .map((rating) => Number(rating[field] ?? rating.score)) .filter((value) => Number.isFinite(value) && value >= 1 && value <= 5); if (!values.length) return null; return values.reduce((total, value) => total + value, 0) / values.length; } function riderRatingCategorySummaries(riderId) { const aggregate = riderRatingAggregateForRider(riderId); if (aggregate) { return riderRatingCategoryDefinitions.map((definition) => ({ key: definition.key, label: definition.label, percent: aggregate[definition.percentField], count: aggregate.ratingCount })); } const ratings = ratingsForRider(riderId); return riderRatingCategoryDefinitions.map((definition) => ({ key: definition.key, label: definition.label, percent: ratingPercentFromAverage(averageScoreForRatings(ratings, definition.field)), count: ratings.length })); } function averageRatingForRider(riderId) { const aggregate = riderRatingAggregateForRider(riderId); if (aggregate) { return { average: Number(aggregate.overallPercent) / 20, count: aggregate.ratingCount, percent: aggregate.overallPercent }; } const ratings = ratingsForRider(riderId); if (!ratings.length) return null; const average = ratings.reduce((total, rating) => total + Number(rating.score || 0), 0) / ratings.length; return { average, count: ratings.length, percent: ratingPercentFromAverage(average) }; } function ratingSummaryForRider(riderId) { const rating = averageRatingForRider(riderId); return rating ? `${Math.round(rating.percent ?? rating.average * 20)}% from ${rating.count} rating${rating.count === 1 ? "" : "s"}` : "new"; } function requestDestinationText(request) { const area = request?.destinationArea; const detail = request?.destinationFormattedAddress || request?.destination; const destinationText = detail || area || "Destination"; const stops = normalizeRideStops(request?.rideStops); return stops.length ? `${stops.join(" -> ")} -> ${destinationText}` : destinationText; } function estimatedTravelMinutesForRequest(request) { const stored = Number(request?.estimatedTravelMinutes); if (Number.isFinite(stored) && stored > 0) return stored; const guidance = fareGuidanceForRide( request?.country, request?.city, request?.pickupArea, request?.destinationArea, requestPickupGps(request), request?.rideStops ); return guidance?.minutes ?? 30; } function destinationUpdateWindowMinutes(request) { return Math.max(5, Math.ceil(estimatedTravelMinutesForRequest(request) * destinationUpdateTravelFraction)); } function canUpdateRideDestination(request) { if (!request || activeRole() !== "passenger" || !requestBelongsToPassenger(request)) return false; return ["open", "matched", "arrived", "in_progress"].includes(request.status); } function requestDestinationMatchesDailyRegions(request, rider = currentRiderRecord()) { if (!request || !rider) return false; if (requestHasRiderMatch(request)) return true; if (riderShowsAllNearbyPickups(rider)) return true; const regions = riderDailyDestinationRegions(rider).map((region) => region.toLowerCase()); if (!regions.length) return false; const destinationArea = String(request.destinationArea ?? "").toLowerCase(); const destinationText = String(request.destination ?? "").toLowerCase(); return regions.some((region) => destinationArea === region || destinationText.includes(region)); } function isSubscriptionActive(rider) { if (!rider || rider.status !== "approved") return false; const now = Date.now(); const trialActive = rider.trialEndsAt && new Date(rider.trialEndsAt).getTime() >= now; const paidActive = rider.subscriptionPaidUntil && new Date(rider.subscriptionPaidUntil).getTime() >= now; return Boolean(trialActive || paidActive); } function daysUntil(value) { if (!value) return 0; return Math.max(0, Math.ceil((new Date(value).getTime() - Date.now()) / 86400000)); } function riderAccessEnd(rider) { if (!rider) return null; const trialTime = rider.trialEndsAt ? new Date(rider.trialEndsAt).getTime() : 0; const paidTime = rider.subscriptionPaidUntil ? new Date(rider.subscriptionPaidUntil).getTime() : 0; if (paidTime >= trialTime && rider.subscriptionPaidUntil) return rider.subscriptionPaidUntil; return rider.trialEndsAt ?? rider.subscriptionPaidUntil ?? null; } function riderAccessLabel(rider) { const now = Date.now(); if (rider?.subscriptionPaidUntil && new Date(rider.subscriptionPaidUntil).getTime() >= now) return "paid access"; if (rider?.trialEndsAt && new Date(rider.trialEndsAt).getTime() >= now) return "free trial"; return "rider access"; } function pluralDays(days) { return `${days} day${days === 1 ? "" : "s"}`; } function mapPaymentRequestFromDatabase(request, riderMap = new Map()) { const rider = riderMap.get(request.rider_id); return { id: request.id, riderId: request.rider_id, riderName: rider?.name ?? rider?.full_name ?? "Rider", planType: request.plan_type ?? "monthly", amount: request.amount_xaf, provider: request.provider, paymentPhone: request.payment_phone, reference: request.provider_reference, status: request.status, reviewNote: request.review_note ?? "", reviewedBy: request.reviewed_by, reviewedAt: request.reviewed_at, createdAt: request.created_at }; } function mapPaymentAccountFromDatabase(account, profileMap = new Map()) { const profile = profileMap.get(account.user_id); return { id: account.id, userId: account.user_id, userName: profile?.full_name ?? profile?.email ?? "Account holder", role: account.role, provider: account.provider, accountType: account.account_type, accountHolder: account.account_holder, accountLast4: account.account_last4, institutionName: account.institution_name, reference: account.provider_reference, providerCustomerReference: account.provider_customer_reference ?? "", status: account.status, createdAt: account.created_at, updatedAt: account.updated_at }; } function mapBusinessAccountFromDatabase(row, profileMap = new Map()) { const owner = profileMap.get(row.owner_id); return { id: row.id, ownerId: row.owner_id, ownerName: owner?.full_name ?? owner?.email ?? "Business owner", businessName: row.business_name, billingEmail: row.billing_email, businessCategory: row.business_category ?? "other", businessAddress: row.business_address ?? "", contactName: row.contact_name ?? "", contactPhone: row.contact_phone ?? "", planCode: normalizeBusinessPlanCode(row.plan_code), verificationStatus: row.verification_status ?? "pending_review", reviewedBy: row.reviewed_by ?? null, reviewedAt: row.reviewed_at ?? null, freeTrialStartedAt: row.free_trial_started_at ?? null, freeTrialEndsAt: row.free_trial_ends_at ?? null, reviewNote: row.review_note ?? "", status: normalizeBusinessAccountStatus(row.status), createdAt: row.created_at, updatedAt: row.updated_at }; } function mapBusinessSubscriptionFromDatabase(row) { return { id: row.id, businessAccountId: row.business_account_id, planCode: normalizeBusinessPlanCode(row.plan_code), amount: centsToDollars(row.amount_cents), provider: row.provider, reference: row.provider_reference ?? "", paidUntil: row.paid_until ?? null, status: row.status, refundedAmount: centsToDollars(row.refunded_amount_cents), refundStatus: row.refund_status ?? "not_refunded", refundReference: row.refund_reference ?? "", refundReason: row.refund_reason ?? "", refundedAt: row.refunded_at ?? null, createdAt: row.created_at, updatedAt: row.updated_at }; } function mapRideSettlementFromDatabase(row, profileMap = new Map()) { const passenger = profileMap.get(row.passenger_id); const rider = profileMap.get(row.rider_id); return { id: row.id, requestId: row.ride_request_id, passengerId: row.passenger_id, passengerName: passenger?.full_name ?? "Passenger", riderId: row.rider_id, riderName: rider?.full_name ?? "Rider", fareAmount: centsToDollars(row.fare_amount_cents), stripeFeeAmount: centsToDollars(row.stripe_fee_cents), facilitationFeeAmount: centsToDollars(row.facilitation_fee_cents), businessServiceFeeAmount: centsToDollars(row.business_service_fee_cents), riderPayoutAmount: centsToDollars(row.rider_payout_cents), status: row.status, providerReference: row.provider_reference ?? "", providerChargeReference: row.provider_charge_reference ?? "", providerTransferReference: row.provider_transfer_reference ?? "", providerFeeAmount: centsToDollars(row.provider_fee_cents), businessServiceFeeProviderReference: row.business_service_fee_provider_reference ?? "", businessServiceFeeStatus: row.business_service_fee_status ?? (row.business_service_fee_cents ? "pending_charge" : "not_applicable"), businessServiceFeeFailureReason: row.business_service_fee_failure_reason ?? "", passengerRefundedAmount: centsToDollars(row.passenger_refunded_cents), businessServiceFeeRefundedAmount: centsToDollars(row.business_service_fee_refunded_cents), lastRefundReference: row.last_refund_reference ?? "", refundReason: row.refund_reason ?? "", processedAt: row.processed_at, failureReason: row.failure_reason ?? "", createdAt: row.created_at, updatedAt: row.updated_at }; } function mapRideTipFromDatabase(row, profileMap = new Map()) { const passenger = profileMap.get(row.passenger_id); const rider = profileMap.get(row.rider_id); return { id: row.id, requestId: row.ride_request_id, passengerId: row.passenger_id, passengerName: passenger?.full_name ?? "Passenger", riderId: row.rider_id, riderName: rider?.full_name ?? "Rider", amount: centsToDollars(row.amount_cents), stripeFeeAmount: centsToDollars(row.stripe_fee_cents), riderPayoutAmount: centsToDollars(row.rider_payout_cents), status: row.status, providerReference: row.provider_reference ?? "", createdAt: row.created_at, updatedAt: row.updated_at }; } function mapFinanceAdjustmentFromDatabase(row, profileMap = new Map()) { const subject = profileMap.get(row.subject_id); return { id: row.id, subjectRole: row.subject_role, subjectId: row.subject_id, subjectName: subject?.full_name ?? subject?.email ?? "Account", rideRequestId: row.ride_request_id ?? "", settlementId: row.settlement_id ?? "", subscriptionPaymentId: row.subscription_payment_id ?? "", businessSubscriptionId: row.business_subscription_id ?? "", adjustmentType: row.adjustment_type, amountCents: row.amount_cents, amount: centsToDollars(row.amount_cents), currency: row.currency ?? "USD", reason: row.reason ?? "", status: row.status, provider: row.provider ?? "", providerReference: row.provider_reference ?? "", visibleToUser: row.visible_to_user !== false, adminId: row.admin_id ?? "", metadata: row.metadata ?? {}, processedAt: row.processed_at ?? null, createdAt: row.created_at, updatedAt: row.updated_at }; } function mapRiderDayPreferenceFromDatabase(preference, riderMap = new Map()) { const rider = riderMap.get(preference.rider_id); return { id: preference.id, riderId: preference.rider_id, riderName: rider?.name ?? rider?.full_name ?? "Rider", serviceDate: preference.service_date, country: preference.country, city: preference.city, originArea: preference.origin_area, regions: Array.isArray(preference.destination_regions) ? preference.destination_regions : [], showAllNearbyPickups: preference.show_all_nearby_pickups === true, updatesUsed: preference.updates_used, createdAt: preference.created_at, updatedAt: preference.updated_at }; } function subscriptionPaymentRpcBody(paymentRequest) { return { p_plan_type: paymentRequest.planType ?? "monthly", p_provider: paymentRequest.provider, p_payment_phone: paymentRequest.paymentPhone, p_provider_reference: paymentRequest.reference, p_amount_xaf: paymentRequest.amount }; } async function savePaymentRequestToSupabase(paymentRequest) { if (!hasSupabaseRuntime()) return paymentRequest; const payload = { rider_id: paymentRequest.riderId, plan_type: paymentRequest.planType ?? "monthly", amount_xaf: paymentRequest.amount, provider: paymentRequest.provider, payment_phone: paymentRequest.paymentPhone, provider_reference: paymentRequest.reference, status: "pending" }; const riderMap = new Map([[paymentRequest.riderId, { name: paymentRequest.riderName }]]); if (!subscriptionPaymentRpcUnavailable.submit) { try { const body = subscriptionPaymentRpcBody(paymentRequest); const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("rider_submit_subscription_payment_request", body), "Submitting the subscription payment reference", supabaseProfileSaveTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/rider_submit_subscription_payment_request", { method: "POST", body }), "Submitting the subscription payment reference", supabaseProfileSaveTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastSubscriptionPaymentSource = "subscription payment RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) return mapPaymentRequestFromDatabase(row, riderMap); } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; subscriptionPaymentRpcUnavailable.submit = true; logClientWarning("Subscription payment submit RPC is not installed yet. Falling back to direct table insert.", error); } } assertClientFallbackAllowed("Subscription payment reference submission", "supabase-subscription-payment-requests.sql"); lastSubscriptionPaymentSource = "direct payment request insert fallback"; if (!supabaseClient) { const data = await withSupabaseTimeout( supabaseRestRequest("/rest/v1/subscription_payment_requests", { method: "POST", body: payload, headers: { Prefer: "return=representation" } }), "Submitting the subscription payment reference", supabaseProfileSaveTimeoutMs ); return mapPaymentRequestFromDatabase(Array.isArray(data) ? data[0] : data, riderMap); } const { data, error } = await withSupabaseTimeout( supabaseClient .from("subscription_payment_requests") .insert(payload) .select("*") .single(), "Submitting the subscription payment reference", supabaseProfileSaveTimeoutMs ); if (error) throw error; return mapPaymentRequestFromDatabase(data, riderMap); } async function savePaymentAccountToSupabase(account) { if (!hasSupabaseRuntime()) return account; const body = { p_role: account.role, p_provider: account.provider, p_account_type: account.accountType, p_account_holder: account.accountHolder, p_account_last4: account.accountLast4, p_institution_name: account.institutionName, p_provider_reference: account.reference }; if (!paymentAccountRpcUnavailable) { try { const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("save_payment_account_setup", body), "Saving the payment account", optionalSupabaseRequestTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/save_payment_account_setup", { method: "POST", body }), "Saving the payment account", optionalSupabaseRequestTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastPaymentAccountSource = "payment account RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; return row?.id ? mapPaymentAccountFromDatabase(row, new Map([[account.userId, { full_name: account.userName }]])) : account; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; paymentAccountRpcUnavailable = true; logClientWarning("Payment account RPC is not installed yet. Falling back to direct payment account upsert.", error); } } assertClientFallbackAllowed("Payment account setup", "supabase-payment-accounts.sql"); lastPaymentAccountSource = "direct payment account upsert fallback"; const payload = { user_id: account.userId, role: account.role, provider: account.provider, account_type: account.accountType, account_holder: account.accountHolder, account_last4: account.accountLast4, institution_name: account.institutionName, provider_reference: account.reference, status: "linked", updated_at: new Date().toISOString() }; if (supabaseClient) { const { data, error } = await withSupabaseTimeout( supabaseClient .from("payment_accounts") .upsert(payload, { onConflict: "user_id,role" }) .select("*") .single(), "Saving the payment account", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return mapPaymentAccountFromDatabase(data, new Map([[account.userId, { full_name: account.userName }]])); } const data = await withSupabaseTimeout( supabaseRestRequest("/rest/v1/payment_accounts?on_conflict=user_id,role", { method: "POST", body: payload, headers: { Prefer: "resolution=merge-duplicates,return=representation" } }), "Saving the payment account", optionalSupabaseRequestTimeoutMs ); return mapPaymentAccountFromDatabase(Array.isArray(data) ? data[0] : data, new Map([[account.userId, { full_name: account.userName }]])); } async function saveBusinessAccountToSupabase(account) { if (!hasSupabaseRuntime()) return account; const body = { p_business_name: account.businessName, p_billing_email: account.billingEmail, p_business_category: account.businessCategory, p_business_address: account.businessAddress, p_contact_name: account.contactName, p_contact_phone: account.contactPhone, p_plan_code: normalizeBusinessPlanCode(account.planCode), p_referral_code: account.referralCode ?? "" }; if (businessAccountRpcUnavailable) { throw new Error("Business verification RPC is not installed yet. Run supabase-business-starter-service-fee.sql before creating production business accounts."); } if (!businessAccountRpcUnavailable) { try { const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("create_business_account", body), "Creating the business account", supabaseProfileSaveTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/create_business_account", { method: "POST", body, headers: { Prefer: "return=representation" } }), "Creating the business account", supabaseProfileSaveTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastBusinessAccountSource = "business account RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; return row?.id ? mapBusinessAccountFromDatabase(row, new Map([[account.ownerId, { full_name: account.ownerName }]])) : account; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; businessAccountRpcUnavailable = true; logClientWarning("Business account verification RPC is not installed yet.", error); throw new Error("Business verification RPC is not installed yet. Run supabase-business-starter-service-fee.sql before creating production business accounts."); } } throw new Error("Business verification RPC is not installed yet. Run supabase-business-starter-service-fee.sql before creating production business accounts."); } async function saveRiderDayPreferenceToSupabase(preference) { if (!hasSupabaseRuntime()) return preference; const body = { p_country: preference.country, p_city: preference.city, p_origin_area: preference.originArea, p_destination_regions: preference.regions }; if (!riderDayRegionsRpcUnavailable) { try { const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("rider_save_day_regions", body), "Saving today's rider regions", optionalSupabaseRequestTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/rider_save_day_regions", { method: "POST", body }), "Saving today's rider regions", optionalSupabaseRequestTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastRiderDayRegionsSource = "rider day regions RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; return row?.id ? mapRiderDayPreferenceFromDatabase(row, new Map([[preference.riderId, { name: preference.riderName }]])) : preference; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; riderDayRegionsRpcUnavailable = true; logClientWarning("Rider day regions RPC is not installed yet. Falling back to direct day-region upsert.", error); } } assertClientFallbackAllowed("Rider day-region setup", "supabase-rider-day-regions.sql"); lastRiderDayRegionsSource = "direct rider day-region upsert fallback"; const payload = { rider_id: preference.riderId, service_date: preference.serviceDate, country: preference.country, city: preference.city, origin_area: preference.originArea, destination_regions: preference.regions, show_all_nearby_pickups: preference.showAllNearbyPickups === true, updates_used: preference.updatesUsed, updated_at: new Date().toISOString() }; if (supabaseClient) { const { data, error } = await withSupabaseTimeout( supabaseClient .from("rider_day_preferences") .upsert(payload, { onConflict: "rider_id,service_date" }) .select("*") .single(), "Saving today's rider regions", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return mapRiderDayPreferenceFromDatabase(data, new Map([[preference.riderId, { name: preference.riderName }]])); } const data = await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rider_day_preferences?on_conflict=rider_id,service_date", { method: "POST", body: payload, headers: { Prefer: "resolution=merge-duplicates,return=representation" } }), "Saving today's rider regions", optionalSupabaseRequestTimeoutMs ); return mapRiderDayPreferenceFromDatabase(Array.isArray(data) ? data[0] : data, new Map([[preference.riderId, { name: preference.riderName }]])); } function paymentFormValues(type) { const prefix = type === "passenger" ? "passenger" : "rider"; const account = type === "passenger" ? state.passenger : currentRiderRecord(); const existingAccount = paymentAccountFor(type, account?.id); const provider = els[`${prefix}PaymentProvider`]?.value || (type === "rider" ? "stripe_connect" : "stripe"); const reference = els[`${prefix}PaymentReference`]?.value.trim() || existingAccount?.reference || `${type}-stripe-${account?.id ?? makeId("account")}`; return { id: existingAccount?.id ?? makeId("payacct"), userId: account?.id, userName: account?.name, role: type, provider, accountType: "bank_account", accountHolder: els[`${prefix}AccountHolder`].value.trim(), accountLast4: els[`${prefix}AccountLast4`].value.trim(), institutionName: els[`${prefix}BankName`].value.trim(), reference, status: "linked", createdAt: existingAccount?.createdAt ?? new Date().toISOString(), updatedAt: new Date().toISOString() }; } async function savePaymentSetup(type, event) { event.preventDefault(); const account = type === "passenger" ? state.passenger : currentRiderRecord(); const status = type === "passenger" ? els.passengerPaymentStatus : els.riderPaymentStatus; if (!account || !hasSignedIn(type)) { status.textContent = "Sign in before saving a payment account."; return; } const paymentAccount = paymentFormValues(type); if (!paymentAccount.institutionName || !paymentAccount.accountHolder || !/^\d{4}$/.test(paymentAccount.accountLast4) || paymentAccount.reference.length < 4) { status.textContent = type === "rider" ? "Enter account holder, bank or payout account name, and last 4 digits." : "Enter account holder, bank or processor, last 4 digits, and reference."; return; } try { status.textContent = "Saving payment account..."; const savedAccount = await savePaymentAccountToSupabase(paymentAccount); state.paymentAccounts = upsertById( state.paymentAccounts.filter((item) => !(item.role === type && item.userId === account.id)), savedAccount ); saveState(); renderAll(); status.textContent = paymentAccountSummary(type, account); } catch (error) { status.textContent = `Payment account was not saved: ${error.message}`; } } async function startPaymentMethodSetup(type) { if (!hasSupabaseRuntime()) { throw new Error("Stripe payment setup requires the Supabase staging or production runtime."); } const body = { role: type }; const token = await currentSupabaseAccessToken(); if (!token) throw new Error("Sign in before opening Stripe setup."); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/payment-method-setup-start`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify(body) }), "Starting Stripe payment setup", supabaseProfileSaveTimeoutMs ); const responsePayload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(responsePayload?.error || "Stripe payment setup Edge Function failed."); if (!responsePayload?.url) throw new Error("Stripe did not return a hosted setup URL."); return responsePayload; } async function startPassengerPaymentSetup(event) { event.preventDefault(); if (!state.passenger || !hasSignedIn("passenger")) { els.passengerPaymentStatus.textContent = "Sign in as a passenger before opening payment setup."; return; } if (directRidePaymentMode()) { els.passengerPaymentStatus.textContent = paymentAccountSummary("passenger", state.passenger); if (els.passengerRideGate) { els.passengerRideGate.textContent = "Ready to publish with cash, MTN Mobile Money, or Orange Money."; } return; } let setupWindow = null; try { setupWindow = window.open("", "wakaStripeCardSetup"); if (setupWindow) { setupWindow.document.title = "Opening Stripe"; setupWindow.document.body.innerHTML = '

Opening secure Stripe setup

Keep Waka open in the original tab. This window will continue to Stripe.

'; } } catch { setupWindow = null; } try { setButtonBusy(els.startPassengerPaymentSetup, true); els.passengerPaymentStatus.textContent = "Opening secure Stripe card setup..."; const checkout = await startPaymentMethodSetup("passenger"); rememberPendingPaymentSetup("passenger", checkout.providerReference); schedulePendingPaymentSetupConfirmation(); if (setupWindow && !setupWindow.closed) { els.passengerPaymentStatus.textContent = "Stripe card setup opened in a separate tab. Finish there, then return here; Waka will link the saved card automatically."; setupWindow.location.href = checkout.url; setupWindow.focus(); } else { els.passengerPaymentStatus.textContent = "Popup was blocked. Redirecting this tab to secure Stripe card setup..."; window.location.assign(checkout.url); } } catch (error) { if (setupWindow && !setupWindow.closed) setupWindow.close(); if (paymentSetupRelaxedForTesting()) { try { els.passengerPaymentStatus.textContent = `Stripe setup could not open: ${error.message}. Staging is linking a test payment method...`; const account = state.passenger; const stagingAccount = { id: paymentAccountFor("passenger", account.id)?.id ?? makeId("payacct"), userId: account.id, userName: account.name, role: "passenger", provider: "stripe-test", accountType: "test_card", accountHolder: account.name, accountLast4: "4242", institutionName: "Stripe test mode", reference: `staging-test-${account.id}`, status: "linked", createdAt: paymentAccountFor("passenger", account.id)?.createdAt ?? new Date().toISOString(), updatedAt: new Date().toISOString() }; let savedAccount = stagingAccount; let localOnly = false; try { savedAccount = await savePaymentAccountToSupabase(stagingAccount); } catch (saveError) { localOnly = true; logClientWarning("Staging test payment method could not be saved to Supabase; keeping it local for pilot testing.", saveError); } state.paymentAccounts = upsertById( state.paymentAccounts.filter((item) => !(item.role === "passenger" && item.userId === account.id)), savedAccount ); state.passengerPage = "payment"; saveState(); renderAll(); if (els.passengerPaymentStatus) { els.passengerPaymentStatus.textContent = localOnly ? `Staging test payment method is linked on this device because Stripe setup returned: ${error.message}` : `Staging test payment method is linked because Stripe setup returned: ${error.message}`; } if (els.passengerRideGate) { els.passengerRideGate.textContent = "Passenger payment method is ready for staging."; } return; } catch (fallbackError) { els.passengerPaymentStatus.textContent = `Stripe setup failed, and the staging test setup could not be linked: ${fallbackError.message}`; return; } } els.passengerPaymentStatus.textContent = `Could not open Stripe setup: ${error.message}`; } finally { setButtonBusy(els.startPassengerPaymentSetup, false); } } async function startBusinessSubscriptionCheckout(accountId) { const account = passengerBusinessAccounts().find((item) => item.id === accountId); if (!account) { els.businessAccountStatus.textContent = "Business account was not found."; return; } try { els.businessAccountStatus.textContent = `Opening Business Partner checkout for ${account.businessName}...`; const checkout = await startSubscriptionCheckout("business_subscription", account.id); els.businessAccountStatus.textContent = "Business Partner checkout opened. Stripe will honor any active free business month before the monthly Partner charge starts; Partner waives the 10% business ride fee."; window.location.assign(checkout.url); } catch (error) { els.businessAccountStatus.textContent = `Could not open business upgrade checkout: ${error.message}`; } } async function startSubscriptionCheckout(kind, entityId) { if (!hasSupabaseRuntime()) { throw new Error("Provider-hosted checkout requires the Supabase production runtime."); } const planKey = kind === "rider_subscription" ? selectedSubscriptionPlanKey() : undefined; const renewalMode = kind === "rider_subscription" ? selectedSubscriptionRenewalMode() : undefined; const body = kind === "business_subscription" ? { kind, businessAccountId: entityId } : { kind, riderId: entityId, plan: planKey, renewalMode }; let responsePayload = null; if (supabaseClient?.functions?.invoke) { const { data, error } = await withSupabaseTimeout( supabaseClient.functions.invoke("subscription-checkout-start", { body }), "Starting provider-hosted checkout", supabaseProfileSaveTimeoutMs ); if (error) throw error; responsePayload = data; } else { const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/subscription-checkout-start`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${supabaseRestSession?.access_token || appConfig.supabaseAnonKey}`, "content-type": "application/json" }, body: JSON.stringify(body) }), "Starting provider-hosted checkout", supabaseProfileSaveTimeoutMs ); responsePayload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(responsePayload?.error || "Subscription checkout Edge Function failed."); } if (!responsePayload?.url) throw new Error("The payment provider did not return a subscription checkout URL."); lastSubscriptionPaymentSource = "subscription checkout Edge Function"; return responsePayload; } async function paySubscription() { const rider = currentRiderRecord(); if (!rider || rider.status !== "approved") return; if (directRidePaymentMode()) { if (els.subscriptionPaymentStatus) { els.subscriptionPaymentStatus.textContent = "Waka Cameroon uses rider wallet and commission review for this MVP. No subscription checkout is opened."; } return; } try { setTranslatedStatus(els.subscriptionPaymentStatus, "submittingPaymentSupabase"); const plan = selectedSubscriptionPlan(); const renewal = selectedSubscriptionRenewalMode(); const checkout = await startSubscriptionCheckout("rider_subscription", rider.id); saveState(); if (els.subscriptionPaymentStatus) { els.subscriptionPaymentStatus.textContent = `${plan.label} checkout opened for ${renewal === "automatic" ? "automatic renewal" : "manual upfront renewal"}.`; } else { setTranslatedStatus(els.subscriptionPaymentStatus, "paymentReferenceSubmitted"); } window.location.assign(checkout.url); } catch (error) { setTranslatedStatus(els.subscriptionPaymentStatus, "paymentReferenceFailed", { message: error.message }); } } // Ride request, offer negotiation, lifecycle, chat, safety report, rating, and tip flows. let rideLifecycleRpcUnavailable = false; let lastRideLifecycleSource = "not used"; const rideLifecycleActionInFlight = new Set(); let marketplaceActionRpcUnavailable = { fare: false, offer: false, rejection: false, selection: false, chat: false }; const contactProfilePhotoUrlCache = new Map(); const lifecycleGpsMaxAccuracyMeters = 150; const pickupArrivalMaxDistanceMeters = 200; const stopArrivalMaxDistanceMeters = 300; const destinationCompletionMaxDistanceMeters = 300; function lifecycleGpsAccuracyLimitMeters() { return lifecycleGpsMaxAccuracyMeters; } function lifecycleDistanceLimitMeters(actionName) { if (actionName === "arrive") return pickupArrivalMaxDistanceMeters; if (actionName === "stop") return stopArrivalMaxDistanceMeters; return destinationCompletionMaxDistanceMeters; } const rideLifecycleSupabaseTimeoutMs = Math.max(optionalSupabaseRequestTimeoutMs, supabaseProfileSaveTimeoutMs); let lastMarketplaceActionSource = "not used"; let safetyReportRpcUnavailable = { submit: false, review: false }; let supportTicketRpcUnavailable = { submit: false, review: false }; let lastSafetyReportSource = "not used"; let lastSupportTicketSource = "not used"; function requestPayloadForSupabase(request) { const pickupLocation = gpsPointToDatabase(requestPickupGps(request)); const pickupGps = requestPickupGps(request); const payload = { passenger_id: request.passengerId, business_account_id: request.businessAccountId || null, country: request.country, city: request.city, pickup_area: request.pickupArea, pickup_description: request.pickupDescription, destination_area: request.destinationArea, destination: request.destination, destination_place_id: request.destinationPlaceId ?? null, destination_formatted_address: request.destinationFormattedAddress ?? null, destination_lat: request.destinationLatitude ?? null, destination_lng: request.destinationLongitude ?? null, vehicle_preference: request.vehicle, car_type_preference: normalizeCarTypePreference(request.carTypePreference), ride_stops: normalizeRideStops(request.rideStops), ride_stop_points: rideStopPointsForRoute(request.rideStops, request.rideStopPoints).filter(Boolean), estimated_distance_miles: request.estimatedDistanceMiles, estimated_travel_minutes: request.estimatedTravelMinutes, route_estimate_source: normalizedRouteEstimateSourceForDatabase(request.routeEstimateSource), route_estimate_provider: normalizedRouteEstimateProviderForDatabase(request.routeEstimateSource, request.routeEstimateProvider), route_estimate_cached: Boolean(request.routeEstimateCached), route_estimate_key: request.routeEstimateKey ?? null, route_estimate_destination_fingerprint: request.routeEstimateDestinationFingerprint ?? null, route_estimate_polyline: request.routeEstimatePolyline ?? null, route_estimate_created_at: request.routeEstimateCreatedAt ?? null, fare_offer_xaf: request.fareOffer, fare_mode: normalizePassengerFareMode(request.fareMode), payment_preference: paymentToDatabase(request.paymentPreference), scheduled_at: request.scheduledAt, rider_confirmation_status: request.riderConfirmationStatus, rider_confirmation_requested_at: request.riderConfirmationRequestedAt, rider_confirmed_at: request.riderConfirmedAt, released_at: request.releasedAt, status: request.status }; if (pickupLocation) payload.pickup_location = pickupLocation; if (pickupLocation && pickupGps?.accuracyMeters != null) payload.pickup_gps_accuracy_meters = pickupGps.accuracyMeters; if (pickupLocation && pickupGps?.capturedAt) payload.pickup_gps_captured_at = pickupGps.capturedAt; return payload; } function rideRequestRpcBody(request) { const pickupGps = requestPickupGps(request); return { p_country: request.country, p_city: request.city, p_business_account_id: request.businessAccountId || null, p_pickup_area: request.pickupArea, p_pickup_description: request.pickupDescription, p_destination_area: request.destinationArea, p_destination: request.destination, p_destination_place_id: request.destinationPlaceId ?? null, p_destination_formatted_address: request.destinationFormattedAddress ?? null, p_destination_lat: request.destinationLatitude ?? null, p_destination_lng: request.destinationLongitude ?? null, p_vehicle_preference: request.vehicle, p_car_type_preference: normalizeCarTypePreference(request.carTypePreference), p_ride_stops: normalizeRideStops(request.rideStops), p_ride_stop_points: rideStopPointsForRoute(request.rideStops, request.rideStopPoints).filter(Boolean), p_estimated_distance_miles: request.estimatedDistanceMiles, p_estimated_travel_minutes: request.estimatedTravelMinutes, p_route_estimate_source: normalizedRouteEstimateSourceForDatabase(request.routeEstimateSource), p_route_estimate_provider: normalizedRouteEstimateProviderForDatabase(request.routeEstimateSource, request.routeEstimateProvider), p_route_estimate_cached: Boolean(request.routeEstimateCached), p_route_estimate_key: request.routeEstimateKey ?? null, p_route_estimate_destination_fingerprint: request.routeEstimateDestinationFingerprint ?? null, p_route_estimate_polyline: request.routeEstimatePolyline ?? null, p_route_estimate_created_at: request.routeEstimateCreatedAt ?? null, p_fare_offer_xaf: request.fareOffer, p_fare_mode: normalizePassengerFareMode(request.fareMode), p_payment_preference: paymentToDatabase(request.paymentPreference), p_scheduled_at: request.scheduledAt, p_pickup_lat: pickupGps?.latitude ?? null, p_pickup_lng: pickupGps?.longitude ?? null, p_pickup_accuracy_meters: pickupGps?.accuracyMeters ?? null, p_pickup_captured_at: pickupGps?.capturedAt ?? null }; } async function saveRideRequestToSupabase(request) { if (!hasSupabaseRuntime()) return request; if (!rideRequestRpcUnavailable) { try { const body = rideRequestRpcBody(request); const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("passenger_create_ride_request", body), "Publishing the ride request", supabaseProfileSaveTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/passenger_create_ride_request", { method: "POST", body }), "Publishing the ride request", supabaseProfileSaveTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastRidePostSource = "ride request RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) { const savedRequest = mapRideRequestFromDatabase(row); await processRideRequestPushDelivery(savedRequest.id, { eventTypes: ["nearby_ride_request"] }); return savedRequest; } } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; rideRequestRpcUnavailable = true; logClientWarning("Ride request RPC is not installed yet. Falling back to direct table insert.", error); } } assertClientFallbackAllowed("Ride request publishing", "supabase-ride-request-rpc.sql"); lastRidePostSource = "direct ride request insert fallback"; if (!supabaseClient) { const data = await withSupabaseTimeout( supabaseRestRequest("/rest/v1/ride_requests", { method: "POST", body: requestPayloadForSupabase(request), headers: { Prefer: "return=representation" } }), "Publishing the ride request", supabaseProfileSaveTimeoutMs ); return mapRideRequestFromDatabase(Array.isArray(data) ? data[0] : data); } const { data, error } = await withSupabaseTimeout( supabaseClient .from("ride_requests") .insert(requestPayloadForSupabase(request)) .select("*") .single(), "Publishing the ride request", supabaseProfileSaveTimeoutMs ); if (error) throw error; return mapRideRequestFromDatabase(data); } function rideNotificationDeliveryFunctionName() { return String(appConfig.notificationDeliveryFunctionName || "notification-delivery").trim() || "notification-delivery"; } async function processRideRequestPushDelivery(requestId, { eventTypes = [] } = {}) { if (!requestId || !hasSupabaseRuntime()) return null; const functionName = rideNotificationDeliveryFunctionName(); const scopedEventTypes = Array.isArray(eventTypes) ? [...new Set(eventTypes.map((type) => String(type || "").trim()).filter(Boolean))] : []; const body = { requestId, limit: 100 }; if (scopedEventTypes.length) body.eventTypes = scopedEventTypes; try { if (supabaseClient?.functions?.invoke) { const { data, error } = await withSupabaseTimeout( supabaseClient.functions.invoke(functionName, { body }), "Processing ride request phone push", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return data; } const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${functionName}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${supabaseRestSession?.access_token || appConfig.supabaseAnonKey}`, "content-type": "application/json" }, body: JSON.stringify(body) }), "Processing ride request phone push", optionalSupabaseRequestTimeoutMs ); const payload = await response.json().catch(() => null); if (!response.ok) throw new Error(payload?.error || "Notification delivery Edge Function failed."); return payload; } catch (error) { logClientWarning("Ride request phone push could not be processed immediately.", error); return { error: error.message }; } } async function updateRideRequestFareInSupabase(requestId, fareOffer) { const request = stateLookupIndexes().requestMap.get(requestId); if (request && Number(fareOffer) !== Number(request.fareOffer ?? 0) && typeof passengerCanSendFareProposal === "function" && !passengerCanSendFareProposal(request)) { throw new Error(fareProposalLimitMessage("passenger", request)); } if (!hasSupabaseRuntime()) return; if (!marketplaceActionRpcUnavailable.fare) { try { const body = { p_request_id: requestId, p_fare_offer_xaf: fareOffer }; const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("passenger_update_open_request_fare", body), "Updating the passenger fare", supabaseProfileSaveTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/passenger_update_open_request_fare", { method: "POST", body }), "Updating the passenger fare", supabaseProfileSaveTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastMarketplaceActionSource = "marketplace action RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) { await processRideRequestPushDelivery(row.id, { eventTypes: ["passenger_fare_increased"] }); return mapRideRequestFromDatabase(row); } throw new Error("Passenger fare RPC did not return an updated request."); } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; marketplaceActionRpcUnavailable.fare = true; logClientWarning("Passenger fare RPC is not installed yet. Fare updates need server-side request checks.", error); throw new Error("Run supabase-marketplace-actions-rpc.sql before fare updates in Supabase mode."); } } if (marketplaceActionRpcUnavailable.fare) { throw new Error("Run supabase-marketplace-actions-rpc.sql before fare updates in Supabase mode."); } } async function saveOfferToSupabase(offer) { if (!hasSupabaseRuntime()) return offer; if (!marketplaceActionRpcUnavailable.offer) { try { const body = { p_ride_request_id: offer.requestId, p_fare_xaf: offer.fare, p_type: offer.type, p_public_note: offer.note || null }; const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("rider_save_offer", body), "Saving the rider offer", supabaseProfileSaveTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/rider_save_offer", { method: "POST", body }), "Saving the rider offer", supabaseProfileSaveTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastMarketplaceActionSource = "marketplace action RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) return mapOfferFromDatabase(row); throw new Error("Rider offer RPC did not return a saved offer."); } catch (error) { if (areaProximityRpcMissing(error)) areaProximityRpcUnavailable = true; if (!adminDirectoryRpcMissing(error)) throw error; marketplaceActionRpcUnavailable.offer = true; logClientWarning( areaProximityRpcMissing(error) ? "Server-side area proximity helper is not installed yet. Rider offers need server-side proximity checks." : "Rider offer RPC is not installed yet. Offer submission needs server-side proximity checks.", error ); throw new Error(areaProximityRpcMissing(error) ? "Run supabase-area-proximity.sql and supabase-marketplace-actions-rpc.sql before rider offers in Supabase mode." : "Run supabase-marketplace-actions-rpc.sql before rider offers in Supabase mode."); } } if (marketplaceActionRpcUnavailable.offer) { throw new Error("Run supabase-marketplace-actions-rpc.sql before rider offers in Supabase mode."); } } async function withdrawRiderOfferFromSupabase(requestId) { if (!hasSupabaseRuntime()) return null; const body = { p_ride_request_id: requestId }; const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("rider_withdraw_offer", body), "Leaving the rider negotiation", supabaseProfileSaveTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/rider_withdraw_offer", { method: "POST", body }), "Leaving the rider negotiation", supabaseProfileSaveTimeoutMs ); if (supabaseClient && data.error) throw data.error; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) { await processRideRequestPushDelivery(row.id, { eventTypes: ["rider_offer_withdrawn"] }); return mapRideRequestFromDatabase(row); } return null; } async function rejectRiderOfferInSupabase(offerId) { if (!hasSupabaseRuntime()) return null; if (!marketplaceActionRpcUnavailable.rejection) { try { const body = { p_offer_id: offerId }; const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("passenger_reject_rider_offer", body), "Rejecting the rider offer", supabaseProfileSaveTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/passenger_reject_rider_offer", { method: "POST", body }), "Rejecting the rider offer", supabaseProfileSaveTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastMarketplaceActionSource = "marketplace action RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) { await processRideRequestPushDelivery(row.id, { eventTypes: ["passenger_rejected_offer"] }); return mapRideRequestFromDatabase(row); } return null; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; marketplaceActionRpcUnavailable.rejection = true; logClientWarning("Passenger rider-offer rejection RPC is not installed yet. Rejection needs server-side rider suppression.", error); throw new Error("Run supabase-marketplace-actions-rpc.sql before rejecting rider offers in Supabase mode."); } } throw new Error("Run supabase-marketplace-actions-rpc.sql before rejecting rider offers in Supabase mode."); } async function chooseOfferInSupabase(request, offer) { if (!hasSupabaseRuntime()) return; if (!marketplaceActionRpcUnavailable.selection) { try { const body = { p_offer_id: offer.id }; const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("passenger_select_rider_offer", body), "Choosing the rider offer", supabaseProfileSaveTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/passenger_select_rider_offer", { method: "POST", body }), "Choosing the rider offer", supabaseProfileSaveTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastMarketplaceActionSource = "marketplace action RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) return mapRideRequestFromDatabase(row, new Map(), new Map([[offer.id, offer]])); return null; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; marketplaceActionRpcUnavailable.selection = true; logClientWarning("Passenger rider-selection RPC is not installed yet. Rider selection needs server-side proximity checks.", error); throw new Error("Run supabase-marketplace-actions-rpc.sql before choosing riders in Supabase mode."); } } if (marketplaceActionRpcUnavailable.selection) { throw new Error("Run supabase-marketplace-actions-rpc.sql before choosing riders in Supabase mode."); } } function currentActorIdForChat() { if (activeRole() === "passenger") return state.sessions.passenger?.userId ?? state.passenger?.id; if (activeRole() === "rider") return state.sessions.rider?.userId ?? state.rider?.id; return state.adminSession?.userId ?? null; } function chatMessageShouldNotifyCounterparty(message) { return Boolean(message?.requestId && message.sender !== "system" && String(message.text ?? "").trim()); } async function saveChatMessageToSupabase(message, { throwOnError = false } = {}) { if (!hasSupabaseRuntime()) return; const senderId = message.senderId || currentActorIdForChat(); if (!senderId) { const error = new Error("Sign in before sending chat messages."); if (throwOnError) throw error; return null; } const body = message.sender === "system" ? `[System] ${message.systemPayload ?? message.text}` : message.text; try { if (!marketplaceActionRpcUnavailable.chat) { try { const rpcBody = { p_ride_request_id: message.requestId, p_body: body, p_sender_role: message.sender === "rider" ? "rider" : message.sender === "passenger" ? "passenger" : null }; const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("save_ride_chat_message", rpcBody), "Saving the chat message", optionalSupabaseRequestTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/save_ride_chat_message", { method: "POST", body: rpcBody }), "Saving the chat message", optionalSupabaseRequestTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastMarketplaceActionSource = "marketplace action RPC"; if (chatMessageShouldNotifyCounterparty(message)) { void processRideRequestPushDelivery(message.requestId, { eventTypes: ["ride_chat_message"] }); } const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; return row?.id ? mapChatFromDatabase(row) : null; } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; marketplaceActionRpcUnavailable.chat = true; logClientWarning("Ride chat RPC is not installed yet. Falling back to direct chat insert.", error); } } const payload = { ride_request_id: message.requestId, sender_id: senderId, sender_role: message.sender === "rider" ? "rider" : message.sender === "passenger" ? "passenger" : null, body }; assertClientFallbackAllowed("Ride chat message save", "supabase-marketplace-actions-rpc.sql"); lastMarketplaceActionSource = "direct table write fallback"; if (!supabaseClient) { await withSupabaseTimeout( supabaseRestRequest("/rest/v1/ride_chats", { method: "POST", body: payload, headers: { Prefer: "return=minimal" } }), "Saving the chat message", optionalSupabaseRequestTimeoutMs ); return null; } const { error } = await withSupabaseTimeout( supabaseClient.from("ride_chats").insert(payload), "Saving the chat message", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return null; } catch (error) { logClientWarning("Chat message was not synced to Supabase.", error); if (throwOnError) throw error; return null; } } function contactRelayFunctionName() { return String(appConfig.contactRelayFunctionName || "ride-contact-relay").trim() || "ride-contact-relay"; } function ridePaymentSettlementFunctionName() { return String(appConfig.ridePaymentSettlementFunctionName || "ride-payment-settlement").trim() || "ride-payment-settlement"; } async function processRidePaymentSettlement(requestId) { if (!hasSupabaseRuntime()) return null; const token = await currentSupabaseAccessToken(); if (!token) throw new Error("Sign in before processing the ride payment."); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${ridePaymentSettlementFunctionName()}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ requestId }) }), "Processing completed ride payment", supabaseProfileSaveTimeoutMs ); const payload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(payload?.error || "Stripe ride payment settlement failed."); return payload; } async function relayRideChatMessageToPhone(message) { if (!hasSupabaseRuntime() || message.sender === "system") return; const token = await currentSupabaseAccessToken(); if (!token) return; try { const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${contactRelayFunctionName()}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ channel: "sms", rideRequestId: message.requestId, message: message.text }) }), "Sending Waka SMS relay", optionalSupabaseRequestTimeoutMs ); const payload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(payload?.error || "Waka SMS relay failed."); if (els.chatStatus) els.chatStatus.textContent = "Open - SMS relay sent"; } catch (error) { if (els.chatStatus) { els.chatStatus.textContent = /not configured|missing|provider/i.test(String(error.message)) ? "Open - in-app sent; SMS relay not configured" : "Open - in-app sent; SMS relay failed"; } logClientWarning("Waka SMS relay was not sent.", error); } } async function saveSafetyReportToSupabase(report) { if (!hasSupabaseRuntime()) return report; const payload = { ride_request_id: report.requestId, reporter_id: report.reporterId, reporter_role: report.reporterRole, reported_user_id: report.reportedUserId, category: report.category, severity: report.severity, details: report.details, status: "open" }; const preserveDisplayFields = (savedReport) => ({ ...savedReport, reporterName: report.reporterName ?? savedReport.reporterName, reportedUserName: report.reportedUserName ?? savedReport.reportedUserName, routeSummary: report.routeSummary ?? savedReport.routeSummary }); if (!safetyReportRpcUnavailable.submit) { try { const rpcBody = { p_ride_request_id: report.requestId, p_reported_user_id: report.reportedUserId, p_category: report.category, p_severity: report.severity, p_details: report.details }; const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("submit_safety_report", rpcBody), "Submitting the safety report", optionalSupabaseRequestTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/submit_safety_report", { method: "POST", body: rpcBody }), "Submitting the safety report", optionalSupabaseRequestTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastSafetyReportSource = "safety report RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) return preserveDisplayFields(mapSafetyReportFromDatabase(row)); } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; safetyReportRpcUnavailable.submit = true; logClientWarning("Safety report submit RPC is not installed yet. Falling back to direct table insert.", error); } } assertClientFallbackAllowed("Safety report submission", "supabase-safety-reports.sql"); lastSafetyReportSource = "direct safety report insert fallback"; if (!supabaseClient) { const data = await withSupabaseTimeout( supabaseRestRequest("/rest/v1/safety_reports", { method: "POST", body: payload, headers: { Prefer: "return=representation" } }), "Submitting the safety report", optionalSupabaseRequestTimeoutMs ); return preserveDisplayFields(mapSafetyReportFromDatabase(Array.isArray(data) ? data[0] : data)); } const { data, error } = await withSupabaseTimeout( supabaseClient .from("safety_reports") .insert(payload) .select("*") .single(), "Submitting the safety report", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return preserveDisplayFields(mapSafetyReportFromDatabase(data)); } async function saveSupportTicketToSupabase(ticket) { if (!hasSupabaseRuntime()) return ticket; const payload = { account_id: ticket.accountId, account_role: ticket.accountRole, category: ticket.category, subject: ticket.subject, message: ticket.message, priority: ticket.priority, status: "open" }; const preserveDisplayFields = (savedTicket) => ({ ...savedTicket, accountName: ticket.accountName ?? savedTicket.accountName }); if (!supportTicketRpcUnavailable.submit) { try { const rpcBody = { p_category: ticket.category, p_subject: ticket.subject, p_message: ticket.message, p_priority: ticket.priority }; const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("submit_support_ticket", rpcBody), "Submitting the support request", optionalSupabaseRequestTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/submit_support_ticket", { method: "POST", body: rpcBody }), "Submitting the support request", optionalSupabaseRequestTimeoutMs ); if (supabaseClient && data.error) throw data.error; lastSupportTicketSource = "support ticket RPC"; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) return preserveDisplayFields(mapSupportTicketFromDatabase(row)); } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; supportTicketRpcUnavailable.submit = true; logClientWarning("Support ticket RPC is not installed yet. Falling back to direct table insert.", error); } } assertClientFallbackAllowed("Support ticket submission", "supabase-support-tickets.sql"); lastSupportTicketSource = "direct support ticket insert fallback"; if (!supabaseClient) { const data = await withSupabaseTimeout( supabaseRestRequest("/rest/v1/support_tickets", { method: "POST", body: payload, headers: { Prefer: "return=representation" } }), "Submitting the support request", optionalSupabaseRequestTimeoutMs ); return preserveDisplayFields(mapSupportTicketFromDatabase(Array.isArray(data) ? data[0] : data)); } const { data, error } = await withSupabaseTimeout( supabaseClient .from("support_tickets") .insert(payload) .select("*") .single(), "Submitting the support request", optionalSupabaseRequestTimeoutMs ); if (error) throw error; return preserveDisplayFields(mapSupportTicketFromDatabase(data)); } async function saveRideRatingToSupabase(rating) { if (!hasSupabaseRuntime()) return rating; await ensureRideRatingReviewerSession(rating); const legacyRpcBody = { p_ride_request_id: rating.requestId, p_rated_user_id: rating.ratedUserId, p_score: rating.score, p_comment: rating.comment }; const rpcBody = { ...legacyRpcBody, p_safety_score: rating.safetyScore ?? rating.score, p_punctuality_score: rating.punctualityScore ?? rating.score, p_communication_score: rating.communicationScore ?? rating.score, p_vehicle_score: rating.vehicleScore ?? rating.score }; const submitRatingRpc = async (functionName, body) => { const result = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc(functionName, body), "Submitting the ride rating", optionalSupabaseRequestTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest(`/rest/v1/rpc/${functionName}`, { method: "POST", body }), "Submitting the ride rating", optionalSupabaseRequestTimeoutMs ); if (supabaseClient && result.error) throw result.error; return result; }; try { let data; try { data = await submitRatingRpc("submit_ride_rating_v2", rpcBody); } catch (error) { if (!adminDirectoryRpcMissing(error)) throw error; data = await submitRatingRpc("submit_ride_rating", legacyRpcBody); } if (supabaseClient && data.error) throw data.error; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) return mapRideRatingFromDatabase(row); throw new Error("Ride rating RPC did not return a saved rating."); } catch (error) { if (adminDirectoryRpcMissing(error)) { logClientWarning("Ride rating RPC is not installed yet. Ratings need server-side completed-ride and counterparty checks.", error); throw new Error("Run supabase-ride-ratings.sql before ride ratings in Supabase mode."); } throw error; } } function rideRatingRoleLabel(role = activeRole()) { return role === "rider" ? "rider" : "passenger"; } async function ensureRideRatingReviewerSession(rating) { if (!hasSupabaseRuntime()) return; const reviewerId = String(rating?.reviewerId ?? "").trim(); const roleLabel = rideRatingRoleLabel(rating?.reviewerRole); if (!reviewerId) { throw new Error(`Sign in again as the ${roleLabel} before submitting this rating.`); } if (typeof getSupabaseUser !== "function") return; const user = await getSupabaseUser(); if (!user?.id) { throw new Error(`Sign in again as the ${roleLabel} before submitting this rating.`); } if (String(user.id) !== reviewerId) { throw new Error(`This browser is currently signed in as another Waka account. Sign in again as the ${roleLabel} for this completed ride, then submit the rating.`); } } async function saveRideTipToSupabase(tip) { if (!hasSupabaseRuntime()) return tip; const rpcBody = { p_ride_request_id: tip.requestId, p_tip_amount_cents: dollarsToCents(tip.amount) }; try { const data = supabaseClient ? await withSupabaseTimeout( supabaseClient.rpc("passenger_tip_rider", rpcBody), "Submitting the rider tip", optionalSupabaseRequestTimeoutMs ) : await withSupabaseTimeout( supabaseRestRequest("/rest/v1/rpc/passenger_tip_rider", { method: "POST", body: rpcBody }), "Submitting the rider tip", optionalSupabaseRequestTimeoutMs ); if (supabaseClient && data.error) throw data.error; const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data; if (row?.id) return mapRideTipFromDatabase(row); throw new Error("Passenger tip RPC did not return a saved tip."); } catch (error) { if (adminDirectoryRpcMissing(error)) { logClientWarning("Ride tip RPC is not installed yet. Tips need server-side completion and payout checks.", error); throw new Error("Run supabase-ride-lifecycle.sql before passenger tips in Supabase mode."); } throw error; } } async function updateRideDestinationInSupabase(request, nextDestination, guidance, nextStops = request.rideStops, destinationPlace = null, nextStopPoints = rideStopPointsForRoute(nextStops, request.rideStopPoints)) { if (!hasSupabaseRuntime()) return null; const destinationChanged = ![request.destination, request.destinationFormattedAddress] .some((value) => String(value ?? "").trim().toLowerCase() === String(nextDestination ?? "").trim().toLowerCase()); const existingPlace = destinationChanged ? null : normalizedPlaceSelection({ placeId: request.destinationPlaceId, displayName: request.destination, formattedAddress: request.destinationFormattedAddress, latitude: request.destinationLatitude, longitude: request.destinationLongitude }); const selectedPlace = normalizedPlaceSelection(destinationPlace) ?? existingPlace; const nextDestinationArea = destinationAreaForPublish( request.country, request.city, request.destinationArea, guidance?.destinationFormattedAddress ?? selectedPlace?.formattedAddress ?? nextDestination, selectedPlace ); const body = { p_request_id: request.id, p_destination_area: nextDestinationArea, p_destination: nextDestination, p_destination_place_id: guidance?.destinationPlaceId ?? selectedPlace?.placeId ?? null, p_destination_formatted_address: guidance?.destinationFormattedAddress ?? selectedPlace?.formattedAddress ?? null, p_destination_lat: guidance?.destinationLatitude ?? selectedPlace?.latitude ?? null, p_destination_lng: guidance?.destinationLongitude ?? selectedPlace?.longitude ?? null, p_ride_stops: normalizeRideStops(nextStops), p_ride_stop_points: normalizeRideStopPoints(nextStopPoints, nextStops), p_estimated_distance_miles: guidance?.distanceMiles ?? request.estimatedDistanceMiles ?? null, p_estimated_travel_minutes: guidance?.minutes ?? request.estimatedTravelMinutes ?? null, p_route_estimate_source: normalizedRouteEstimateSourceForDatabase(guidance?.source ?? request.routeEstimateSource), p_route_estimate_provider: normalizedRouteEstimateProviderForDatabase(guidance?.source ?? request.routeEstimateSource, guidance?.provider ?? request.routeEstimateProvider), p_route_estimate_cached: Boolean(guidance?.cached ?? request.routeEstimateCached), p_route_estimate_key: guidance?.routeKey ?? request.routeEstimateKey ?? null, p_route_estimate_destination_fingerprint: guidance?.destinationFingerprint ?? request.routeEstimateDestinationFingerprint ?? null, p_route_estimate_polyline: guidance?.routePolyline ?? request.routeEstimatePolyline ?? null, p_route_estimate_created_at: guidance?.estimatedAt ?? request.routeEstimateCreatedAt ?? null }; const row = await callSupabaseRpc( "passenger_update_ride_destination", body, "Updating the ride destination", optionalSupabaseRequestTimeoutMs ); if (Array.isArray(row)) return row[0] ?? null; return row ?? null; } const routeChangeEventPrefix = "WAKA_ROUTE_CHANGE_EVENT "; function routeChangeTypeLabel(type) { return type === "add_stop" ? "added stop" : "final destination change"; } function routeChangeVerb(type) { return type === "add_stop" ? "add a stop" : "change the destination"; } function routeChangeSystemText(event) { const type = routeChangeTypeLabel(event.type); const additionalFare = formatMoney(event.additionalFare ?? 0, event.country); const totalFare = formatMoney(event.totalFare ?? 0, event.country); const destination = event.destination ? ` Destination: ${event.destination}.` : ""; const stopCount = normalizeRideStops(event.rideStops).length; const stopText = stopCount ? ` Stops on route: ${stopCount}.` : ""; if (event.action === "accepted") { return `Rider acknowledged the ${type}. The route is updated. Added fare: ${additionalFare}. New ride total: ${totalFare}.${destination}${stopText}`; } if (event.action === "declined") { return `Rider declined the ${type}. The agreed route and fare stay unchanged.`; } return `Passenger requested a ${type}. Rider must acknowledge to update the route, or decline to keep the current route. Added fare if accepted: ${additionalFare}. New ride total: ${totalFare}.${destination}${stopText}`; } function parseRouteChangeEventText(text) { const clean = String(text ?? "").replace(/^\[System\]\s*/i, "").trim(); if (!clean.startsWith(routeChangeEventPrefix)) return null; try { const event = JSON.parse(clean.slice(routeChangeEventPrefix.length)); if (!event || event.kind !== "route_change" || !event.id || !event.requestId) return null; return { event, message: routeChangeSystemText(event) }; } catch { return null; } } function routeChangeEventPayload(change, action, request = state.requests.find((item) => item.id === change.requestId)) { return { kind: "route_change", action, id: change.id, requestId: change.requestId, type: change.type, country: request?.country ?? change.country ?? defaultLaunchCountry(), destinationArea: change.destinationArea ?? routeChangeDestinationArea(request, change), destination: change.destination, destinationPlaceId: change.destinationPlaceId ?? null, destinationFormattedAddress: change.destinationFormattedAddress ?? null, destinationLatitude: change.destinationLatitude ?? null, destinationLongitude: change.destinationLongitude ?? null, rideStops: normalizeRideStops(change.rideStops), rideStopPoints: normalizeRideStopPoints(change.rideStopPoints, change.rideStops), routeEstimate: change.routeEstimate ?? null, routeDelta: change.routeDelta ?? null, additionalFare: Math.max(0, Number(change.additionalFare ?? 0) || 0), totalFare: Math.max(0, Number(change.totalFare ?? 0) || 0), acceptedRouteChangeFare: Math.max(0, Number(change.acceptedRouteChangeFare ?? request?.acceptedRouteChangeFare ?? 0) || 0), requestedAt: change.requestedAt ?? new Date().toISOString(), decidedAt: action === "proposed" ? null : new Date().toISOString(), passengerId: request?.passengerId ?? change.passengerId ?? null, riderId: selectedRiderIdForRequest(request) ?? change.riderId ?? null }; } function pushRouteChangeSystemEvent(change, action, request = state.requests.find((item) => item.id === change.requestId)) { const event = routeChangeEventPayload(change, action, request); const message = { id: makeId("chat"), requestId: event.requestId, sender: "system", text: routeChangeSystemText(event), systemPayload: `${routeChangeEventPrefix}${JSON.stringify(event)}`, routeChangeEvent: event, createdAt: new Date().toISOString() }; state.chats.push(message); void saveChatMessageToSupabase(message); return message; } function routeChangeRequestsForRequest(requestId) { return (state.routeChangeRequests ?? []) .filter((change) => change.requestId === requestId) .sort((a, b) => new Date(b.requestedAt ?? b.createdAt ?? 0) - new Date(a.requestedAt ?? a.createdAt ?? 0)); } function pendingRouteChangeForRequest(request) { if (!request?.id) return null; return routeChangeRequestsForRequest(request.id).find((change) => change.status === "pending") ?? null; } function requestWithPendingRouteChange(request, change = pendingRouteChangeForRequest(request)) { if (!request || !change || change.status !== "pending") return request; return { ...request, ...routeChangePatch(change, request), pendingRouteChangeId: change.id, pendingRouteChange: change }; } function riderVisibleRouteRequest(request) { return activeRole() === "rider" && requestIsActiveForCurrentRider(request) ? requestWithPendingRouteChange(request) : request; } function routeChangeNeedsRiderApproval(request) { return Boolean(request && ["matched", "arrived", "in_progress"].includes(request.status) && (selectedRiderIdForRequest(request) || requestHasSelectedOffer(request))); } function routeChangeEstimatedTotal(request, additionalFare) { const base = routeChangeNeedsRiderApproval(request) ? agreedFareForRequest(request) : Number(request?.fareOffer ?? 0); return Math.max(0, Number(base || 0) + Number(additionalFare || 0)); } function routeChangeBaseFare(request) { return routeChangeNeedsRiderApproval(request) ? agreedFareForRequest(request) : Number(request?.fareOffer ?? 0) || 0; } function routeChangeIsAfterPickup(request) { return request?.status === "in_progress"; } function routeChangeElapsedRideMinutes(request) { if (!routeChangeIsAfterPickup(request) || !request?.startedAt) return 0; const startedAt = new Date(request.startedAt).getTime(); if (!Number.isFinite(startedAt)) return 0; return Math.max(0, Math.ceil((Date.now() - startedAt) / 60000)); } function destinationChangeBillableMinutes(guidance, stops = []) { const distanceMiles = positiveNumberOrNull(guidance?.distanceMiles); const stopCount = normalizeRideStops(stops).length; const rawMinutes = positiveNumberOrNull(guidance?.minutes); return distanceMiles == null ? rawMinutes : Math.min( rawMinutes ?? Math.ceil(distanceMiles * 2.1), Math.max(1, Math.ceil(distanceMiles * routeChangeFareConfig.billableMinutesPerAddedMile + stopCount * fareGuidanceConfig.perStopMinutes)) ); } function destinationChangeReplacementFare(guidance, country, stops = []) { if (!guidance) return null; const distanceMiles = positiveNumberOrNull(guidance.distanceMiles); const billableMinutes = destinationChangeBillableMinutes(guidance, stops); const fareGuidance = distanceMiles == null ? null : fareGuidanceFromDistance(distanceMiles, billableMinutes, stops, { source: guidance.source, provider: guidance.provider, cached: guidance.cached, routeKey: guidance.routeKey, routePolyline: guidance.routePolyline, country, city: guidance.city, destinationFingerprint: guidance.destinationFingerprint, estimatedAt: guidance.estimatedAt }); const fare = Number(fareGuidance?.midpoint ?? guidance.midpoint ?? guidance.min ?? guidance.max); if (!Number.isFinite(fare) || fare <= 0) return null; return Math.max(minimumFareOffer(country), Math.ceil(fare)); } function routeChangeAdditionalFare(request, guidance, nextStops, type, baselineGuidance = null) { if (type === "change_destination" && !routeChangeIsAfterPickup(request)) { const replacementFare = destinationChangeReplacementFare(guidance, request?.country, nextStops); if (replacementFare != null) { return Math.ceil(replacementFare - routeChangeBaseFare(request)); } } const delta = routeChangeDeltaSummary(request, guidance, nextStops, baselineGuidance); const elapsedRideMinutes = type === "change_destination" ? routeChangeElapsedRideMinutes(request) : 0; const directCost = ( delta.addedMiles * fareGuidanceConfig.perMileUsd + delta.billableAddedMinutes * fareGuidanceConfig.perMinuteUsd * routeChangeFareConfig.trafficTimeChargeMultiplier + delta.addedStops * fareGuidanceConfig.perStopUsd + elapsedRideMinutes * fareGuidanceConfig.perMinuteUsd * routeChangeFareConfig.trafficTimeChargeMultiplier ) * fareGuidanceConfig.fuelIndex; const baseFare = Math.max(0, agreedFareForRequest(request)); const proportionalCost = type !== "add_stop" && delta.baselineDistanceMiles != null && delta.nextDistanceMiles != null && delta.nextDistanceMiles > delta.baselineDistanceMiles ? baseFare * ((delta.nextDistanceMiles - delta.baselineDistanceMiles) / delta.baselineDistanceMiles) : 0; const minimum = type === "add_stop" ? routeChangeFareConfig.minStopFareUsd : routeChangeFareConfig.minAdditionalFareUsd; const surcharge = routeChangeIsAfterPickup(request) ? 1 + routeChangeFareConfig.afterPickupSurchargeRate : 1; return Math.max(1, Math.ceil(Math.max(directCost, proportionalCost, minimum) * surcharge)); } function routeChangeTotalFare(request, additionalFare, guidance, nextStops, type) { if (type === "change_destination" && !routeChangeIsAfterPickup(request)) { const replacementFare = destinationChangeReplacementFare(guidance, request?.country, nextStops); if (replacementFare != null) return replacementFare; } return routeChangeEstimatedTotal(request, additionalFare); } function routeChangeGuidanceIsConfirmed(guidance) { if (!guidance) return false; return normalizedRouteEstimateSourceForDatabase(guidance.source) === "google-routes"; } function fallbackGuidanceIsUsableForRouteChange(guidance, request) { return Boolean(guidance) && positiveNumberOrNull(request?.estimatedDistanceMiles) != null && positiveNumberOrNull(guidance?.distanceMiles) != null && positiveNumberOrNull(guidance?.minutes) != null; } function routeChangeFallbackGuidance(request, nextStops) { const storedDistance = positiveNumberOrNull(request?.estimatedDistanceMiles); const storedMinutes = positiveNumberOrNull(request?.estimatedTravelMinutes); if (storedDistance == null) return null; const currentStops = normalizeRideStops(request?.rideStops); const updatedStops = normalizeRideStops(nextStops); const addedStops = Math.max(0, updatedStops.length - currentStops.length); const adjustedDistanceMiles = storedDistance * (1 + addedStops * fareGuidanceConfig.stopDistanceMultiplier); const adjustedMinutes = (storedMinutes ?? Math.ceil(storedDistance * 2.1)) + addedStops * fareGuidanceConfig.perStopMinutes; return fareGuidanceFromDistance(adjustedDistanceMiles, adjustedMinutes, updatedStops, { source: "zone", provider: "stored-route-fallback", country: request?.country, city: request?.city }); } function routeChangeDestinationFallbackGuidance(request, destination, nextStops, destinationPlace = null) { const pickupGps = normalizeGpsPoint(requestPickupGps(request)); const selectedDestination = normalizedPlaceSelection(destinationPlace); const destinationPoint = validGpsCoordinate(Number(selectedDestination?.latitude), Number(selectedDestination?.longitude)) ? { latitude: Number(selectedDestination.latitude), longitude: Number(selectedDestination.longitude) } : null; if (pickupGps && destinationPoint) { const stopPoints = normalizeRideStops(nextStops).map(stopRoutePoint); const points = [pickupGps, ...stopPoints.filter(Boolean), destinationPoint]; const straightKm = points.slice(1).reduce((total, point, index) => ( total + gpsDistanceKmBetween(points[index], point) ), 0); if (Number.isFinite(straightKm) && straightKm > 0) { const missingStopPointCount = Math.max(0, normalizeRideStops(nextStops).length - stopPoints.filter(Boolean).length); const distanceMiles = Math.max(0.6, straightKm * riderPickupEtaRoadFactor * kmToMiles); const minutes = Math.max(3, Math.ceil(distanceMiles * 2.1 + missingStopPointCount * fareGuidanceConfig.perStopMinutes)); return fareGuidanceFromDistance(distanceMiles, minutes, nextStops, { source: "place-preview", provider: "local-route-change-preview", country: request?.country, city: request?.city }); } } const storedFallback = routeChangeFallbackGuidance(request, nextStops); return storedFallback ? { ...storedFallback, provider: "stored-route-destination-fallback" } : null; } function requireConfirmedRouteChangeGuidance(guidance, label, fallback = null, request = null) { if (!routeEstimatesEnabled()) return guidance; const safeGuidance = safeRouteGuidance(guidance, fallback, label.toLowerCase()); if (routeChangeGuidanceIsConfirmed(safeGuidance)) return safeGuidance; if (fallbackGuidanceIsUsableForRouteChange(fallback, request)) return fallback; throw new Error(`${label} could not be confirmed by Google Routes. Choose the pickup, stop, and destination from address suggestions, then try again.`); } function positiveNumberOrNull(value) { const number = Number(value); return Number.isFinite(number) && number > 0 ? number : null; } function routeChangeBaselineGuidance(request, guidance = null) { return { distanceMiles: positiveNumberOrNull(guidance?.distanceMiles) ?? positiveNumberOrNull(request?.estimatedDistanceMiles), minutes: positiveNumberOrNull(guidance?.minutes) ?? positiveNumberOrNull(request?.estimatedTravelMinutes), source: guidance?.source ?? request?.routeEstimateSource ?? null, provider: guidance?.provider ?? request?.routeEstimateProvider ?? null, cached: Boolean(guidance?.cached ?? request?.routeEstimateCached), routeKey: guidance?.routeKey ?? request?.routeEstimateKey ?? null, routePolyline: guidance?.routePolyline ?? request?.routeEstimatePolyline ?? null, destinationFingerprint: guidance?.destinationFingerprint ?? request?.routeEstimateDestinationFingerprint ?? null, estimatedAt: guidance?.estimatedAt ?? request?.routeEstimateCreatedAt ?? null }; } function routeChangeDeltaSummary(request, guidance, nextStops, baselineGuidance = null) { const baseline = routeChangeBaselineGuidance(request, baselineGuidance); const nextDistance = Number(guidance?.distanceMiles); const nextMinutes = Number(guidance?.minutes); const addedMiles = baseline.distanceMiles != null && Number.isFinite(nextDistance) ? Math.max(0, nextDistance - baseline.distanceMiles) : 0; const addedMinutes = baseline.minutes != null && Number.isFinite(nextMinutes) ? Math.max(0, nextMinutes - baseline.minutes) : 0; const previousStops = normalizeRideStops(request?.rideStops).length; const addedStops = Math.max(0, normalizeRideStops(nextStops).length - previousStops); const billableAddedMinutes = Math.min( addedMinutes, Math.max(0, Math.ceil( addedMiles * routeChangeFareConfig.billableMinutesPerAddedMile + addedStops * fareGuidanceConfig.perStopMinutes )) ); return { baselineDistanceMiles: baseline.distanceMiles, baselineMinutes: baseline.minutes, nextDistanceMiles: Number.isFinite(nextDistance) && nextDistance > 0 ? nextDistance : null, nextMinutes: Number.isFinite(nextMinutes) && nextMinutes > 0 ? nextMinutes : null, addedMiles, addedMinutes, billableAddedMinutes, addedStops, nextProvider: guidance?.provider ?? null, baselineSource: baseline.source, baselineProvider: baseline.provider }; } function routeChangeDistanceLine(request, delta) { const pieces = []; if (delta.baselineDistanceMiles != null) { pieces.push(`Current route: ${formatRouteDistanceForRequest(delta.baselineDistanceMiles, request)}`); } if (delta.nextDistanceMiles != null) { pieces.push(`Updated route: ${formatRouteDistanceForRequest(delta.nextDistanceMiles, request)}`); } pieces.push(`Added drive: ${formatRouteDistanceForRequest(delta.addedMiles, request)}`); if (delta.addedMinutes > 0) pieces.push(`Google traffic change: about ${Math.ceil(delta.addedMinutes)} minutes`); return pieces.join("\n"); } function routeChangeConfirmationMessage(request, type, delta, additionalFare, totalFare, actionLabel, guidance = null, nextStops = []) { const baseFare = routeChangeBaseFare(request); const lines = []; if (type === "change_destination" && !routeChangeIsAfterPickup(request)) { if (delta.nextDistanceMiles != null) { lines.push(`New route from pickup: ${formatRouteDistanceForRequest(delta.nextDistanceMiles, request)}`); } const billableMinutes = destinationChangeBillableMinutes(guidance ?? { distanceMiles: delta.nextDistanceMiles, minutes: delta.nextMinutes }, nextStops); if (billableMinutes != null) { lines.push(`Fare pricing time: about ${Math.ceil(billableMinutes)} minutes`); } lines.push(`Current fare: ${formatMoney(baseFare, request.country)}`); lines.push(`New fare: ${formatMoney(totalFare, request.country)}`); if (additionalFare > 0) { lines.push(`Fare increases by ${formatMoney(additionalFare, request.country)}`); } else if (additionalFare < 0) { lines.push(`Fare decreases by ${formatMoney(Math.abs(additionalFare), request.country)}`); } else { lines.push("Fare stays the same."); } lines.push(""); lines.push("Passenger has not been picked up yet, so Waka reprices the trip from pickup to the new destination."); } else { lines.push(routeChangeDistanceLine(request, delta)); const elapsedMinutes = routeChangeElapsedRideMinutes(request); if (type === "change_destination" && elapsedMinutes > 0) { lines.push(`Elapsed ride time considered: about ${elapsedMinutes} minutes`); } lines.push(`Added fare: ${formatMoney(additionalFare, request.country)}`); lines.push(`New ride total: ${formatMoney(totalFare, request.country)}`); if (routeChangeNeedsExtraReview(delta)) { lines.push(""); lines.push("This looks like a large route change. Check the stop or destination address before applying."); } } const heading = actionLabel === "Apply" ? "Apply route update" : `${actionLabel} ${routeChangeVerb(type)}`; return `${heading}?\n\n${lines.join("\n")}`; } function routeChangeNeedsExtraReview(delta) { return delta.addedMiles >= routeChangeFareConfig.detourReviewMiles || delta.addedMinutes >= routeChangeFareConfig.detourReviewMinutes; } function routeChangeStopNeedsReselection(type, delta) { return type === "add_stop" && routeChangeNeedsExtraReview(delta) && delta.nextProvider !== "stored-route-fallback"; } function routeChangePatch(change, request = null) { const routeEstimate = change.routeEstimate ?? {}; return { destinationArea: routeChangeDestinationArea(request, change), destination: change.destination ?? request?.destination, destinationPlaceId: change.destinationPlaceId ?? request?.destinationPlaceId ?? null, destinationFormattedAddress: change.destinationFormattedAddress ?? request?.destinationFormattedAddress ?? null, destinationLatitude: change.destinationLatitude ?? request?.destinationLatitude ?? null, destinationLongitude: change.destinationLongitude ?? request?.destinationLongitude ?? null, rideStops: normalizeRideStops(change.rideStops ?? request?.rideStops), rideStopPoints: normalizeRideStopPoints(change.rideStopPoints ?? request?.rideStopPoints, change.rideStops ?? request?.rideStops), estimatedDistanceMiles: routeEstimate.distanceMiles ?? request?.estimatedDistanceMiles ?? null, estimatedTravelMinutes: routeEstimate.minutes ?? request?.estimatedTravelMinutes ?? null, routeEstimateSource: routeEstimate.source ?? request?.routeEstimateSource ?? null, routeEstimateProvider: routeEstimate.provider ?? request?.routeEstimateProvider ?? null, routeEstimateCached: Boolean(routeEstimate.cached ?? request?.routeEstimateCached), routeEstimateKey: routeEstimate.routeKey ?? request?.routeEstimateKey ?? null, routeEstimatePolyline: routeEstimate.routePolyline ?? request?.routeEstimatePolyline ?? null, routeEstimateDestinationFingerprint: routeEstimate.destinationFingerprint ?? request?.routeEstimateDestinationFingerprint ?? null, routeEstimateCreatedAt: routeEstimate.estimatedAt ?? request?.routeEstimateCreatedAt ?? null }; } function routeChangeDestinationPlace(change = {}) { return normalizedPlaceSelection({ placeId: change.destinationPlaceId, displayName: change.destination, formattedAddress: change.destinationFormattedAddress || change.destination, latitude: change.destinationLatitude, longitude: change.destinationLongitude }); } function routeChangeDestinationArea(request, change = {}) { if (!request) return change?.destinationArea ?? null; return change?.destinationArea ?? destinationAreaForPublish( request.country, request.city, request.destinationArea, change.destinationFormattedAddress || change.destination || request.destination, routeChangeDestinationPlace(change) ); } function applyAcceptedRouteChange(change, savedRequest = null) { updateRequestById(change.requestId, (request) => { const acceptedFare = Math.max( Number(request.acceptedRouteChangeFare ?? 0) || 0, Number(change.acceptedRouteChangeFare ?? 0) || 0, Number(savedRequest?.accepted_route_change_fare ?? savedRequest?.acceptedRouteChangeFare ?? 0) || 0 ); return { ...request, ...routeChangePatch(change, request), ...(savedRequest?.id === change.requestId ? mapRideRequestFromDatabase(savedRequest, new Map(), stateLookupIndexes().offerMap) : {}), acceptedRouteChangeFare: savedRequest?.accepted_route_change_fare ?? acceptedFare }; }); } async function routeChangeGuidanceForRoute(request, destination, stops, pickupGps, pickupDescription, destinationPlace = null) { const guidanceKey = routeGuidanceInputKey( request.country, request.city, request.pickupArea, request.destinationArea, pickupDescription, destination, stops, pickupGps, destinationPlace ); let guidance = cachedConfirmedFareGuidanceForKey(guidanceKey); if (guidance) return guidance; if (fareGuidanceInFlightKey === guidanceKey) { guidance = await waitForConfirmedFareGuidance(guidanceKey); if (guidance) return guidance; } guidance = await accurateFareGuidanceForRide( request.country, request.city, request.pickupArea, request.destinationArea, destination, pickupGps, stops, pickupDescription, destinationPlace ); if (routeGuidanceConfirmedForPublish(guidance)) { lastRouteFareGuidance = guidance; lastRouteFareGuidanceKey = guidanceKey; } return guidance; } function upsertRouteChangeRequest(change) { state.routeChangeRequests = upsertById(state.routeChangeRequests ?? [], change); } function mergeRouteChangeEventsFromChats(messages = state.chats) { const events = messages .map((message) => message.routeChangeEvent ?? parseRouteChangeEventText(message.body ?? message.text)?.event) .filter((event) => event?.kind === "route_change" && event.id && event.requestId) .sort((a, b) => new Date(a.decidedAt ?? a.requestedAt ?? 0) - new Date(b.decidedAt ?? b.requestedAt ?? 0)); events.forEach((event) => { const existing = (state.routeChangeRequests ?? []).find((change) => change.id === event.id); const request = state.requests.find((item) => item.id === event.requestId); const baseChange = { ...(existing ?? {}), id: event.id, requestId: event.requestId, type: event.type, country: event.country, destinationArea: event.destinationArea ?? existing?.destinationArea ?? request?.destinationArea ?? null, destination: event.destination, destinationPlaceId: event.destinationPlaceId ?? null, destinationFormattedAddress: event.destinationFormattedAddress ?? null, destinationLatitude: event.destinationLatitude ?? null, destinationLongitude: event.destinationLongitude ?? null, rideStops: normalizeRideStops(event.rideStops), rideStopPoints: normalizeRideStopPoints(event.rideStopPoints, event.rideStops), routeEstimate: event.routeEstimate ?? null, routeDelta: event.routeDelta ?? null, additionalFare: Number(event.additionalFare ?? 0) || 0, totalFare: Number(event.totalFare ?? 0) || 0, acceptedRouteChangeFare: Number(event.acceptedRouteChangeFare ?? 0) || 0, requestedAt: event.requestedAt ?? existing?.requestedAt ?? new Date().toISOString(), riderId: event.riderId ?? existing?.riderId ?? selectedRiderIdForRequest(request), passengerId: event.passengerId ?? existing?.passengerId ?? request?.passengerId ?? null }; if (event.action === "declined") { upsertRouteChangeRequest({ ...baseChange, status: "declined", decidedAt: event.decidedAt ?? new Date().toISOString() }); return; } if (event.action === "accepted") { const wasAccepted = existing?.status === "accepted"; upsertRouteChangeRequest({ ...baseChange, status: "accepted", decidedAt: event.decidedAt ?? new Date().toISOString() }); if (!wasAccepted) applyAcceptedRouteChange(baseChange); return; } upsertRouteChangeRequest({ ...baseChange, status: existing?.status ?? "pending" }); }); } async function acceptRideRouteChangeInSupabase(request, change) { if (!hasSupabaseRuntime()) return null; const routeEstimate = change.routeEstimate ?? {}; const body = { p_request_id: request.id, p_change_id: change.id, p_destination_area: routeChangeDestinationArea(request, change), p_destination: change.destination, p_destination_place_id: change.destinationPlaceId ?? null, p_destination_formatted_address: change.destinationFormattedAddress ?? null, p_destination_lat: change.destinationLatitude ?? null, p_destination_lng: change.destinationLongitude ?? null, p_ride_stops: normalizeRideStops(change.rideStops), p_ride_stop_points: normalizeRideStopPoints(change.rideStopPoints, change.rideStops), p_estimated_distance_miles: routeEstimate.distanceMiles ?? request.estimatedDistanceMiles ?? null, p_estimated_travel_minutes: routeEstimate.minutes ?? request.estimatedTravelMinutes ?? null, p_route_estimate_source: normalizedRouteEstimateSourceForDatabase(routeEstimate.source ?? request.routeEstimateSource), p_route_estimate_provider: normalizedRouteEstimateProviderForDatabase(routeEstimate.source ?? request.routeEstimateSource, routeEstimate.provider ?? request.routeEstimateProvider), p_route_estimate_cached: Boolean(routeEstimate.cached ?? request.routeEstimateCached), p_route_estimate_key: routeEstimate.routeKey ?? request.routeEstimateKey ?? null, p_route_estimate_polyline: routeEstimate.routePolyline ?? request.routeEstimatePolyline ?? null, p_route_estimate_destination_fingerprint: routeEstimate.destinationFingerprint ?? request.routeEstimateDestinationFingerprint ?? null, p_route_estimate_created_at: routeEstimate.estimatedAt ?? request.routeEstimateCreatedAt ?? null, p_additional_fare_xaf: Math.max(0, Math.ceil(Number(change.additionalFare ?? 0) || 0)) }; const row = await callSupabaseRpcResult( "rider_accept_ride_route_change", body, "Accepting the route change", optionalSupabaseRequestTimeoutMs ); const saved = Array.isArray(row) ? row[0] ?? null : row ?? null; if (saved?.id) { await processRideRequestPushDelivery(request.id, { eventTypes: ["route_change_accepted"] }); } return saved; } async function requestRideRouteChangeInSupabase(request, change) { if (!hasSupabaseRuntime()) return null; const body = { p_request_id: request.id, p_change_id: change.id, p_change_type: change.type, p_destination: change.destination ?? request.destination, p_destination_place_id: change.destinationPlaceId ?? null, p_destination_formatted_address: change.destinationFormattedAddress ?? null, p_destination_lat: change.destinationLatitude ?? null, p_destination_lng: change.destinationLongitude ?? null, p_ride_stops: normalizeRideStops(change.rideStops), p_ride_stop_points: normalizeRideStopPoints(change.rideStopPoints, change.rideStops), p_route_estimate: change.routeEstimate ?? null, p_additional_fare_xaf: Math.max(0, Math.ceil(Number(change.additionalFare ?? 0) || 0)), p_total_fare_xaf: Math.max(0, Math.ceil(Number(change.totalFare ?? 0) || 0)), p_change_payload: routeChangeEventPayload(change, "proposed", request) }; let row = null; try { row = await callSupabaseRpcResult( "passenger_request_ride_route_change", body, "Sending the route change request", optionalSupabaseRequestTimeoutMs ); } catch (error) { const fallbackChange = await requestRideRouteChangeViaDestinationUpdateFallback(request, change, error); if (fallbackChange?.id) return fallbackChange; throw error; } const saved = Array.isArray(row) ? row[0] ?? null : row ?? null; if (saved?.id) { await processRideRequestPushDelivery(request.id, { eventTypes: ["route_change_requested"] }); } return saved; } async function requestRideRouteChangeViaDestinationUpdateFallback(request, change, originalError) { if (!hasSupabaseRuntime() || !request?.id || !change?.id) return null; logClientWarning("Route-change RPC failed; trying destination-update fallback so the rider still receives an approval request.", originalError); try { await updateRideDestinationInSupabase( request, change.destination ?? request.destination, change.routeEstimate ?? null, normalizeRideStops(change.rideStops), routeChangeDestinationPlace(change), normalizeRideStopPoints(change.rideStopPoints, change.rideStops) ); const result = typeof selectRideRouteChangesForRequests === "function" ? await selectRideRouteChangesForRequests([request.id]) : { data: [] }; const pendingChanges = (result.data ?? []) .map(mapRideRouteChangeFromDatabase) .filter((item) => item.requestId === request.id && item.status === "pending") .sort((a, b) => new Date(b.requestedAt ?? b.createdAt ?? 0) - new Date(a.requestedAt ?? a.createdAt ?? 0)); const fallbackChange = pendingChanges[0] ?? null; if (fallbackChange?.id) { await processRideRequestPushDelivery(request.id, { eventTypes: ["route_change_requested"] }); return fallbackChange; } } catch (fallbackError) { logClientWarning("Destination-update route-change fallback failed.", fallbackError); } return null; } async function declineRideRouteChangeInSupabase(request, change) { if (!hasSupabaseRuntime()) return null; const row = await callSupabaseRpcResult( "rider_decline_ride_route_change", { p_request_id: request.id, p_change_id: change.id }, "Declining the route change", optionalSupabaseRequestTimeoutMs ); const saved = Array.isArray(row) ? row[0] ?? null : row ?? null; if (saved?.id) { await processRideRequestPushDelivery(request.id, { eventTypes: ["route_change_declined"] }); } return saved; } async function acceptRouteChangeRequest(changeId) { const change = (state.routeChangeRequests ?? []).find((item) => item.id === changeId); const request = state.requests.find((item) => item.id === change?.requestId); if (!change || !request || !riderIdentityMatches(selectedRiderIdForRequest(request)) || change.status !== "pending") return; let savedRequest = null; try { savedRequest = await acceptRideRouteChangeInSupabase(request, change); } catch (error) { void showWakaGoodAlert(`Could not accept route change: ${error.message}`); return; } if (hasSupabaseRuntime() && !savedRequest) { void showWakaGoodAlert("Could not accept route change: Waka could not save the updated route. Refresh and try again."); return; } const acceptedRouteChangeFare = Math.max( Number(savedRequest?.accepted_route_change_fare ?? 0) || 0, (Number(request.acceptedRouteChangeFare ?? 0) || 0) + (Number(change.additionalFare ?? 0) || 0) ); const acceptedChange = { ...change, status: "accepted", decidedAt: new Date().toISOString(), acceptedRouteChangeFare }; upsertRouteChangeRequest(acceptedChange); applyAcceptedRouteChange(acceptedChange, savedRequest); const updatedRequest = state.requests.find((item) => item.id === request.id) ?? request; pushRouteChangeSystemEvent(acceptedChange, "accepted", updatedRequest); saveState(); renderAll(); if (updatedRequest.status === "in_progress" && typeof openRiderActiveRideNavigation === "function") { const navKey = riderActiveRideNavigationKey(updatedRequest, "rider-route-change-nav"); if (shouldOpenRiderActiveRideNavigation(updatedRequest, navKey)) openRiderActiveRideNavigation(updatedRequest, navKey); } void refreshMarketplace({ silent: true }); } async function declineRouteChangeRequest(changeId) { const change = (state.routeChangeRequests ?? []).find((item) => item.id === changeId); const request = state.requests.find((item) => item.id === change?.requestId); if (!change || !request || !riderIdentityMatches(selectedRiderIdForRequest(request)) || change.status !== "pending") return; let savedChange = null; try { savedChange = await declineRideRouteChangeInSupabase(request, change); } catch (error) { void showWakaGoodAlert(`Could not decline route change: ${error.message}`); return; } if (hasSupabaseRuntime() && !savedChange) { void showWakaGoodAlert("Could not decline route change: Waka could not save your decision. Refresh and try again."); return; } const declinedChange = { ...change, ...(savedChange ? mapRideRouteChangeFromDatabase(savedChange) : {}), status: "declined", decidedAt: new Date().toISOString() }; upsertRouteChangeRequest(declinedChange); pushRouteChangeSystemEvent(declinedChange, "declined", request); saveState(); renderAll(); void refreshMarketplace({ silent: true }); } async function submitDestinationUpdate(event, requestId) { event.preventDefault(); const form = event.currentTarget; const status = form.querySelector(".destination-update-status"); const input = form.querySelector(".destination-update-input"); const stopsInput = form.querySelector(".stops-update-input"); const routeChangeType = event.submitter?.dataset.routeChangeType === "stop" ? "add_stop" : "change_destination"; const request = state.requests.find((item) => item.id === requestId); if (!canUpdateRideDestination(request)) { status.textContent = "Route changes are closed for this ride."; return; } const currentStops = normalizeRideStops(request.rideStops); const currentStopPoints = normalizeRideStopPoints(request.rideStopPoints, currentStops); let nextDestination = request.destinationFormattedAddress || request.destination; let nextStops = currentStops; let nextStopPoints = currentStopPoints; const currentDestinationPlace = normalizedPlaceSelection({ placeId: request.destinationPlaceId, displayName: request.destination, formattedAddress: request.destinationFormattedAddress, latitude: request.destinationLatitude, longitude: request.destinationLongitude }); let destinationPlace = currentDestinationPlace; if (routeChangeType === "change_destination") { nextDestination = input.value.trim(); if (nextDestination.length < 3) { status.textContent = "Enter the new destination address before requesting the change."; return; } const destinationChanged = ![request.destination, request.destinationFormattedAddress] .some((value) => String(value ?? "").trim().toLowerCase() === nextDestination.toLowerCase()); if (!destinationChanged) { status.textContent = "Enter a destination that is different from the current one."; return; } destinationPlace = destinationPlaceMatchesInput(form.__destinationUpdatePlace, nextDestination) ? normalizedPlaceSelection(form.__destinationUpdatePlace) : null; } else { const addedStops = normalizeRideStops(stopsInput?.value ?? ""); if (!addedStops.length) { status.textContent = "Enter at least one stop address before requesting an added stop."; return; } nextStops = normalizeRideStops([...currentStops, ...addedStops]); if (nextStops.length === currentStops.length) { status.textContent = "That stop is already on this route."; return; } nextStopPoints = rideStopPointsForRoute(nextStops, currentStopPoints); } const fallbackGuidance = routeChangeType === "add_stop" ? routeChangeFallbackGuidance(request, nextStops) : routeChangeDestinationFallbackGuidance(request, nextDestination, nextStops, destinationPlace); let guidance = fallbackGuidance; try { let baselineGuidance = null; if (routeEstimatesEnabled()) { status.textContent = "Checking updated driving distance..."; const pickupGps = requestPickupGps(request); const accurateGuidance = await routeChangeGuidanceForRoute( request, nextDestination, nextStops, pickupGps, request.pickupDescription, destinationPlace ).catch((error) => { logClientWarning("Could not confirm updated route before pricing route change; using fallback guidance when available.", error); return null; }); guidance = requireConfirmedRouteChangeGuidance(accurateGuidance, "Updated route", fallbackGuidance, request); status.textContent = "Comparing route change against current route..."; const accurateBaselineGuidance = await routeChangeGuidanceForRoute( request, request.destinationFormattedAddress || request.destination, currentStops, pickupGps, request.pickupDescription, currentDestinationPlace ).catch((error) => { logClientWarning("Could not refresh current route baseline before pricing route change.", error); return null; }); baselineGuidance = routeChangeBaselineGuidance(request, accurateBaselineGuidance); } const delta = routeChangeDeltaSummary(request, guidance, nextStops, baselineGuidance); const additionalFare = routeChangeAdditionalFare(request, guidance, nextStops, routeChangeType, baselineGuidance); const totalFare = routeChangeTotalFare(request, additionalFare, guidance, nextStops, routeChangeType); const change = { id: makeId("routeChange"), requestId: request.id, type: routeChangeType, country: request.country, destinationArea: routeChangeDestinationArea(request, { destination: nextDestination, destinationPlaceId: guidance?.destinationPlaceId ?? destinationPlace?.placeId ?? null, destinationFormattedAddress: guidance?.destinationFormattedAddress ?? destinationPlace?.formattedAddress ?? null, destinationLatitude: guidance?.destinationLatitude ?? destinationPlace?.latitude ?? null, destinationLongitude: guidance?.destinationLongitude ?? destinationPlace?.longitude ?? null }), destination: nextDestination, destinationPlaceId: guidance?.destinationPlaceId ?? destinationPlace?.placeId ?? null, destinationFormattedAddress: guidance?.destinationFormattedAddress ?? destinationPlace?.formattedAddress ?? null, destinationLatitude: guidance?.destinationLatitude ?? destinationPlace?.latitude ?? null, destinationLongitude: guidance?.destinationLongitude ?? destinationPlace?.longitude ?? null, rideStops: nextStops, rideStopPoints: nextStopPoints, routeEstimate: guidance ? { distanceMiles: guidance.distanceMiles, minutes: guidance.minutes, source: guidance.source, provider: guidance.provider, cached: guidance.cached, routeKey: guidance.routeKey, routePolyline: guidance.routePolyline, destinationFingerprint: guidance.destinationFingerprint, estimatedAt: guidance.estimatedAt } : null, routeDelta: delta, additionalFare, totalFare, requestedAt: new Date().toISOString(), status: routeChangeNeedsRiderApproval(request) ? "pending" : "accepted", passengerId: request.passengerId, riderId: selectedRiderIdForRequest(request) }; if (routeChangeNeedsRiderApproval(request)) { const verbalNote = request.status === "in_progress" ? "\nPickup has already happened. Ask the rider verbally before sending this request; the rider still must accept it in the app." : ""; const ok = await showWakaGoodConfirm(`${routeChangeConfirmationMessage(request, routeChangeType, delta, additionalFare, change.totalFare, "Request to", guidance, nextStops)}${verbalNote}`); if (!ok) { status.textContent = "Route change was not sent."; return; } status.textContent = "Sending route change to rider..."; let savedChange = null; try { savedChange = await requestRideRouteChangeInSupabase(request, change); } catch (error) { status.textContent = `Route change failed: ${error.message}`; return; } if (hasSupabaseRuntime() && !savedChange) { status.textContent = "Route change failed: Waka could not save this request. Refresh and try again."; return; } if (savedChange) { Object.assign(change, mapRideRouteChangeFromDatabase(savedChange)); } upsertRouteChangeRequest(change); pushRouteChangeSystemEvent(change, "proposed", request); if (typeof addPassengerRideNotice === "function" && requestBelongsToPassenger(request)) { addPassengerRideNotice( routeChangeType === "add_stop" ? "Stop request sent" : "Destination change sent", `Waiting for rider approval. Added fare: ${formatMoney(additionalFare, request.country)}. New total if accepted: ${formatMoney(change.totalFare, request.country)}.`, request.id ); } if (typeof clearPassengerDestinationUpdateDraft === "function") clearPassengerDestinationUpdateDraft(request.id); status.textContent = "Sent to rider for approval."; saveState(); renderAll(); void refreshMarketplace({ silent: true }); return; } const nextFareOffer = Math.max(0, Number(change.totalFare ?? routeChangeEstimatedTotal(request, additionalFare))); if (request.status === "open" && nextFareOffer !== Number(request.fareOffer ?? 0) && !passengerCanSendFareProposal(request)) { status.textContent = fareProposalLimitMessage("passenger", request); return; } const ok = await showWakaGoodConfirm(routeChangeConfirmationMessage(request, routeChangeType, delta, additionalFare, change.totalFare, "Apply", guidance, nextStops)); if (!ok) { status.textContent = "Route change was not applied."; return; } status.textContent = "Updating route and fare..."; const saved = await updateRideDestinationInSupabase(request, nextDestination, guidance, nextStops, destinationPlace, nextStopPoints); let savedFare = null; if (request.status === "open" && nextFareOffer !== Number(request.fareOffer ?? 0)) { savedFare = await updateRideRequestFareInSupabase(request.id, nextFareOffer).catch((fareError) => { logClientWarning("Route change fare update did not sync to Supabase.", fareError); return null; }); } updateRequestById(request.id, (item) => ({ ...item, ...routeChangePatch(change, item), destination: saved?.destination ?? change.destination, destinationArea: saved?.destination_area ?? change.destinationArea ?? item.destinationArea, destinationPlaceId: saved?.destination_place_id ?? change.destinationPlaceId, destinationFormattedAddress: saved?.destination_formatted_address ?? change.destinationFormattedAddress, destinationLatitude: saved?.destination_lat ?? change.destinationLatitude, destinationLongitude: saved?.destination_lng ?? change.destinationLongitude, rideStops: normalizeRideStops(saved?.ride_stops ?? nextStops), rideStopPoints: normalizeRideStopPoints(saved?.ride_stop_points ?? nextStopPoints, saved?.ride_stops ?? nextStops), fareOffer: savedFare?.fareOffer ?? savedFare?.fare_offer_xaf ?? nextFareOffer, fareHistory: savedFare?.fareHistory ?? savedFare?.fare_history ?? (nextFareOffer !== Number(item.fareOffer ?? 0) ? fareProposalHistoryWithNextFare(item, item.fareOffer, nextFareOffer) : item.fareHistory) })); upsertRouteChangeRequest(change); const fareChangeText = additionalFare > 0 ? `Fare offer increased by ${formatMoney(additionalFare, request.country)} to ${formatMoney(nextFareOffer, request.country)}.` : additionalFare < 0 ? `Fare offer decreased by ${formatMoney(Math.abs(additionalFare), request.country)} to ${formatMoney(nextFareOffer, request.country)}.` : `Fare offer stays at ${formatMoney(nextFareOffer, request.country)}.`; pushSystemChat(request.id, `Passenger updated the route to ${requestDestinationText({ ...request, destination: nextDestination, rideStops: nextStops })}. ${fareChangeText}`); if (typeof addPassengerRideNotice === "function" && requestBelongsToPassenger(request)) { addPassengerRideNotice( routeChangeType === "add_stop" ? "Stop added" : "Destination updated", `Route updated. ${fareChangeText}`, request.id ); } if (typeof clearPassengerDestinationUpdateDraft === "function") clearPassengerDestinationUpdateDraft(request.id); saveState(); renderAll(); } catch (error) { status.textContent = `Route change failed: ${/edge function|non-2xx|failed|timeout|network|unavailable/i.test(String(error.message)) ? "route distance is temporarily unavailable. Try again in a moment." : error.message}`; } } function canCancelBeforeStart(request) { if (!request || !preStartCancellationStatuses.includes(request.status)) return false; if (activeRole() === "passenger") return requestBelongsToPassenger(request); return activeRole() === "rider" && riderIdentityMatches(selectedRiderIdForRequest(request)); } function canCancelInProgress(request) { return Boolean(request && request.status === "in_progress" && ((activeRole() === "passenger" && requestBelongsToPassenger(request)) || (activeRole() === "rider" && riderIdentityMatches(selectedRiderIdForRequest(request))))); } function canSeeRideLifecycleActions(request) { if (!request) return false; if (activeRole() === "passenger") return requestBelongsToPassenger(request); return activeRole() === "rider" && riderIdentityMatches(selectedRiderIdForRequest(request)); } function activeRideForRole(preferredRequest = selectedRequest()) { if (activeRole() === "rider" && state.rider) { const inProgressRide = riderInProgressImmediateRide(state.rider); if (inProgressRide) return inProgressRide; } if (canSeeRideLifecycleActions(preferredRequest)) return preferredRequest; if (activeRole() === "passenger" && state.passenger) { return state.requests.find((request) => requestBelongsToPassenger(request) && ["open", "matched", "arrived", "in_progress"].includes(request.status)) ?? null; } if (activeRole() === "rider" && state.rider) { return state.requests.find((request) => riderIdentityMatches(selectedRiderIdForRequest(request)) && ["matched", "arrived", "in_progress"].includes(request.status)) ?? null; } return null; } function rideStopProgressText(request) { const stops = normalizeRideStops(request?.rideStops); if (!stops.length) return "No added stops."; const index = rideStopIndex(request); if (index >= stops.length) return `All ${stops.length} added stop${stops.length === 1 ? "" : "s"} marked. Continue to destination.`; return `Next stop ${index + 1} of ${stops.length}: ${stops[index]}.`; } function rideLifecycleActionSummary(request) { if (!request) return ""; if (request.status === "open") { return requestReopenedAfterRiderCancellation(request) ? "Rider cancelled. This request is open again." : "Cancel any time before choosing a rider."; } if (request.status === "matched") return activeRole() === "rider" ? "Arrive only when GPS is near pickup." : `Rider on the way. ${cancellationFeeText(request)}`.trim(); if (request.status === "arrived") return activeRole() === "rider" ? "Confirm pickup to start destination navigation." : `Rider arrived. Meet the rider when ready. ${cancellationFeeText(request)}`.trim(); if (request.status === "in_progress") return activeRole() === "passenger" ? `${rideStopProgressText(request)} Rider completes at drop-off.` : `${rideStopProgressText(request)} Complete at destination. ${cancellationFeeText(request)}`.trim(); if (request.status === "completed") return `Ride is already marked complete. ${rideFinancialSummary(request)}`.trim(); if (request.status === "cancelled") return `Ride has been cancelled. ${cancellationFeeText(request)}`.trim(); return "Ride actions update as the trip progresses."; } function canTipRequest(request) { return Boolean(request && activeRole() === "passenger" && requestBelongsToPassenger(request) && request.status === "completed" && selectedRiderIdForRequest(request) && !passengerTipForRequest(request.id)); } function reportableRideForRole(preferredRequest = selectedRequest()) { if (canReportOnRequest(preferredRequest)) return preferredRequest; const actionRequest = activeRideForRole(preferredRequest); return canReportOnRequest(actionRequest) ? actionRequest : null; } function canReportOnRequest(request) { if (!request || !rideReportStatuses.includes(request.status)) return false; if (activeRole() === "passenger") return requestBelongsToPassenger(request); if (activeRole() === "rider") return riderIdentityMatches(selectedRiderIdForRequest(request)); return false; } function reportTargetForRequest(request) { if (!request) return { id: null, name: "Unknown account" }; if (activeRole() === "passenger") { return { id: selectedRiderIdForRequest(request), name: selectedRiderFirstNameForRequest(request) }; } return { id: request.passengerId, name: passengerFirstNameForRequest(request) }; } function activeRideContactForRequest(request) { if (!request || !canChatOnRequest(request)) return null; const fallbackName = activeRole() === "rider" ? passengerFirstNameForRequest(request) : selectedRiderFirstNameForRequest(request); return { name: firstNameOnly(request.contactName, fallbackName), profilePhotoPath: request.contactProfilePhotoPath ?? "", relayPhone: request.contactRelayPhone ?? "", relayStatus: request.contactRelayStatus ?? "relay_not_configured" }; } function phoneCallHref(phone) { const normalized = String(phone ?? "").replace(/[^\d+]/g, ""); return normalized ? `tel:${normalized}` : ""; } function contactRelayStatusText(contact) { if (contact?.relayPhone) return `Waka relay active: ${contact.relayPhone}`; if (contact?.relayStatus && contact.relayStatus !== "relay_not_configured") { return `Waka call relay status: ${String(contact.relayStatus).replace(/_/g, " ")}. Chat remains active.`; } return "Chat is active. Masked calling appears here when the Waka relay number is configured; voice relay is reserved for this ride."; } function appendContactActions(container, request) { const contact = activeRideContactForRequest(request); if (!contact) return; const compactPassengerContact = activeRole() === "passenger"; const panel = document.createElement("div"); panel.className = "contact-actions"; panel.classList.toggle("compact-contact-actions", compactPassengerContact); const avatar = document.createElement("span"); avatar.className = "profile-avatar contact-profile-avatar"; avatar.textContent = contact.name.slice(0, 1).toUpperCase() || "W"; panel.append(avatar); ensureContactProfilePhotoUrl(contact, avatar); if (!compactPassengerContact) { const title = document.createElement("strong"); title.textContent = `Text or call ${contact.name}`; panel.append(title); const detail = document.createElement("small"); detail.textContent = contactRelayStatusText(contact); panel.append(detail); } if (contact.relayPhone) { const callLink = document.createElement("a"); callLink.className = compactPassengerContact ? "secondary-action icon-only-action" : "secondary-action"; callLink.href = phoneCallHref(contact.relayPhone); callLink.setAttribute("aria-label", `Call ${contact.name} through Waka relay`); callLink.textContent = compactPassengerContact ? "☎" : "Call through Waka"; panel.append(callLink); } else { const callButton = document.createElement("button"); callButton.type = "button"; callButton.className = compactPassengerContact ? "secondary-action icon-only-action" : "secondary-action"; callButton.disabled = true; callButton.setAttribute("aria-label", "Call relay pending"); callButton.textContent = compactPassengerContact ? "☎" : "Call relay pending"; panel.append(callButton); } container.append(panel); } async function ensureContactProfilePhotoUrl(contact, avatar) { if (!contact?.profilePhotoPath || !avatar || !isSupabaseMode() || !supabaseClient) return; const cached = contactProfilePhotoUrlCache.get(contact.profilePhotoPath); if (cached) { avatar.innerHTML = ``; return; } try { const { data, error } = await supabaseClient.storage .from(appConfig.buckets.profilePhotos) .createSignedUrl(contact.profilePhotoPath, 600); if (error || !data?.signedUrl) throw error || new Error("Signed profile photo URL was not returned."); contactProfilePhotoUrlCache.set(contact.profilePhotoPath, data.signedUrl); avatar.innerHTML = ``; } catch (error) { logClientWarning("Matched contact profile photo could not be opened.", error); } } function ratingTargetForRequest(request) { if (!request) return null; if (activeRole() === "passenger") { const riderId = selectedRiderIdForRequest(request); return riderId ? { id: riderId, name: selectedRiderFirstNameForRequest(request) } : null; } if (activeRole() === "rider" && selectedRiderIdForRequest(request) === state.rider?.id) { return { id: request.passengerId, name: passengerFirstNameForRequest(request) }; } return null; } async function resolveRideRatingContext(request) { let resolvedRequest = request; let target = ratingTargetForRequest(resolvedRequest); if (hasSupabaseRuntime() && resolvedRequest?.status === "completed" && roleCanSeeRequest(resolvedRequest) && typeof loadMarketplaceFromSupabase === "function") { try { await loadMarketplaceFromSupabase({ includeAccountData: true }); resolvedRequest = stateLookupIndexes().requestMap.get(resolvedRequest.id) ?? resolvedRequest; target = ratingTargetForRequest(resolvedRequest); } catch (error) { logClientWarning("Could not refresh completed ride details before rating.", error); } } return { request: resolvedRequest, target }; } function existingRatingForRequest(request) { const reviewerId = activeRole() === "rider" ? state.rider?.id : state.passenger?.id; if (!request?.id || !reviewerId) return null; return rideRatingRecords().find((rating) => rating.requestId === request.id && rating.reviewerId === reviewerId) ?? null; } function canRateRequest(request) { return Boolean(request && request.status === "completed" && roleCanSeeRequest(request) && ratingTargetForRequest(request) && !existingRatingForRequest(request)); } function currentEligibleOfferContext() { const request = selectedRequest(); const rider = currentRiderRecord(); if (!request) { translatedAlert("selectRideRequestFirst"); return null; } if (!rider) { translatedAlert("createRiderFirst"); return null; } if (!hasSignedIn("rider")) { translatedAlert("riderSignInRequired"); return null; } if (rider.status !== "approved") { translatedAlert("riderApprovalRequired"); return null; } if (!isSubscriptionActive(rider)) { translatedAlert("riderAccessRequired"); return null; } if (!paymentAccountReady("rider", rider)) { translatedAlert("riderPaymentRequired"); return null; } if (typeof riderAvailabilityIsActivated === "function" && !riderAvailabilityIsActivated()) { if (els.offerRequestContext) els.offerRequestContext.textContent = riderAvailabilityRequiredText(); if (typeof setRiderWorkspacePage === "function") setRiderWorkspacePage("initialize"); return null; } if (!riderCurrentFreshGps(rider)) { translatedAlert("riderLiveGpsRequired"); return null; } if (!roleCanSeeRequest(request)) { translatedAlert("selectNearbyRequest"); return null; } const blockingRide = riderBlockingImmediateRide(rider); if (blockingRide && blockingRide.id !== request.id && !isScheduledRequest(request)) { void showWakaGoodAlert("New immediate offers unlock when this ride is about 7 minutes from drop-off."); return null; } if (request.status !== "open") { if (requestIsActiveForCurrentRider(request, rider)) { if (els.offerRequestContext) { const heldForCurrentRide = riderShouldHoldNextRideNavigation(request, rider); const nextStop = heldForCurrentRide ? "Queued after current ride" : request.status === "in_progress" ? `Destination: ${requestDestinationDisplayText(request)}` : `Pickup: ${requestPickupDisplayText(request)}`; els.offerRequestContext.textContent = `This ride is already matched to you. ${nextStop}.`; } const currentRide = riderInProgressImmediateRide(rider); focusRiderRequestView(currentRide && currentRide.id !== request.id ? currentRide : request, { refresh: false, replace: true }); return null; } translatedAlert("requestClosed"); return null; } return { request, rider }; } function closeOpenBargainingAfterRideMatch({ matchedRequestId, selectedOfferId, selectedRiderId }) { if (!matchedRequestId || !selectedOfferId || !selectedRiderId) return; const openRequestIds = new Set(state.requests .filter((request) => request.status === "open") .map((request) => request.id)); state.offers = state.offers.filter((offer) => { if (offer.id === selectedOfferId) return true; if (offer.requestId === matchedRequestId) return false; if (offer.riderId === selectedRiderId && openRequestIds.has(offer.requestId)) return false; return true; }); } const marketplaceActionInFlightKeys = new Set(); function showMarketplaceActionBusyMessage(message) { if (activeRole() === "rider" && els.offerRequestContext) { els.offerRequestContext.textContent = message; return; } if (els.selectedSummary) els.selectedSummary.textContent = message; } async function runMarketplaceActionOnce(actionKey, busyMessage, action) { if (!actionKey) return action(); if (marketplaceActionInFlightKeys.has(actionKey)) { showMarketplaceActionBusyMessage(busyMessage); return null; } marketplaceActionInFlightKeys.add(actionKey); try { return await action(); } finally { marketplaceActionInFlightKeys.delete(actionKey); } } function negotiatedFareAcceptanceSummary({ request, fare, offeredByLabel, acceptingRole }) { const amount = formatMoney(fare, request?.country); const pickup = requestPickupDisplayText(request); const destination = requestDestinationDisplayText(request); const schedule = isScheduledRequest(request) ? formatDateTime(request.scheduledAt) : "Immediate ride"; const roleCopy = acceptingRole === "rider" ? "This will match you to this passenger." : "This will match you with this rider."; const otherRiderCopy = "After matching, this request will no longer be available to other riders."; return [ "Confirm negotiated fare", "", `Fare to accept: ${amount}`, `Offer from: ${offeredByLabel || "Waka user"}`, `Pickup: ${pickup}`, `Destination: ${destination}`, `Timing: ${schedule}`, "", roleCopy, otherRiderCopy, "", "OK: accept this fare and match the ride.", "Cancel: continue negotiations." ].join("\n"); } async function confirmNegotiatedFareAcceptance(details) { if (!requestIsNegotiableFare(details?.request)) return true; return await showWakaGoodConfirm(negotiatedFareAcceptanceSummary(details)); } function normalizedOfferFareHistoryEntries(offer) { const rows = Array.isArray(offer?.fareHistory) ? offer.fareHistory : []; return rows .map((entry) => ({ fare: Number(entry?.fare ?? entry?.amount ?? entry?.fare_xaf ?? entry), createdAt: entry?.createdAt ?? entry?.created_at ?? offer?.createdAt ?? null })) .filter((entry) => Number.isFinite(entry.fare) && entry.fare > 0) .sort((a, b) => new Date(a.createdAt ?? 0).getTime() - new Date(b.createdAt ?? 0).getTime()); } function offerWithUpdatedFareHistory(savedOffer, previousOffer, fare) { const history = normalizedOfferFareHistoryEntries(previousOffer); const previousFare = Number(previousOffer?.fare); if (Number.isFinite(previousFare) && previousFare > 0) { history.push({ fare: previousFare, createdAt: previousOffer?.createdAt ?? null }); } const nextFare = Number(fare ?? savedOffer?.fare); if (Number.isFinite(nextFare) && nextFare > 0) { history.push({ fare: nextFare, createdAt: savedOffer?.createdAt ?? new Date().toISOString() }); } const deduped = history.reduce((trail, entry) => { const previous = trail[trail.length - 1]; if (!previous || Number(previous.fare) !== Number(entry.fare)) trail.push(entry); return trail; }, []); return { ...savedOffer, fareHistory: deduped }; } function fareProposalTrail(source, currentFare, currentCreatedAt = null) { const trail = typeof fareHistoryTrail === "function" ? fareHistoryTrail(source, currentFare, currentCreatedAt) : []; if (trail.length) return trail; const fare = Number(currentFare); return Number.isFinite(fare) && fare > 0 ? [{ fare, createdAt: currentCreatedAt ?? source?.createdAt ?? null }] : []; } function fareProposalAttemptCount(source, currentFare, currentCreatedAt = null) { return fareProposalTrail(source, currentFare, currentCreatedAt).length; } function passengerFareProposalAttemptCount(request) { return fareProposalAttemptCount(request, request?.fareOffer, request?.createdAt); } function riderFareProposalAttemptCount(request, rider = currentRiderRecord()) { const offer = state.offers.find((item) => item.requestId === request?.id && riderIdentityMatches(item.riderId, rider)); return offer ? fareProposalAttemptCount(offer, offer.fare, offer.createdAt) : 0; } function fareProposalAttemptsRemaining(count) { return Math.max(0, fareProposalAttemptLimit - Number(count || 0)); } function passengerCanSendFareProposal(request) { return passengerFareProposalAttemptCount(request) < fareProposalAttemptLimit; } function riderCanSendFareProposal(request, rider = currentRiderRecord()) { return riderFareProposalAttemptCount(request, rider) < fareProposalAttemptLimit; } function fareProposalLimitMessage(role, request) { const actor = role === "rider" ? "Rider" : "Passenger"; const fare = role === "rider" ? state.offers.find((item) => item.requestId === request?.id && riderIdentityMatches(item.riderId))?.fare : request?.fareOffer; const fareText = Number.isFinite(Number(fare)) && Number(fare) > 0 ? ` Current fare on the table: ${formatMoney(fare, request?.country)}.` : ""; return `${actor} fare proposals are limited to ${fareProposalAttemptLimit} attempts for this request.${fareText} Accept or decline instead of sending another fare.`; } function fareProposalHistoryWithNextFare(source, currentFare, nextFare) { const history = fareProposalTrail(source, currentFare, source?.updatedAt ?? source?.createdAt ?? null); const proposedFare = Number(nextFare); if (Number.isFinite(proposedFare) && proposedFare > 0) { const previous = history[history.length - 1]; if (!previous || Number(previous.fare) !== proposedFare) { history.push({ fare: proposedFare, createdAt: new Date().toISOString() }); } } return history; } async function saveRiderOffer({ request, rider, fare, type }) { const note = ""; if (note.length > riderOfferNoteMaxLength) { els.offerRequestContext.textContent = `Keep rider notes to ${riderOfferNoteMaxLength} characters or less.`; return; } if (type === "counter" && !riderCanSendFareProposal(request, rider)) { if (els.offerRequestContext) els.offerRequestContext.textContent = fareProposalLimitMessage("rider", request); return; } const xlFloor = xlSpecialFareFloorForRequest(request); if (xlFloor != null && Number(fare) <= xlFloor) { els.offerRequestContext.textContent = `XL/Special offers must be above ${formatMoney(xlFloor, request.country)} for this route.`; return; } const existing = state.offers.find((offer) => offer.requestId === request.id && offer.riderId === rider.id); const minimumNextFare = Number(request.fareOffer ?? 0); if (type === "counter" && Number(fare) <= minimumNextFare) { els.offerRequestContext.textContent = `Enter any whole-dollar counter-offer higher than ${formatMoney(minimumNextFare, request.country)}.`; return; } const offer = { id: existing?.id ?? makeId("offer"), requestId: request.id, riderId: rider.id, fare, type, note, ...offerPickupDistanceSnapshot(request, rider), createdAt: new Date().toISOString() }; try { if (paymentSetupRelaxedForTesting()) { await ensureStagingPaymentAccountForTesting("rider", rider, { localFallback: false }); } const savedOffer = await saveOfferToSupabase(offer); const savedOfferWithHistory = offerWithUpdatedFareHistory(savedOffer, existing, fare); state.offers = state.offers.filter((item) => item.id !== offer.id && item.id !== savedOfferWithHistory.id); state.offers.unshift(savedOfferWithHistory); if (type === "accepted") { state.requests = state.requests.map((item) => { if (item.id !== request.id) return item; return preserveRideRequestPickup({ ...item, status: "matched", selectedOfferId: savedOfferWithHistory.id, agreedFare: savedOfferWithHistory.fare, selectedRiderId: savedOfferWithHistory.riderId, selectedRiderName: firstNameOnly(rider?.name, "Rider"), riderConfirmationStatus: isScheduledRequest(item) ? "not_requested" : null, riderConfirmationRequestedAt: null, riderConfirmedAt: null, releasedAt: null, matchedAt: new Date().toISOString() }, item); }); const currentRide = riderInProgressImmediateRide(rider); state.selectedRequestId = currentRide && currentRide.id !== request.id ? currentRide.id : request.id; pushSystemChat( request.id, currentRide && currentRide.id !== request.id ? `Rider accepted the passenger fare of ${formatMoney(savedOfferWithHistory.fare)}. This next pickup is queued until the current ride is completed.` : `Rider accepted the passenger fare of ${formatMoney(savedOfferWithHistory.fare)}. Ride matched automatically.` ); closeOpenBargainingAfterRideMatch({ matchedRequestId: request.id, selectedOfferId: savedOfferWithHistory.id, selectedRiderId: savedOfferWithHistory.riderId }); } els.offerForm.reset(); const shouldReturnToMarketplace = type === "counter" && activeRole() === "rider" && typeof returnRiderToMarketplace === "function"; saveState(); if (shouldReturnToMarketplace) returnRiderToMarketplace({ replace: true, refresh: true }); else renderAll(); if (type === "accepted") { const matchedRequest = preserveRideRequestPickup(state.requests.find((item) => item.id === request.id) ?? request, request); if (typeof riderPickupNavigationShouldWaitForDropoff === "function" && riderPickupNavigationShouldWaitForDropoff(matchedRequest, rider)) { if (els.offerRequestContext) { els.offerRequestContext.textContent = "Next pickup queued. Finish the current drop-off before Waka opens navigation to the new pickup."; } } else { const navKey = `rider-pickup-nav-${matchedRequest.id}-${matchedRequest.matchedAt || savedOfferWithHistory.id}`; if (typeof openRiderPickupNavigationWhenPrecise === "function") { void openRiderPickupNavigationWhenPrecise(matchedRequest, navKey); } else if (typeof shouldOpenRiderPickupNavigation === "function" && requestHasPrecisePickupNavigation(matchedRequest) && shouldOpenRiderPickupNavigation(matchedRequest, navKey)) { openRiderPickupNavigation(matchedRequest, navKey); } } } void processRideRequestPushDelivery(request.id, { eventTypes: [type === "accepted" ? "ride_matched" : "rider_counter_offer"] }); if (typeof forceMarketplaceRefreshSoon === "function") { forceMarketplaceRefreshSoon(type === "accepted" ? "rider_accept_after_offer" : "rider_counter_after_offer"); } else if (!shouldReturnToMarketplace) { void refreshMarketplace({ silent: true }); } } catch (error) { translatedAlert("offerSendFailed", { message: error.message }); } } async function acceptPassengerFare() { const requestId = selectedRequest()?.id ?? state.selectedRequestId ?? "unknown-request"; const riderId = currentRiderRecord()?.id ?? state.rider?.id ?? "unknown-rider"; await runMarketplaceActionOnce(`rider-accept:${requestId}:${riderId}`, "Acceptance is already being confirmed. Please wait a moment.", async () => { if (hasSupabaseRuntime()) await refreshMarketplace({ silent: true, reason: "rider_accept_precheck" }); const context = currentEligibleOfferContext(); if (!context) return; if (!await confirmNegotiatedFareAcceptance({ request: context.request, fare: context.request.fareOffer, offeredByLabel: passengerFirstNameForRequest(context.request), acceptingRole: "rider" })) { if (els.offerRequestContext) els.offerRequestContext.textContent = "Acceptance cancelled. Continue negotiating or send a counter-offer."; return; } await saveRiderOffer({ ...context, fare: context.request.fareOffer, type: "accepted" }); }); } function nextRiderRequestAfterLeaving(requestId) { return visibleRequestsForRole().find((request) => request.id !== requestId) ?? null; } function locallyDismissRiderMarketplaceRequest(request, rider = currentRiderRecord()) { if (!request?.id || activeRole() !== "rider") return null; if (typeof rememberRiderDismissedRequest === "function") rememberRiderDismissedRequest(request, rider); state.offers = state.offers.filter((offer) => !(offer.requestId === request.id && offer.riderId === rider?.id)); const nextRequest = nextRiderRequestAfterLeaving(request.id); if (state.selectedRequestId === request.id) state.selectedRequestId = null; if (typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute("requests", { replace: true, requestId: "" }); } saveState(); renderAll(); return nextRequest; } async function dropRiderNegotiation() { const request = selectedRequest(); const rider = currentRiderRecord(); if (!request || activeRole() !== "rider") { translatedAlert("selectRideRequestFirst"); return; } const ownOffer = state.offers.find((offer) => offer.requestId === request.id && offer.riderId === rider?.id); const nextRequest = locallyDismissRiderMarketplaceRequest(request, rider); els.offerForm?.reset(); if (els.offerRequestContext) { els.offerRequestContext.textContent = nextRequest ? "Request declined. Review the marketplace when ready." : ownOffer ? "Request declined and your open rider offer is being withdrawn. Select another request when one appears." : "Request declined. Select another request when one appears."; } if (request.status === "open") { void withdrawRiderOfferFromSupabase(request.id) .then((updatedRequest) => { if (updatedRequest?.id) state.requests = upsertById(state.requests, updatedRequest); void refreshMarketplace({ silent: true, reason: "rider_left_negotiation" }); }) .catch((error) => { const staleRequest = /no longer open|no longer available|not found|cancelled/i.test(error.message || ""); if (staleRequest) state.requests = state.requests.filter((item) => item.id !== request.id); else logClientWarning("Rider decline could not be synced immediately; local marketplace was updated.", error); void refreshMarketplace({ silent: true, reason: "rider_left_negotiation_sync_failed" }); }); } else { void refreshMarketplace({ silent: true, reason: "rider_left_negotiation" }); } } async function ignoreRiderMarketplaceRequest(requestId) { const request = stateLookupIndexes().requestMap.get(requestId) ?? null; if (!request || activeRole() !== "rider") return; const rider = currentRiderRecord(); locallyDismissRiderMarketplaceRequest(request, rider); if (request.status === "open") { void withdrawRiderOfferFromSupabase(request.id) .then((updatedRequest) => { if (updatedRequest?.id) state.requests = upsertById(state.requests, updatedRequest); void refreshMarketplace({ silent: true, reason: "rider_declined_request" }); }) .catch((error) => { const staleRequest = /no longer open|no longer available|not found|cancelled/i.test(error.message || ""); if (staleRequest) state.requests = state.requests.filter((item) => item.id !== request.id); else logClientWarning("Rider declined a request locally, but backend sync was delayed.", error); void refreshMarketplace({ silent: true, reason: "rider_declined_request_sync_failed" }); }); } else { void refreshMarketplace({ silent: true, reason: "rider_declined_request" }); } } async function sendOffer(event) { event.preventDefault(); if (hasSupabaseRuntime()) await refreshMarketplace({ silent: true, reason: "rider_offer_precheck" }); const context = currentEligibleOfferContext(); if (!context) return; if (requestIsNonNegotiableFare(context.request)) { els.offerRequestContext.textContent = "This passenger chose a non-negotiable fare. Accept the fare or decline the request."; return; } const typedFare = String(els.counterFare?.value ?? "").trim(); const typedNoteFare = String(els.counterNote?.value ?? "").trim(); const fareSource = typedFare || (/^\$?\d+(\.\d{1,2})?$/.test(typedNoteFare) ? typedNoteFare : ""); const requestedFare = Number(String(fareSource).replace(/[^\d.]/g, "")); if (!requestedFare) { els.offerRequestContext.textContent = "Enter your counter-offer fare, or use Accept passenger fare."; if (els.counterFare && !els.counterFare.disabled) els.counterFare.focus(); return; } if (!Number.isInteger(requestedFare)) { els.offerRequestContext.textContent = "Use whole-dollar counter-offers. Enter a higher whole amount or cancel if the fare does not work."; return; } if (requestedFare === context.request.fareOffer) { els.offerRequestContext.textContent = "That matches the passenger fare. Use Accept passenger fare, or enter a different counter-offer."; return; } const minimumNextFare = Number(context.request.fareOffer ?? 0); if (requestedFare <= minimumNextFare) { els.offerRequestContext.textContent = `Enter any whole-dollar counter-offer higher than ${formatMoney(minimumNextFare, context.request.country)}.`; return; } const xlFloor = xlSpecialFareFloorForRequest(context.request); if (xlFloor != null && requestedFare <= xlFloor) { els.offerRequestContext.textContent = `XL/Special counter-offers must be above ${formatMoney(xlFloor, context.request.country)}.`; return; } if (!riderCanSendFareProposal(context.request, context.rider)) { els.offerRequestContext.textContent = fareProposalLimitMessage("rider", context.request); return; } await runMarketplaceActionOnce(`rider-counter:${context.request.id}:${context.rider.id}`, "Counter-offer is already being sent. Please wait a moment.", async () => { await saveRiderOffer({ ...context, fare: requestedFare, type: "counter" }); }); } async function chooseOffer(offerId) { const offer = state.offers.find((item) => item.id === offerId); if (!offer) return; const requestToMatch = state.requests.find((request) => request.id === offer.requestId); if (activeRole() !== "passenger" || !requestBelongsToPassenger(requestToMatch)) { translatedAlert("passengerOwnRequestRequired"); return; } if (offerIsExpired(offer, requestToMatch)) { void showWakaGoodAlert("That rider offer has expired. Keep the request open or counter with a fresh fare so riders can respond again."); renderAll(); return; } const rider = state.riders.find((item) => item.id === offer.riderId); let passengerConfirmedAcceptance = false; try { await runMarketplaceActionOnce(`passenger-select:${offer.requestId}`, "Fare acceptance is already being confirmed. Please wait a moment.", async () => { if (hasSupabaseRuntime()) await refreshMarketplace({ silent: true, reason: "passenger_accept_offer_precheck" }); const freshOffer = state.offers.find((item) => item.id === offer.id) ?? offer; const freshRequest = state.requests.find((request) => request.id === freshOffer.requestId) ?? requestToMatch; const freshRider = state.riders.find((item) => item.id === freshOffer.riderId) ?? rider; if (freshRequest?.status !== "open") { translatedAlert("requestClosed"); renderAll(); return; } if (offerIsExpired(freshOffer, freshRequest)) { void showWakaGoodAlert("That rider offer has expired. Keep the request open or counter with a fresh fare so riders can respond again."); renderAll(); return; } if (!await confirmNegotiatedFareAcceptance({ request: freshRequest, fare: freshOffer.fare, offeredByLabel: firstNameOnly(freshRider?.name, "Rider"), acceptingRole: "passenger" })) { if (els.selectedSummary) els.selectedSummary.textContent = "Acceptance cancelled. Continue negotiating or review other rider offers."; return; } passengerConfirmedAcceptance = true; passengerInitiatedRideMatchRequestIds.add(freshOffer.requestId); let savedRequest = null; try { savedRequest = await chooseOfferInSupabase(freshRequest, freshOffer); } catch (error) { translatedAlert("chooseRiderFailed", { message: error.message }); return; } state.requests = state.requests.map((request) => { if (request.id !== freshOffer.requestId) return request; const serverState = savedRequest?.id === request.id ? savedRequest : {}; return preserveRideRequestPickup({ ...request, ...serverState, status: "matched", selectedOfferId: freshOffer.id, agreedFare: freshOffer.fare, selectedRiderId: freshOffer.riderId, selectedRiderName: firstNameOnly(freshRider?.name, "Rider"), riderConfirmationStatus: isScheduledRequest(request) ? "not_requested" : null, riderConfirmationRequestedAt: null, riderConfirmedAt: null, releasedAt: null, matchedAt: new Date().toISOString() }, request); }); closeOpenBargainingAfterRideMatch({ matchedRequestId: freshOffer.requestId, selectedOfferId: freshOffer.id, selectedRiderId: freshOffer.riderId }); state.selectedRequestId = freshOffer.requestId; state.passengerPage = "trips"; if (typeof updatePassengerWorkspaceRoute === "function") { updatePassengerWorkspaceRoute("trips", { replace: true, requestId: freshOffer.requestId, preferPathRoute: true }); } const systemMessage = { id: makeId("chat"), requestId: freshOffer.requestId, sender: "system", text: isScheduledRequest(freshRequest) ? `Scheduled ride matched at ${formatMoney(freshOffer.fare)} for ${formatDateTime(freshRequest.scheduledAt)}. Passenger can request confirmation before travel.` : `Ride matched at ${formatMoney(freshOffer.fare)}. Confirm pickup and ${paymentLabel(freshRequest.paymentPreference).toLowerCase()} before the ride starts.`, createdAt: new Date().toISOString() }; state.chats.push(systemMessage); void saveChatMessageToSupabase(systemMessage); saveState(); renderAll(); void processRideRequestPushDelivery(freshOffer.requestId, { eventTypes: ["ride_matched"] }); void recordInsuranceTelemetryTransitionInSupabase( { ...freshRequest, id: freshOffer.requestId, selectedOfferId: freshOffer.id, selectedRiderId: freshOffer.riderId }, "match" ); if (typeof forceMarketplaceRefreshSoon === "function") forceMarketplaceRefreshSoon("passenger_choose_offer"); else void refreshMarketplace({ silent: true }); }); } finally { if (passengerConfirmedAcceptance) { window.setTimeout(() => passengerInitiatedRideMatchRequestIds.delete(offer.requestId), 90000); } } } function pushSystemChat(requestId, text) { const message = { id: makeId("chat"), requestId, sender: "system", text, createdAt: new Date().toISOString() }; state.chats.push(message); void saveChatMessageToSupabase(message); } function updateRequestById(requestId, updater) { state.requests = state.requests.map((request) => request.id === requestId ? updater(request) : request); } async function changeRideStateInSupabase(requestId, actionName, reason = "", gpsPoint = null) { if (!hasSupabaseRuntime()) return; const point = normalizeGpsPoint(gpsPoint); const gpsBody = { request_id: requestId, action_name: actionName, reason, p_lat: point?.latitude ?? null, p_lng: point?.longitude ?? null, p_accuracy_meters: point?.accuracyMeters ?? null, p_captured_at: point?.capturedAt ?? null, p_actor_role: activeRole() === "rider" ? "rider" : activeRole() === "passenger" ? "passenger" : null }; const legacyBody = { request_id: requestId, action_name: actionName, reason }; try { const data = await callSupabaseRpcResult( "change_ride_state", gpsBody, "Updating the ride status", rideLifecycleSupabaseTimeoutMs ); lastRideLifecycleSource = "ride lifecycle RPC"; const row = Array.isArray(data) ? data[0] ?? null : data; return row?.id ? mapRideRequestFromDatabase(row, new Map(), stateLookupIndexes().offerMap) : null; } catch (error) { const message = error.message ?? String(error); const missingFunction = /schema cache|could not find the function|function .*change_ride_state.*does not exist|pgrst202|404/i.test(message); if (missingFunction && point) { try { const data = await callSupabaseRpcResult( "change_ride_state", legacyBody, "Updating the ride status", rideLifecycleSupabaseTimeoutMs ); lastRideLifecycleSource = "ride lifecycle RPC"; const row = Array.isArray(data) ? data[0] ?? null : data; return row?.id ? mapRideRequestFromDatabase(row, new Map(), stateLookupIndexes().offerMap) : null; } catch (legacyError) { error = legacyError; } } if (missingFunction) rideLifecycleRpcUnavailable = true; throw new Error(missingFunction ? "Ride lifecycle is not installed in Supabase yet. Run supabase-ride-lifecycle.sql, then retry." : error.message); } } function lifecyclePointForRequest(request, actionName) { if (actionName === "arrive") return requestPickupGpsForMatching(request); if (actionName === "stop") return rideStopPointAt(request, rideStopIndex(request)); if (actionName === "complete" && request?.destinationLatitude != null && request?.destinationLongitude != null) { return { latitude: request.destinationLatitude, longitude: request.destinationLongitude, accuracyMeters: null, capturedAt: request.routeEstimateCreatedAt ?? request.createdAt }; } return null; } function lifecycleCanUseAddressOnlyPickupArrival(request) { const pickupText = String(request?.pickupDescription ?? request?.pickupFormattedAddress ?? "").trim(); return Boolean(pickupText && !pickupUsesCurrentLocationText(pickupText) && !pickupUsesGpsFallbackText(pickupText)); } async function validatedLifecycleGpsPoint(request, actionName) { if (!["arrive", "stop", "complete"].includes(actionName)) return null; if (typeof getCurrentGpsPoint !== "function") { throw new Error("Phone GPS is required for this ride step, but this browser cannot capture location."); } const target = lifecyclePointForRequest(request, actionName); if (actionName === "stop" && !target) return null; const addressOnlyPickupArrival = actionName === "arrive" && !target && lifecycleCanUseAddressOnlyPickupArrival(request); if (!target && !addressOnlyPickupArrival) { throw new Error(actionName === "arrive" ? "Pickup coordinates are required before registering arrival." : actionName === "stop" ? "Stop coordinates are required before registering arrival. The passenger must choose stops from address suggestions." : "Destination coordinates are required before completing the ride."); } const current = normalizeGpsPoint(await getCurrentGpsPoint()); if (!current) throw new Error("Phone GPS returned an invalid location."); const accuracyLimit = lifecycleGpsAccuracyLimitMeters(); if (current.accuracyMeters == null || current.accuracyMeters > accuracyLimit) { throw new Error(`Phone GPS accuracy must be ${accuracyLimit} meters or better for this ride step.`); } if (addressOnlyPickupArrival) return current; const distanceMeters = (gpsDistanceKmBetween(current, target) ?? Number.POSITIVE_INFINITY) * 1000; const allowedMeters = lifecycleDistanceLimitMeters(actionName); if (distanceMeters > allowedMeters) { throw new Error(actionName === "arrive" ? `Arrival was not registered because your phone is about ${Math.round(distanceMeters)} meters from the pickup address.` : actionName === "stop" ? `Stop arrival was not registered because your phone is about ${Math.round(distanceMeters)} meters from this stop.` : `Ride completion was not registered because your phone is about ${Math.round(distanceMeters)} meters from the destination address.`); } return current; } function rideLifecycleMessage(request, actionName, actorRole = activeRole()) { const cancellationFee = actionName === "cancel" ? rideCancellationCompensationEstimate(request, Date.now(), actorRole) : null; if (actionName === "cancel" && request?.status === "in_progress") { if (actorRole === "rider") { return "Rider cancelled the ride after pickup. Passenger will not be charged because the rider cancelled."; } const actor = "Passenger cancelled"; return `${actor} the ride after pickup.${cancellationFee?.amount > 0 ? ` Partial rider compensation pending: ${formatMoney(cancellationFee.amount, request.country)} for about ${cancellationFee.elapsedMinutes} minute${cancellationFee.elapsedMinutes === 1 ? "" : "s"} after pickup.` : ""}`; } return { arrive: `${selectedRiderFirstNameForRequest(request)} marked arrival at the pickup point.`, start: "Rider confirmed the passenger pickup.", stop: `${selectedRiderFirstNameForRequest(request)} marked arrival at ${nextRideLeg(request).label.toLowerCase()}.`, complete: "Ride completed.", cancel: actorRole === "rider" ? "Rider cancelled before the ride started. The passenger request was reopened for other nearby riders." : `Passenger cancelled the ride before it started.${cancellationFee?.amount > 0 ? ` Rider compensation fee pending: ${formatMoney(cancellationFee.amount, request.country)}.` : ""}` }[actionName] ?? "Ride status updated."; } function applyRideLifecycleState(request, actionName, reason = "", actorRole = activeRole(), actorId = currentActorIdForChat()) { if (actionName === "cancel") { if (request.status === "in_progress" && ["passenger", "rider"].includes(actorRole)) { const cancellationFee = actorRole === "rider" ? riderCancellationNoPassengerChargeEstimate(request) : inProgressCancellationCompensationEstimate(request); return { ...request, status: "cancelled", cancelledBy: actorId, cancelledAt: new Date().toISOString(), cancelReason: reason, cancellationFeeAmount: cancellationFee.amount, cancellationFeeCurrency: cancellationFee.currency, cancellationFeeStatus: cancellationFee.status, cancellationFeeRiderId: actorRole === "rider" ? null : selectedRiderIdForRequest(request), cancellationFeeElapsedMinutes: actorRole === "rider" ? null : cancellationFee.elapsedMinutes }; } if (actorRole === "rider") { const cancelingRiderIds = [state.rider?.id, state.rider?.supabaseUserId, actorId] .filter(Boolean) .map(String); state.offers = state.offers.filter((offer) => !( offer.requestId === request.id && cancelingRiderIds.includes(String(offer.riderId)) )); return { ...request, status: "open", selectedOfferId: null, agreedFare: null, selectedRiderId: null, selectedRiderName: null, currentStopIndex: 0, lastStopArrivedAt: null, matchedAt: null, arrivedAt: null, startedAt: null, completedAt: null, riderConfirmationStatus: isScheduledRequest(request) ? "released" : null, riderConfirmationRequestedAt: null, riderConfirmedAt: null, releasedAt: new Date().toISOString(), cancelledBy: null, cancelledAt: null, cancelReason: reason }; } const cancellationFee = passengerCancellationFeeEstimate(request); return { ...request, status: "cancelled", cancelledBy: actorId, cancelledAt: new Date().toISOString(), cancelReason: reason, cancellationFeeAmount: cancellationFee.amount, cancellationFeeCurrency: cancellationFee.currency, cancellationFeeStatus: cancellationFee.status, cancellationFeeRiderId: selectedRiderIdForRequest(request), cancellationFeeElapsedMinutes: cancellationFee.elapsedMinutes }; } if (actionName === "stop") { const stops = normalizeRideStops(request?.rideStops); return { ...request, currentStopIndex: Math.min(stops.length, rideStopIndex(request) + 1), lastStopArrivedAt: new Date().toISOString() }; } const nextStatus = { arrive: "arrived", start: "in_progress", complete: "completed" }[actionName]; if (!nextStatus) return request; const nowIso = new Date().toISOString(); return { ...request, status: nextStatus, arrivedAt: actionName === "arrive" ? nowIso : request.arrivedAt, startedAt: actionName === "start" ? nowIso : request.startedAt, currentStopIndex: actionName === "start" ? 0 : request.currentStopIndex, lastStopArrivedAt: actionName === "start" ? null : request.lastStopArrivedAt, completedAt: actionName === "complete" ? nowIso : request.completedAt }; } function rideLifecycleTargetAlreadyReached(request, actionName, previousRequest = null) { if (!request) return false; if (actionName === "arrive") return ["arrived", "in_progress", "completed"].includes(request.status); if (actionName === "start") return ["in_progress", "completed"].includes(request.status); if (actionName === "stop") return request.status === "in_progress" && rideStopIndex(request) > rideStopIndex(previousRequest); if (actionName === "complete") return request.status === "completed"; return false; } function reconcileAppliedRiderPrePickupCancellation(requestId, previousRequest = null) { if (activeRole() !== "rider" || !previousRequest || !["matched", "arrived"].includes(previousRequest.status)) { return false; } const refreshed = state.requests.find((item) => item.id === requestId); const riderId = state.rider?.id; const noLongerMatchedToThisRider = !refreshed || selectedRiderIdForRequest(refreshed) !== riderId || !["matched", "arrived", "in_progress"].includes(refreshed.status); const reopenedForMarketplace = refreshed?.status === "open" && !selectedRiderIdForRequest(refreshed); if (!noLongerMatchedToThisRider && !reopenedForMarketplace) return false; return clearRiderPrePickupCancellationView(requestId, previousRequest); } function clearRiderPrePickupCancellationView(requestId, previousRequest = null, { refresh = false } = {}) { if (activeRole() !== "rider" || !requestId) return false; const request = previousRequest ?? state.requests.find((item) => item.id === requestId); if (request && !["matched", "arrived"].includes(request.status)) return false; if (request && typeof rememberRiderDismissedRequest === "function") { rememberRiderDismissedRequest(request, currentRiderRecord()); } if (request && typeof rememberRiderPrePickupCancellationClear === "function") { rememberRiderPrePickupCancellationClear(request, currentRiderRecord()); } state.riderPage = "requests"; state.selectedRequestId = null; state.offers = state.offers.filter((offer) => offer.requestId !== requestId); state.requests = state.requests.filter((item) => item.id !== requestId); clearStateLookupIndexes(); if (typeof resetRideDockPanels === "function") resetRideDockPanels(); if (typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute("requests", { replace: true, requestId: "" }); } saveState(); if (typeof returnRiderToMarketplace === "function") { returnRiderToMarketplace({ replace: true, refresh }); } else { renderAll(); if (refresh) void refreshMarketplace({ silent: true, reason: "rider_cancel_force_clear" }); } return true; } async function rideLifecycleAlreadyAppliedAfterRefresh(requestId, actionName, previousRequest = null) { if (!hasSupabaseRuntime() || !["arrive", "start", "stop", "complete", "cancel"].includes(actionName)) return false; await refreshMarketplace({ silent: true, reason: "ride_lifecycle_idempotent_recheck" }); if (actionName === "cancel" && reconcileAppliedRiderPrePickupCancellation(requestId, previousRequest)) return true; const refreshed = state.requests.find((item) => item.id === requestId); if (!rideLifecycleTargetAlreadyReached(refreshed, actionName, previousRequest)) return false; renderAll(); return true; } function clearRiderSelectedTerminalRide(requestId, actionName) { if (activeRole() !== "rider" || !["cancel", "complete"].includes(actionName)) return; const request = state.requests.find((item) => item.id === requestId); if (!request || !["completed", "cancelled"].includes(request.status)) return; if (state.selectedRequestId !== requestId) return; state.selectedRequestId = null; if (typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute("requests", { replace: true, requestId: "" }); } } function rideLifecycleNotificationEventTypes(actionName) { return { arrive: ["ride_arrived"], start: ["ride_started"], stop: ["ride_stop_arrived"], complete: ["ride_completed"], cancel: ["ride_cancelled"] }[actionName] ?? []; } function createLocalRideSettlement(request, fareAmount = agreedFareForRequest(request)) { if (!request || request.status === "completed" || rideSettlementRecords().some((settlement) => settlement.requestId === request.id)) return; const breakdown = rideFinancialBreakdown({ ...request, agreedFare: fareAmount, acceptedRouteChangeFare: 0 }); state.rideSettlements = upsertById(state.rideSettlements, { id: makeId("settlement"), requestId: request.id, passengerId: request.passengerId, passengerName: request.passengerName, riderId: selectedRiderIdForRequest(request), riderName: selectedRiderNameForRequest(request) ?? "Rider", fareAmount: Number(fareAmount || 0), stripeFeeAmount: centsToDollars(breakdown.stripeFeeCents), facilitationFeeAmount: centsToDollars(breakdown.facilitationFeeCents), businessServiceFeeAmount: centsToDollars(breakdown.businessServiceFeeCents), riderPayoutAmount: centsToDollars(breakdown.riderPayoutCents), status: "pending_provider_payout", providerReference: "", createdAt: new Date().toISOString() }); } async function changeRideLifecycle(requestId, actionName, reason = "") { const request = state.requests.find((item) => item.id === requestId); if (!request) return false; const actorRole = activeRole(); const actorId = currentActorIdForChat(); if ( actorRole === "rider" && ["start", "stop", "complete"].includes(actionName) && riderIdentityMatches(selectedRiderIdForRequest(request)) ) { const pendingChange = pendingRouteChangeForRequest(request); if (pendingChange) { showRiderRouteChangeDecisionForRequest(request, pendingChange); void showWakaGoodAlert("Review the passenger route change before proceeding. Accept to update navigation, or decline to keep the current route."); return false; } } if (actionName === "cancel" && actorRole === "rider") { riderInitiatedRideCancellationRequestIds.add(requestId); } const riderPrePickupCancellationReopensRequest = actionName === "cancel" && actorRole === "rider" && ["matched", "arrived"].includes(request.status); const lifecycleKey = `${requestId}:${actionName}`; if (rideLifecycleActionInFlight.has(lifecycleKey)) return false; const cancellationSettlementEstimate = actionName === "cancel" && ((actorRole === "passenger" && ["matched", "arrived"].includes(request.status)) || (actorRole === "passenger" && request.status === "in_progress")) ? rideCancellationCompensationEstimate(request, Date.now(), actorRole) : null; try { rideLifecycleActionInFlight.add(lifecycleKey); const lifecycleGps = await validatedLifecycleGpsPoint(request, actionName); const savedRequest = await changeRideStateInSupabase(requestId, actionName, reason, lifecycleGps); updateRequestById(requestId, (item) => { const localState = applyRideLifecycleState(item, actionName, reason, actorRole, actorId); return savedRequest?.id === requestId ? { ...localState, ...savedRequest } : localState; }); void recordInsuranceTelemetryTransitionInSupabase( savedRequest?.id === requestId ? savedRequest : request, actionName, lifecycleGps ); } catch (error) { if (/(Ride state change not allowed|Updating the ride status is taking too long)/i.test(error.message) && await rideLifecycleAlreadyAppliedAfterRefresh(requestId, actionName, request)) { return true; } if (/(Ride state change not allowed|Updating the ride status is taking too long)/i.test(error.message) && actionName === "cancel" && activeRole() === "rider" && ["matched", "arrived"].includes(request.status) && clearRiderPrePickupCancellationView(requestId, request)) { return true; } void showWakaGoodAlert(error.message); return false; } finally { rideLifecycleActionInFlight.delete(lifecycleKey); } let settlementMessage = ""; if (actionName === "complete") { createLocalRideSettlement(request); if (!ridePaymentProviderSupportsOnline()) { settlementMessage = "Direct cash or mobile-money payment is handled between passenger and rider. Waka commission is handled through the rider wallet after the free period."; } else { try { const settlement = await processRidePaymentSettlement(requestId); if (settlement?.ok) { settlementMessage = settlement.alreadySettled ? "Stripe ride payment was already settled." : settlement.chargeHeld ? `Passenger card was charged. Rider payout ${formatMoney(centsToDollars(settlement.riderPayoutCents || 0), request.country)} is held until the rider finishes Stripe payout setup or admin releases it.` : `Stripe ride payment processed. Rider payout: ${formatMoney(centsToDollars(settlement.riderPayoutCents || 0), request.country)} after estimated Stripe fee ${formatMoney(centsToDollars(settlement.stripeFeeCents || 0), request.country)}.`; } } catch (error) { settlementMessage = `Stripe ride payment needs admin review: ${error.message}`; void showWakaGoodAlert(settlementMessage); logClientWarning("Completed ride payment settlement failed.", error); } } } if (actionName === "cancel" && cancellationSettlementEstimate?.amount > 0) { createLocalRideSettlement(request, cancellationSettlementEstimate.amount); if (!ridePaymentProviderSupportsOnline()) { settlementMessage = "Cancellation compensation is recorded for admin review; no automatic card charge runs in the Cameroon direct-payment MVP."; } else { try { const settlement = await processRidePaymentSettlement(requestId); if (settlement?.ok) { settlementMessage = settlement.alreadySettled ? "Cancellation compensation payment was already settled." : settlement.chargeHeld ? `Passenger card was charged for cancellation compensation. Rider payout ${formatMoney(centsToDollars(settlement.riderPayoutCents || 0), request.country)} is held until Stripe payout setup or admin release.` : `Cancellation compensation payment processed. Rider payout: ${formatMoney(centsToDollars(settlement.riderPayoutCents || 0), request.country)} after estimated Stripe fee ${formatMoney(centsToDollars(settlement.stripeFeeCents || 0), request.country)}.`; } } catch (error) { settlementMessage = `Cancellation compensation payment needs admin review: ${error.message}`; void showWakaGoodAlert(settlementMessage); logClientWarning("Ride cancellation compensation payment settlement failed.", error); } } } pushSystemChat(requestId, rideLifecycleMessage(request, actionName, actorRole)); if (settlementMessage) pushSystemChat(requestId, settlementMessage); clearRiderSelectedTerminalRide(requestId, actionName); if (actionName === "complete" && typeof openQueuedRiderPickupAfterDropoff === "function") { openQueuedRiderPickupAfterDropoff(requestId); } saveState(); if (riderPrePickupCancellationReopensRequest) { clearRiderPrePickupCancellationView(requestId, request); } else { renderAll(); void refreshMarketplace({ silent: true }); } const lifecycleEventTypes = riderPrePickupCancellationReopensRequest ? ["ride_reopened"] : rideLifecycleNotificationEventTypes(actionName); if (lifecycleEventTypes.length) { await processRideRequestPushDelivery(requestId, { eventTypes: lifecycleEventTypes }); } return true; } async function cancelRideBeforeStart(requestId) { let request = state.requests.find((item) => item.id === requestId); const originalRequest = request; if (activeRole() === "rider" && hasSupabaseRuntime()) { try { await refreshMarketplace({ silent: true, reason: "rider_cancel_precheck" }); request = state.requests.find((item) => item.id === requestId); if (!canCancelBeforeStart(request)) { clearRiderPrePickupCancellationView(requestId, originalRequest ?? request ?? { id: requestId, status: "matched" }); return; } } catch (error) { logClientWarning("Marketplace refresh before rider cancellation was skipped.", error); } } if (!canCancelBeforeStart(request)) return; if (activeRole() === "passenger") { const estimate = passengerCancellationFeeEstimate(request); if (estimate.amount > 0) { const ok = await showWakaGoodConfirm(`Cancelling now may charge ${formatMoney(estimate.amount, request.country)} to compensate the rider for ${estimate.elapsedMinutes} minute${estimate.elapsedMinutes === 1 ? "" : "s"} since match. Continue?`); if (!ok) return; } } const reason = await showWakaGoodPrompt("Optional cancellation reason", ""); if (reason === null) return; await changeRideLifecycle(requestId, "cancel", reason.trim()); } async function cancelRideInProgress(requestId) { const request = state.requests.find((item) => item.id === requestId); if (!canCancelInProgress(request)) return; const actorRole = activeRole(); const estimate = actorRole === "rider" ? riderCancellationNoPassengerChargeEstimate(request) : inProgressCancellationCompensationEstimate(request); const ok = actorRole === "rider" ? await showWakaGoodConfirm("End this ride now? Passenger will not be charged because you are cancelling from the rider side.") : await showWakaGoodConfirm(`Cancel this ride now? Waka will charge the passenger a partial fare of ${formatMoney(estimate.amount, request.country)} based on about ${estimate.elapsedMinutes} minute${estimate.elapsedMinutes === 1 ? "" : "s"} after pickup, then route that payout through Stripe.`); if (!ok) return; const reason = await showWakaGoodPrompt("Optional cancellation reason", ""); if (reason === null) return; await changeRideLifecycle(requestId, "cancel", reason.trim()); } async function requestScheduledRideConfirmation(requestId) { const request = state.requests.find((item) => item.id === requestId); if (!request || !requestBelongsToPassenger(request) || !isScheduledRequest(request) || request.status !== "matched") return; if (hasSupabaseRuntime()) { try { await callSupabaseRpc( "request_scheduled_ride_confirmation", { request_id: requestId }, "Requesting scheduled ride confirmation", optionalSupabaseRequestTimeoutMs ); } catch (error) { translatedAlert("requestConfirmationFailed", { message: error.message }); return; } } updateRequestById(requestId, (item) => ({ ...item, riderConfirmationStatus: "requested", riderConfirmationRequestedAt: new Date().toISOString() })); pushSystemChat(requestId, `Passenger requested rider confirmation for the scheduled ride on ${formatDateTime(request.scheduledAt)}.`); saveState(); renderAll(); } async function confirmScheduledRide(requestId) { const request = state.requests.find((item) => item.id === requestId); if (!request || selectedRiderIdForRequest(request) !== state.rider?.id || request.riderConfirmationStatus !== "requested") return; if (hasSupabaseRuntime()) { try { await callSupabaseRpc( "rider_confirm_scheduled_ride", { request_id: requestId }, "Confirming the scheduled ride", optionalSupabaseRequestTimeoutMs ); } catch (error) { translatedAlert("confirmScheduledFailed", { message: error.message }); return; } } updateRequestById(requestId, (item) => ({ ...item, riderConfirmationStatus: "confirmed", riderConfirmedAt: new Date().toISOString() })); pushSystemChat(requestId, `Rider confirmed the scheduled ride for ${formatDateTime(request.scheduledAt)}.`); saveState(); renderAll(); } async function releaseScheduledRide(requestId, message) { const request = state.requests.find((item) => item.id === requestId); if (!request || !isScheduledRequest(request) || request.status !== "matched") return; const allowed = (activeRole() === "passenger" && requestBelongsToPassenger(request)) || (activeRole() === "rider" && selectedRiderIdForRequest(request) === state.rider?.id); if (!allowed) return; if (hasSupabaseRuntime()) { try { await callSupabaseRpc( "release_scheduled_ride_match", { request_id: requestId }, "Reopening the scheduled ride", optionalSupabaseRequestTimeoutMs ); } catch (error) { translatedAlert("reopenScheduledFailed", { message: error.message }); return; } } updateRequestById(requestId, (item) => ({ ...item, status: "open", selectedOfferId: null, agreedFare: null, selectedRiderId: null, selectedRiderName: null, riderConfirmationStatus: "released", riderConfirmationRequestedAt: null, riderConfirmedAt: null, releasedAt: new Date().toISOString() })); pushSystemChat(requestId, message); saveState(); renderAll(); } async function sendChat(event) { event.preventDefault(); const request = selectedRequest(); const text = els.chatInput.value.trim(); if (!request || !canChatOnRequest(request) || !text) return; const sender = activeRole(); const message = { id: makeId("chat"), requestId: request.id, senderId: currentActorIdForChat(), sender, text, deliveryStatus: "sending", createdAt: new Date().toISOString() }; state.chats.push(message); els.chatInput.value = ""; saveState(); renderChat(); try { const savedMessage = await saveChatMessageToSupabase(message, { throwOnError: true }); const deliveredMessage = savedMessage?.id ? { ...savedMessage, deliveryStatus: "sent" } : { ...message, deliveryStatus: "sent" }; state.chats = upsertById(state.chats.filter((item) => item.id !== message.id), deliveredMessage); saveState(); renderChat(); void relayRideChatMessageToPhone(message); if (els.chatStatus) els.chatStatus.textContent = "Open - message sent"; if (typeof forceMarketplaceRefreshSoon === "function") forceMarketplaceRefreshSoon("ride_chat_sent"); } catch (error) { state.chats = state.chats.map((item) => item.id === message.id ? { ...item, deliveryStatus: "failed", deliveryError: error.message } : item); saveState(); renderChat(); if (els.chatStatus) els.chatStatus.textContent = `Open - message failed: ${error.message}`; } } async function submitSafetyReport(event) { event.preventDefault(); const request = reportableRideForRole(); if (!canReportOnRequest(request)) { setTranslatedStatus(els.safetyReportStatus, "safetyReportUnavailable"); return; } const details = els.safetyReportDetails.value.trim(); if (details.length < 10) { setTranslatedStatus(els.safetyReportStatus, "safetyReportNeedsDetail"); return; } const reporterId = currentActorIdForChat(); if (!reporterId) { setTranslatedStatus(els.safetyReportStatus, "safetyReportSignInRequired"); return; } const target = reportTargetForRequest(request); const report = { id: makeId("report"), requestId: request.id, reporterId, reporterName: activeRole() === "passenger" ? state.passenger?.name : state.rider?.name, reporterRole: activeRole(), reportedUserId: target.id, reportedUserName: target.name, category: els.safetyReportCategory.value, severity: els.safetyReportSeverity.value, details, status: "open", routeSummary: `${request.pickupArea} to ${requestDestinationText(request)}`, createdAt: new Date().toISOString() }; try { setTranslatedStatus(els.safetyReportStatus, hasSupabaseRuntime() ? "submittingSafetySupabase" : "savingSafetyReport"); const savedReport = await saveSafetyReportToSupabase(report); state.safetyReports = upsertById(state.safetyReports, savedReport); els.safetyReportDetails.value = ""; saveState(); renderAll(); setTranslatedStatus(els.safetyReportStatus, "safetyReportSubmitted"); } catch (error) { setTranslatedStatus(els.safetyReportStatus, "safetyReportFailed", { message: error.message }); } } function supportTicketConfirmationMessage(ticket) { const reference = String(ticket?.id ?? "") .replace(/[^a-z0-9]/gi, "") .slice(0, 8) .toUpperCase(); return `Delivered to Waka admin inbox${reference ? ` (Ref ${reference})` : ""}.`; } async function submitAccountSupportTicket(event) { event.preventDefault(); const form = event.currentTarget; const type = form?.dataset.supportType === "rider" ? "rider" : "passenger"; const account = type === "rider" ? state.rider : state.passenger; const session = state.sessions[type]; const categoryInput = els[`${type}SupportCategory`]; const subjectInput = els[`${type}SupportSubject`]; const messageInput = els[`${type}SupportMessage`]; const status = els[`${type}SupportStatus`]; if (!account?.id || !session) { if (status) { status.classList.remove("success-status"); status.classList.add("error-status"); status.textContent = "Sign in before contacting Waka support."; } return; } const message = messageInput?.value.trim() ?? ""; const subject = subjectInput?.value.trim() || `${type === "rider" ? "Rider" : "Passenger"} support request`; if (message.length < 10) { if (status) { status.classList.remove("success-status"); status.classList.add("error-status"); status.textContent = "Add enough detail for Waka admin to understand the request."; } return; } const ticket = { id: makeId("support"), accountId: account.id, accountRole: type, accountName: account.name || account.email || account.phone || type, category: categoryInput?.value || "other", subject, message, priority: categoryInput?.value === "safety" ? "high" : "medium", status: "open", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; try { if (status) { status.classList.remove("success-status", "error-status"); status.textContent = hasSupabaseRuntime() ? "Sending support request to Waka admin..." : "Saving support request..."; } const savedTicket = await saveSupportTicketToSupabase(ticket); state.supportTickets = upsertById(state.supportTickets, savedTicket); if (subjectInput) subjectInput.value = ""; if (messageInput) messageInput.value = ""; saveState(); if (status) { status.classList.remove("error-status"); status.classList.add("success-status"); status.textContent = supportTicketConfirmationMessage(savedTicket); } } catch (error) { if (status) { status.classList.remove("success-status"); status.classList.add("error-status"); status.textContent = `Could not send support request: ${error.message}`; } } } async function submitRideRating(event) { event.preventDefault(); const initialRequest = typeof selectedWorkspaceRequest === "function" ? selectedWorkspaceRequest() : selectedRequest(); if (els.rideRatingStatus && hasSupabaseRuntime()) { els.rideRatingStatus.textContent = "Checking completed ride details before submitting..."; } const { request, target } = await resolveRideRatingContext(initialRequest); if (!(request && request.status === "completed" && roleCanSeeRequest(request) && target && !existingRatingForRequest(request))) { els.rideRatingStatus.textContent = "Ratings open after selecting a completed ride that has not already been rated."; return; } const reviewerId = currentActorIdForChat(); const categoryScore = (element) => Number(element?.value || els.rideRatingScore.value); const rating = { id: makeId("rating"), requestId: request.id, reviewerId, reviewerRole: activeRole(), reviewerName: activeRole() === "passenger" ? state.passenger?.name : state.rider?.name, ratedUserId: target.id, ratedUserName: target.name, score: Number(els.rideRatingScore.value), safetyScore: categoryScore(els.rideRatingSafety), punctualityScore: categoryScore(els.rideRatingPunctuality), communicationScore: categoryScore(els.rideRatingCommunication), vehicleScore: categoryScore(els.rideRatingVehicle), comment: els.rideRatingComment.value.trim(), createdAt: new Date().toISOString() }; try { els.rideRatingStatus.textContent = hasSupabaseRuntime() ? "Submitting rating to Supabase..." : "Saving rating..."; const savedRating = await saveRideRatingToSupabase(rating); state.rideRatings = upsertById(state.rideRatings, savedRating); if (state.rider?.id === savedRating.ratedUserId && typeof loadMyRiderRatingSummaryFromSupabase === "function") { await loadMyRiderRatingSummaryFromSupabase(); } if (state.rider?.id === savedRating.ratedUserId) state.rider.rating = ratingSummaryForRider(state.rider.id); state.riders = state.riders.map((rider) => ( rider.id === savedRating.ratedUserId ? { ...rider, rating: ratingSummaryForRider(rider.id) } : rider )); els.rideRatingComment.value = ""; saveState(); renderAll(); els.rideRatingStatus.textContent = "Rating submitted. Thank you for helping accountability."; } catch (error) { els.rideRatingStatus.textContent = `Could not submit rating: ${error.message}`; } } async function submitRideTip(event, requestId) { event.preventDefault(); const form = event.currentTarget; const status = form.querySelector(".ride-tip-status"); const input = form.querySelector(".ride-tip-input"); const request = state.requests.find((item) => item.id === requestId); if (!canTipRequest(request)) { status.textContent = "Tips are available once, after a completed passenger ride."; return; } const amount = Number(String(input.value).replace(/[^\d.]/g, "")); if (!Number.isFinite(amount) || amount <= 0) { status.textContent = "Enter a tip amount greater than $0."; return; } const tipCents = dollarsToCents(amount); const stripeFeeCents = stripeProcessingFeeCents(tipCents); const tip = { id: makeId("tip"), requestId: request.id, passengerId: request.passengerId, passengerName: request.passengerName, riderId: selectedRiderIdForRequest(request), riderName: selectedRiderNameForRequest(request) ?? "Rider", amount: centsToDollars(tipCents), stripeFeeAmount: centsToDollars(stripeFeeCents), riderPayoutAmount: centsToDollars(Math.max(0, tipCents - stripeFeeCents)), status: "pending_provider_payout", providerReference: "", createdAt: new Date().toISOString() }; try { status.textContent = hasSupabaseRuntime() ? "Submitting tip through Supabase..." : "Saving tip..."; const savedTip = await saveRideTipToSupabase(tip); state.rideTips = upsertById(state.rideTips, savedTip); input.value = ""; saveState(); renderAll(); } catch (error) { status.textContent = `Could not submit tip: ${error.message}`; } } // Passenger-facing workspace, ride request, offer review, map, chat, and account UI. let passengerNearbyRiderCountsTimer = null; let passengerNearbyRiderCountsRequestId = 0; let passengerNearbyRiderCountsAutoTimer = null; let passengerNearbyRiderCountsLastRefreshAt = 0; let passengerWorkspacePageSelectedInSession = false; let passengerFareBoostLastFocus = null; let passengerOfferCounterLastFocus = null; let passengerFareBoostOpenRequestId = null; let passengerRideDockRequestId = null; let passengerRideDockOpenPanel = null; let passengerRideOptionLastPointerAt = 0; let passengerFareModeLastPointerAt = 0; let passengerAddStopLastPointerAt = 0; let riderRideDockRequestId = null; let riderRideDockOpenPanel = null; const recentAddressConfirmationPromises = new Set(); const rideRequestOptionPanelNames = ["stops", "timing", "vehicle", "fare"]; function updateRidePaymentOptions(country = selectedPassengerCountry()) { if (!els.paymentPreference) return; const options = ridePaymentOptionsForCountry(country); const selectedValue = validPaymentPreferenceForCountry(els.paymentPreference.value, country); populateSelectOptions(els.paymentPreference, options, selectedValue); } function updateScheduledRideControls() { if (!els.rideTiming || !els.scheduledAt) return; const scheduled = els.rideTiming.value === "scheduled"; const field = els.scheduledAtField ?? els.scheduledAt.closest("label"); if (field) field.hidden = !scheduled; els.scheduledAt.disabled = !scheduled; if (!scheduled) els.scheduledAt.value = ""; } function openScheduledDateTimePicker() { if (!els.rideTiming || !els.scheduledAt || els.rideTiming.value !== "scheduled" || els.scheduledAt.disabled) return; window.setTimeout(() => { els.scheduledAt.focus(); try { els.scheduledAt.showPicker?.(); } catch { // Some mobile browsers block programmatic picker opening; focus still reveals the input. } }, 0); } function rideRequestOptionTarget(name) { if (name === "stops") return { button: els.addRideStop, panel: els.rideStopsPanel, focus: els.rideStops }; if (name === "timing") return { button: els.toggleRideTiming, panel: els.rideTimingPanel, focus: els.rideTiming }; if (name === "vehicle") return { button: els.toggleVehiclePreference, panel: els.vehiclePreferencePanel, focus: els.vehiclePreference }; if (name === "fare") return { button: els.toggleFareDetails, panel: els.fareDetailsPanel, focus: els.fareOffer }; return { button: null, panel: null, focus: null }; } function negotiatedFareInlineActive() { return Boolean(passengerFareMode() === "negotiable" && els.destination?.value.trim()); } function setRideRequestOptionPanelState(name, expanded, { focus = false } = {}) { const target = rideRequestOptionTarget(name); const isExpanded = Boolean(expanded); if (target.panel) target.panel.hidden = !isExpanded; if (target.button) { target.button.classList.toggle("active", isExpanded); target.button.setAttribute("aria-expanded", isExpanded ? "true" : "false"); } if (focus && isExpanded && target.focus instanceof HTMLElement) { window.setTimeout(() => target.focus.focus(), 0); } } function closeRideRequestOptionPanel(name) { if (name === "fare" && negotiatedFareInlineActive()) return; const target = rideRequestOptionTarget(name); if (target.panel) target.panel.hidden = true; if (target.button) { target.button.classList.remove("active"); target.button.setAttribute("aria-expanded", "false"); } } function closeOtherRideRequestOptionPanels(activeName) { rideRequestOptionPanelNames .filter((name) => name !== activeName) .forEach(closeRideRequestOptionPanel); } function setRideRequestOptionPanel(name, expanded, { focus = false } = {}) { const isExpanded = Boolean(expanded); if (isExpanded) closeOtherRideRequestOptionPanels(name); setRideRequestOptionPanelState(name, isExpanded, { focus }); } function syncNegotiatedFareInlinePanel({ focus = false } = {}) { const inline = negotiatedFareInlineActive(); els.fareDetailsPanel?.classList.toggle("negotiated-fare-inline", inline); if (inline) { setRideRequestOptionPanelState("fare", true, { focus }); } else if (passengerFareMode() === "negotiable") { setRideRequestOptionPanelState("fare", false); } } function toggleRideRequestOptionPanel(name, { focus = true } = {}) { const target = rideRequestOptionTarget(name); const nextExpanded = !target.panel || target.panel.hidden; setRideRequestOptionPanel(name, nextExpanded, { focus }); } function initializeRideRequestOptionPanels() { setRideRequestOptionPanel("stops", Boolean(els.rideStops && !els.rideStops.disabled && normalizeRideStops(els.rideStops.value).length)); setRideRequestOptionPanel("timing", els.rideTiming?.value === "scheduled"); setRideRequestOptionPanel("vehicle", normalizeCarTypePreference(els.vehiclePreference?.value) === "suv"); setRideRequestOptionPanel("fare", false); } function handleRideRequestOptionToggle(name, event) { const now = Date.now(); if (event?.type === "click" && now - passengerRideOptionLastPointerAt < 700) { event.preventDefault?.(); return; } if (event?.type && event.type !== "click" && event.type !== "keydown") { if (now - passengerRideOptionLastPointerAt < 250) { event.preventDefault?.(); return; } passengerRideOptionLastPointerAt = now; } event?.preventDefault?.(); toggleRideRequestOptionPanel(name); } function handleRideRequestOptionKeyToggle(name, event) { if (!["Enter", " "].includes(event?.key)) return; handleRideRequestOptionToggle(name, event); } function activateRideStopTool(event) { const now = Date.now(); if (event?.type === "click" && now - passengerAddStopLastPointerAt < 700) { event.preventDefault?.(); return; } if (event?.type && event.type !== "click" && event.type !== "keydown") { if (now - passengerAddStopLastPointerAt < 250) { event.preventDefault?.(); return; } passengerAddStopLastPointerAt = now; } handleAddRideStop(event); } function activateRideStopToolFromKey(event) { if (!["Enter", " "].includes(event?.key)) return; activateRideStopTool(event); } function handleRideTimingChange() { setRideRequestOptionPanel("timing", true); updateScheduledRideControls(); openScheduledDateTimePicker(); } function setPassengerRiderAvailabilityMessage(message) { if (!els.passengerRiderAvailability) return; els.passengerRiderAvailability.textContent = message; } function renderPassengerRiderAvailabilityCounts(row) { if (!els.passengerRiderAvailability) return; const normal = Math.max(0, Number(row?.normal_count ?? row?.normalCount ?? 0) || 0); const special = Math.max(0, Number(row?.special_count ?? row?.specialCount ?? 0) || 0); const total = Math.max(0, Number(row?.total_count ?? row?.totalCount ?? normal + special) || 0); els.passengerRiderAvailability.innerHTML = ` ${escapeHtml(String(normal))} Normal available ${escapeHtml(String(special))} XL/Special available ${escapeHtml(String(total))} total near pickup `; } function passengerAvailabilityPickupPoint() { if (!state.passenger) return null; const pickupGps = passengerPickupGpsForFormChoice(); const origin = routeOriginForEstimate( state.passenger.country, state.passenger.city, els.pickupArea?.value, els.pickupDescription?.value, pickupGps ); return requestPickupGpsFromRouteOrigin(origin, pickupGps); } async function refreshPassengerNearbyRiderCounts() { if (!els.passengerRiderAvailability) return null; if (!hasSupabaseRuntime() || !state.passenger || !hasSignedIn("passenger")) { setPassengerRiderAvailabilityMessage("Sign in and set pickup to see nearby rider availability."); return null; } const pickupPoint = passengerAvailabilityPickupPoint(); if (!pickupPoint) { setPassengerRiderAvailabilityMessage("Enter a pickup address or check exact current location to see nearby rider availability."); return null; } const requestId = ++passengerNearbyRiderCountsRequestId; passengerNearbyRiderCountsLastRefreshAt = Date.now(); setPassengerRiderAvailabilityMessage("Checking available riders near pickup..."); try { const rows = await callSupabaseRpcResult( "passenger_nearby_rider_counts", { p_country: state.passenger.country, p_city: state.passenger.city, p_pickup_lat: pickupPoint.latitude, p_pickup_lng: pickupPoint.longitude }, "Checking nearby rider availability", optionalSupabaseRequestTimeoutMs ); if (requestId !== passengerNearbyRiderCountsRequestId) return null; const row = Array.isArray(rows) ? rows[0] : rows; renderPassengerRiderAvailabilityCounts(row); return row; } catch (error) { if (requestId === passengerNearbyRiderCountsRequestId) { setPassengerRiderAvailabilityMessage("Nearby rider counts are temporarily unavailable."); } logClientWarning("Nearby rider availability count could not be loaded.", error); return null; } } function schedulePassengerNearbyRiderCountsRefresh(delayMs = 350) { window.clearTimeout(passengerNearbyRiderCountsTimer); passengerNearbyRiderCountsTimer = window.setTimeout(() => { void refreshPassengerNearbyRiderCounts() .finally(() => ensurePassengerNearbyRiderCountsAutoRefresh()); }, delayMs); } function passengerNearbyRiderCountsShouldAutoRefresh() { return Boolean( activeRole() === "passenger" && !document.hidden && hasSignedIn("passenger") && passengerWorkspacePage() === "request" && passengerAvailabilityPickupPoint() ); } function stopPassengerNearbyRiderCountsAutoRefresh() { if (passengerNearbyRiderCountsAutoTimer == null) return; window.clearTimeout(passengerNearbyRiderCountsAutoTimer); passengerNearbyRiderCountsAutoTimer = null; } function ensurePassengerNearbyRiderCountsAutoRefresh() { if (!passengerNearbyRiderCountsShouldAutoRefresh()) { stopPassengerNearbyRiderCountsAutoRefresh(); return; } if (passengerNearbyRiderCountsAutoTimer != null) return; const elapsedMs = passengerNearbyRiderCountsLastRefreshAt ? Date.now() - passengerNearbyRiderCountsLastRefreshAt : passengerNearbyRiderCountsRefreshIntervalMs; const delayMs = Math.max( 500, passengerNearbyRiderCountsRefreshIntervalMs - Math.max(0, elapsedMs) ); passengerNearbyRiderCountsAutoTimer = window.setTimeout(() => { passengerNearbyRiderCountsAutoTimer = null; if (!passengerNearbyRiderCountsShouldAutoRefresh()) return; void refreshPassengerNearbyRiderCounts() .finally(() => ensurePassengerNearbyRiderCountsAutoRefresh()); }, delayMs); } function renderNotificationPreferenceControls(list, type) { if (!list || typeof notificationPreferenceOptions === "undefined") return; const panel = document.createElement("article"); panel.className = "notice-item notification-preferences"; const allEnabled = notificationPreferenceEnabled(type, "all"); const typeOptions = notificationPreferenceOptions.filter((option) => option.key !== "all"); const choices = typeOptions.map((option) => { const checked = notificationPreferenceEnabled(type, option.key) ? " checked" : ""; const disabled = allEnabled ? "" : " disabled"; return ` `; }).join(""); panel.innerHTML = ` Notification preferences

Mute all notifications or choose which updates can interrupt this device. Ride records still stay in this notices list.

${choices}
`; panel.querySelectorAll("[data-notification-preference]").forEach((input) => { input.addEventListener("change", () => { setNotificationPreference(type, input.dataset.notificationPreference, input.checked); }); }); list.append(panel); } function renderAccountNotices(type) { const panel = type === "passenger" ? els.passengerNoticePanel : els.riderNoticePanel; const list = type === "passenger" ? els.passengerNoticeList : els.riderNoticeList; const signedIn = type === "passenger" ? Boolean(state.sessions.passenger && state.passenger) : Boolean(state.sessions.rider && state.rider); if (!signedIn) panel.hidden = true; if (signedIn && type === "rider") panel.hidden = riderWorkspacePage() !== "notices"; if (typeof updatePushNotificationControls === "function") updatePushNotificationControls(type); list.innerHTML = ""; if (!signedIn) return; renderNotificationPreferenceControls(list, type); const notices = currentAccountNotifications(type); if (typeof refreshAccountNotificationsFromSupabase === "function") { const activeNoticePage = type === "passenger" ? passengerWorkspacePage() === "notices" : riderWorkspacePage() === "notices"; const shouldRefresh = true; if (shouldRefresh) { void refreshAccountNotificationsFromSupabase(type, { force: activeNoticePage }).then(() => { const latest = currentAccountNotifications(type); if (latest.length !== notices.length) renderAccountNotices(type); }); } } if (!notices.length) { list.append(emptyState("No notices for this account.")); return; } notices.slice(0, 5).forEach((notice) => { const item = document.createElement("article"); item.className = "notice-item"; item.innerHTML = ` ${escapeHtml(notice.title)}

${escapeHtml(notice.body)}

${formatDateTime(notice.createdAt)} - ${escapeHtml(notificationDeliveryLabel(notice.deliveryChannels))} `; list.append(item); }); } function renderBusinessAccountPanel() { if (!els.businessAccountForm) return; const passengerSignedIn = Boolean(state.sessions.passenger && state.passenger); if (!passengerSignedIn) { els.businessAccountForm.hidden = true; return; } const accounts = passengerBusinessAccounts(); els.businessAccountStatus.textContent = accounts.length ? businessAccountSummary(accounts[0]) : `Business accounts require Waka verification before ride billing. Verified businesses get ${businessFreeTrialDays} days free; after that Starter adds ${Math.round(businessRideServiceFeeRate * 100)}% per completed ride or Partner is ${formatMoney(businessPartnerMonthlySubscriptionFee)}/month with no ${Math.round(businessRideServiceFeeRate * 100)}% ride fee.`; els.businessAccountList.innerHTML = ""; renderReferralPanel("business"); if (!accounts.length) { els.businessAccountList.append(emptyState("No business account is linked to this passenger yet.")); return; } accounts.forEach((account) => { const item = document.createElement("article"); item.className = "notice-item"; const subscription = businessSubscriptionFor(account.id); const canStartPartnerCheckout = account.status === "active" && account.verificationStatus === "verified" && !businessSubscriptionIsActive(subscription); item.innerHTML = ` ${escapeHtml(account.businessName)}

${escapeHtml(businessAccountSummary(account))}

${escapeHtml(account.billingEmail)} - ${escapeHtml(businessPlanLabel(account.planCode))} - ${businessAccountCanRequest(account) ? "Business rides active" : "Admin review or billing required"} ${escapeHtml(businessFreeTrialText(account))} ${account.businessAddress ? `${escapeHtml(account.businessAddress)}` : ""} ${canStartPartnerCheckout ? `
` : ""} `; item.querySelector(".business-subscription-start")?.addEventListener("click", () => startBusinessSubscriptionCheckout(account.id)); els.businessAccountList.append(item); }); } function updateBusinessBillingOptions() { if (!els.rideBillingAccount) return; const selected = automaticRideBillingAccountId() ?? els.rideBillingAccount.value; els.rideBillingAccount.innerHTML = ""; const personal = document.createElement("option"); personal.value = ""; personal.textContent = "Personal ride"; els.rideBillingAccount.append(personal); passengerBusinessAccounts().forEach((account) => { const option = document.createElement("option"); option.value = account.id; option.textContent = businessAccountCanRequest(account) ? `Business: ${account.businessName} (${businessPlanLabel(account.planCode)})` : `Business review: ${account.businessName}`; option.disabled = !businessAccountCanRequest(account); option.selected = selected === account.id && !option.disabled; els.rideBillingAccount.append(option); }); if (selected && [...els.rideBillingAccount.options].some((option) => option.value === selected && !option.disabled)) { els.rideBillingAccount.value = selected; } else { els.rideBillingAccount.value = ""; } } function automaticRideBillingAccountId() { if (!passengerBusinessWorkspaceEnabled()) return null; return passengerBusinessAccounts().find((account) => businessAccountCanRequest(account))?.id ?? null; } const accountModeSelectedThisSession = { passenger: false, rider: false }; const passengerProfilePhotoUrlCache = new Map(); const passengerFareBoostDrafts = new Map(); const passengerOfferCounterDrafts = new Map(); const passengerDestinationUpdateDrafts = new Map(); const passengerDestinationUpdateEditPreserveMs = 5000; function passengerDestinationUpdateDraftKey(requestOrId) { const id = typeof requestOrId === "string" ? requestOrId : requestOrId?.id; return id ? `${id}:route-update` : ""; } function passengerDestinationUpdateCurrentDestination(request) { return String(request?.destinationFormattedAddress || request?.destination || ""); } function rememberPassengerDestinationUpdateDraft(form) { const key = form?.dataset?.routeChangeRequestId; if (!key) return; const destinationInput = form.querySelector(".destination-update-input"); const stopsInput = form.querySelector(".stops-update-input"); passengerDestinationUpdateDrafts.set(key, { destination: String(destinationInput?.value ?? ""), stops: String(stopsInput?.value ?? ""), destinationPlace: normalizedPlaceSelection(form.__destinationUpdatePlace) }); } function markPassengerDestinationUpdateEditing(form) { if (!form) return; form.dataset.routeChangeEditingAt = String(Date.now()); rememberPassengerDestinationUpdateDraft(form); } function rememberPassengerDestinationUpdateDraftInputs() { document.querySelectorAll(".destination-update-form[data-route-change-request-id]").forEach(rememberPassengerDestinationUpdateDraft); } function passengerDestinationUpdateFocusedFieldSnapshot(container = document) { const active = document.activeElement; if (!(active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement)) return null; const form = active.closest(".destination-update-form[data-route-change-request-id]"); if (!form || !container.contains(form)) return null; const field = active.classList.contains("destination-update-input") ? "destination" : active.classList.contains("stops-update-input") ? "stops" : null; if (!field) return null; return { requestId: form.dataset.routeChangeRequestId || "", field, selectionStart: active.selectionStart, selectionEnd: active.selectionEnd, selectionDirection: active.selectionDirection || "none" }; } function passengerDestinationUpdateFocusedFormForRequest(request, container = document) { const active = document.activeElement; const requestId = String(request?.id ?? ""); if (!requestId || !container) return null; if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) { const form = active.closest(".destination-update-form[data-route-change-request-id]"); if (form && container.contains(form) && form.dataset.requestId === requestId) return form; } const now = Date.now(); return [...container.querySelectorAll(".destination-update-form[data-route-change-request-id]")] .find((form) => form.dataset.requestId === requestId && now - Number(form.dataset.routeChangeEditingAt || 0) <= passengerDestinationUpdateEditPreserveMs) ?? null; } function restorePassengerDestinationUpdateFocus(snapshot, container = document) { if (!snapshot?.requestId || !container) return; const form = [...container.querySelectorAll(".destination-update-form[data-route-change-request-id]")] .find((item) => item.dataset.routeChangeRequestId === snapshot.requestId); const control = form?.querySelector(snapshot.field === "stops" ? ".stops-update-input" : ".destination-update-input"); if (!(control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement)) return; try { control.focus({ preventScroll: true }); } catch { control.focus(); } if (typeof control.setSelectionRange !== "function") return; const valueLength = control.value.length; const start = Math.max(0, Math.min(valueLength, Number(snapshot.selectionStart ?? valueLength))); const end = Math.max(start, Math.min(valueLength, Number(snapshot.selectionEnd ?? start))); try { control.setSelectionRange(start, end, snapshot.selectionDirection || "none"); } catch { control.setSelectionRange(start, end); } } function clearPassengerDestinationUpdateDraft(requestOrId) { const key = passengerDestinationUpdateDraftKey(requestOrId); if (key) passengerDestinationUpdateDrafts.delete(key); if (typeof requestOrId === "string") passengerDestinationUpdateDrafts.delete(requestOrId); } function passengerDestinationUpdateDraftForRequest(request) { const key = passengerDestinationUpdateDraftKey(request); if (!key || !passengerDestinationUpdateDrafts.has(key)) { return { destination: passengerDestinationUpdateCurrentDestination(request), stops: "", destinationPlace: normalizedPlaceSelection({ placeId: request?.destinationPlaceId, displayName: request?.destination, formattedAddress: request?.destinationFormattedAddress || request?.destination, latitude: request?.destinationLatitude, longitude: request?.destinationLongitude }) }; } const draft = passengerDestinationUpdateDrafts.get(key); return { destination: String(draft?.destination ?? ""), stops: String(draft?.stops ?? ""), destinationPlace: normalizedPlaceSelection(draft?.destinationPlace) }; } function rememberPassengerFareDraftInputs() { document.querySelectorAll(".fare-boost-input[data-request-id]").forEach((input) => { if (input.dataset.requestId) passengerFareBoostDrafts.set(input.dataset.requestId, input.value); }); document.querySelectorAll(".offer-counter-input[data-offer-counter-key]").forEach((input) => { if (input.dataset.offerCounterKey) passengerOfferCounterDrafts.set(input.dataset.offerCounterKey, input.value); }); rememberPassengerDestinationUpdateDraftInputs(); } function passengerFareBoostFocusedInputSnapshot(container = document) { const active = document.activeElement; if (!(active instanceof HTMLInputElement)) return null; if (!active.classList.contains("fare-boost-input")) return null; if (!container?.contains(active)) return null; const requestId = active.dataset.requestId || ""; if (!requestId) return null; return { requestId, selectionStart: active.selectionStart, selectionEnd: active.selectionEnd, selectionDirection: active.selectionDirection || "none" }; } function rememberPassengerFareBoostFocus(input) { if (!(input instanceof HTMLInputElement)) return; if (!input.classList.contains("fare-boost-input")) return; const requestId = input.dataset.requestId || ""; if (!requestId) return; passengerFareBoostLastFocus = { requestId, selectionStart: input.selectionStart, selectionEnd: input.selectionEnd, selectionDirection: input.selectionDirection || "none", touchedAt: Date.now() }; } function passengerFareBoostFocusSnapshot(container = document) { const focused = passengerFareBoostFocusedInputSnapshot(container); if (focused) { passengerFareBoostLastFocus = { ...focused, touchedAt: Date.now() }; return focused; } return null; } function restorePassengerFareBoostFocus(snapshot, container = document) { if (!snapshot?.requestId || !container) return; const control = [...container.querySelectorAll(".fare-boost-input[data-request-id]")] .find((input) => input.dataset.requestId === snapshot.requestId); if (!(control instanceof HTMLInputElement)) return; try { control.focus({ preventScroll: true }); } catch { control.focus(); } passengerFareBoostLastFocus = { ...snapshot, touchedAt: Date.now() }; if (typeof control.setSelectionRange !== "function") return; if (!["text", "search", "tel", "url", "password"].includes(control.type)) return; const valueLength = control.value.length; const start = Math.max(0, Math.min(valueLength, Number(snapshot.selectionStart ?? valueLength))); const end = Math.max(start, Math.min(valueLength, Number(snapshot.selectionEnd ?? start))); try { control.setSelectionRange(start, end, snapshot.selectionDirection || "none"); } catch { try { control.setSelectionRange(start, end); } catch { // Mobile keyboards may reject selection restoration even when focus restoration works. } } } function passengerOfferCounterFocusedInputSnapshot(container = document) { const active = document.activeElement; if (!(active instanceof HTMLInputElement)) return null; if (!active.classList.contains("offer-counter-input")) return null; if (!container?.contains(active)) return null; const key = active.dataset.offerCounterKey || ""; if (!key) return null; return { key, selectionStart: active.selectionStart, selectionEnd: active.selectionEnd, selectionDirection: active.selectionDirection || "none" }; } function rememberPassengerOfferCounterFocus(input) { if (!(input instanceof HTMLInputElement)) return; if (!input.classList.contains("offer-counter-input")) return; const key = input.dataset.offerCounterKey || ""; if (!key) return; passengerOfferCounterLastFocus = { key, selectionStart: input.selectionStart, selectionEnd: input.selectionEnd, selectionDirection: input.selectionDirection || "none", touchedAt: Date.now() }; } function passengerOfferCounterFocusSnapshot(container = document) { const focused = passengerOfferCounterFocusedInputSnapshot(container); if (focused) { passengerOfferCounterLastFocus = { ...focused, touchedAt: Date.now() }; return focused; } return null; } function restorePassengerOfferCounterFocus(snapshot, container = document) { if (!snapshot?.key || !container) return; const control = [...container.querySelectorAll(".offer-counter-input[data-offer-counter-key]")] .find((input) => input.dataset.offerCounterKey === snapshot.key); if (!(control instanceof HTMLInputElement)) return; try { control.focus({ preventScroll: true }); } catch { control.focus(); } passengerOfferCounterLastFocus = { ...snapshot, touchedAt: Date.now() }; if (typeof control.setSelectionRange !== "function") return; if (!["text", "search", "tel", "url", "password"].includes(control.type)) return; const valueLength = control.value.length; const start = Math.max(0, Math.min(valueLength, Number(snapshot.selectionStart ?? valueLength))); const end = Math.max(start, Math.min(valueLength, Number(snapshot.selectionEnd ?? start))); try { control.setSelectionRange(start, end, snapshot.selectionDirection || "none"); } catch { try { control.setSelectionRange(start, end); } catch { // Some mobile keyboards expose a text input without allowing programmatic selection. } } } function chatInputFocusSnapshot() { const input = els.chatInput; if (!(input instanceof HTMLInputElement) && !(input instanceof HTMLTextAreaElement)) return null; if (document.activeElement !== input) return null; const request = selectedRequest(); return { requestId: request?.id || "", value: input.value, selectionStart: input.selectionStart, selectionEnd: input.selectionEnd, selectionDirection: input.selectionDirection || "none" }; } function restoreChatInputFocus(snapshot, request, shouldRestore) { const input = els.chatInput; if (!snapshot?.requestId || !request?.id || snapshot.requestId !== request.id || !shouldRestore) return; if (!(input instanceof HTMLInputElement) && !(input instanceof HTMLTextAreaElement)) return; if (input.disabled || els.chatForm?.hidden) return; if (input.value !== snapshot.value) input.value = snapshot.value; try { input.focus({ preventScroll: true }); } catch { input.focus(); } if (typeof input.setSelectionRange !== "function") return; const valueLength = input.value.length; const start = Math.max(0, Math.min(valueLength, Number(snapshot.selectionStart ?? valueLength))); const end = Math.max(start, Math.min(valueLength, Number(snapshot.selectionEnd ?? start))); try { input.setSelectionRange(start, end, snapshot.selectionDirection || "none"); } catch { try { input.setSelectionRange(start, end); } catch { // Mobile keyboards may preserve focus but reject selection restoration. } } } function setPassengerProfileAvatarFallback(passenger = state.passenger) { if (!els.passengerProfileAvatar) return; els.passengerProfileAvatar.textContent = passengerInitials(passenger); } async function ensurePassengerProfilePhotoUrl(passenger = state.passenger) { if (!els.passengerProfileAvatar || !passenger?.profilePhotoPath || !isSupabaseMode() || !supabaseClient) return; const cacheKey = passenger.profilePhotoPath; const cached = passengerProfilePhotoUrlCache.get(cacheKey); if (cached) { els.passengerProfileAvatar.innerHTML = ``; return; } try { const { data, error } = await supabaseClient.storage .from(appConfig.buckets.profilePhotos) .createSignedUrl(passenger.profilePhotoPath, 600); if (error || !data?.signedUrl) throw error || new Error("Signed profile photo URL was not returned."); passengerProfilePhotoUrlCache.set(cacheKey, data.signedUrl); if (state.passenger?.profilePhotoPath === passenger.profilePhotoPath) { els.passengerProfileAvatar.innerHTML = ``; } } catch (error) { logClientWarning("Passenger profile picture could not be displayed.", error); setPassengerProfileAvatarFallback(passenger); } } const referralCodeCache = { passenger: null, rider: null, business: null }; const referralCodeLoads = { passenger: null, rider: null, business: null }; const referralCodeErrors = { passenger: null, rider: null, business: null }; function normalizeReferralCodeRow(row) { const item = Array.isArray(row) ? row[0] : row; return item?.code ? { code: String(item.code), ownerRole: item.owner_role ?? item.ownerRole ?? "", status: item.status ?? "active" } : null; } async function loadReferralCodeForRole(role, options = {}) { const force = options.force === true; if (!hasSupabaseRuntime() || !referralRoleSignedIn(role)) return null; if (!force && referralCodeCache[role]) return referralCodeCache[role]; if (!referralCodeLoads[role]) { referralCodeErrors[role] = null; referralCodeLoads[role] = callSupabaseRpcResult( "referral_code_for_user", { p_owner_role: role }, "Loading referral code", optionalSupabaseRequestTimeoutMs ) .then((row) => { referralCodeCache[role] = normalizeReferralCodeRow(row); return referralCodeCache[role]; }) .catch((error) => { referralCodeErrors[role] = compactUserMessage(error, "Referral code could not be loaded."); logClientWarning(`${role} referral code could not be loaded.`, error); return null; }) .finally(() => { referralCodeLoads[role] = null; }); } return referralCodeLoads[role]; } function referralCodeInputForRole(role) { if (role === "business") return els.businessReferralCode; return role === "rider" ? els.riderReferralCode : els.passengerReferralCode; } function referralRoleSignedIn(role) { if (role === "business") return Boolean(hasSignedIn("passenger") && passengerBusinessAccounts().length); return hasSignedIn(role); } function compactUserMessage(error, fallback) { const message = String(error?.message || error || fallback || "Something went wrong.").trim(); return message.length > 180 ? `${message.slice(0, 177)}...` : message; } async function claimReferralCodeValue(role, codeValue, statusEl = null) { const code = String(codeValue ?? "").trim(); if (!code || !hasSupabaseRuntime()) return null; try { const result = await callSupabaseRpcResult( "claim_referral_code", { p_code: code, p_referred_role: role }, "Claiming referral code", optionalSupabaseRequestTimeoutMs ); if (statusEl) statusEl.textContent = `${statusEl.textContent} Referral code applied.`; return result; } catch (error) { logClientWarning(`${role} referral code could not be claimed.`, error); if (statusEl) statusEl.textContent = `${statusEl.textContent} Referral code was not applied: ${error.message}`; return null; } } async function claimReferralCodeForRole(role, statusEl = null) { const input = referralCodeInputForRole(role); const result = await claimReferralCodeValue(role, input?.value, statusEl); if (result && input) input.value = ""; return result; } function referralPanelElements(role) { if (role === "business") { return { panel: els.businessReferralPanel, summary: els.businessReferralSummary, display: els.businessReferralCodeDisplay, copy: els.copyBusinessReferralCode, share: els.shareBusinessReferralCode, email: els.emailBusinessReferralCode, text: els.textBusinessReferralCode, how: els.businessReferralHowItWorks }; } return role === "rider" ? { panel: els.riderReferralPanel, summary: els.riderReferralSummary, display: els.riderReferralCodeDisplay, copy: els.copyRiderReferralCode, share: els.shareRiderReferralCode, email: els.emailRiderReferralCode, text: els.textRiderReferralCode, how: els.riderReferralHowItWorks } : { panel: els.passengerReferralPanel, summary: els.passengerReferralSummary, display: els.passengerReferralCodeDisplay, copy: els.copyPassengerReferralCode, share: els.sharePassengerReferralCode, email: els.emailPassengerReferralCode, text: els.textPassengerReferralCode, how: els.passengerReferralHowItWorks }; } function referralLinkForRole(role, code) { if (role === "business") return `${window.location.origin}/passenger?passengerPage=business&ref=${encodeURIComponent(code)}`; const path = role === "rider" ? "/rider" : "/passenger"; return `${window.location.origin}${path}?ref=${encodeURIComponent(code)}`; } function referralShareMessage(role, code) { const link = referralLinkForRole(role, code); if (role === "business") return `Create a Waka business account with my invite code ${code}: ${link}`; return role === "rider" ? `Join Waka as a rider with my invite code ${code}: ${link}` : `Try Waka with my invite code ${code}: ${link}`; } function referralRewardExplanation(role) { if (role === "business") return "Share your business invite link with hotels, clinics, employers, schools, venues, and other organizations. Waka records business invite relationships for admin review, credits, and partner-benefit decisions after verification."; return role === "rider" ? "Share your rider invite link by text, email, social share, or copy. Rewards post after the referred rider is approved and completes five paid rides; Waka records the reward in the referral ledger for admin review and payout/subscription-credit handling." : "Share your passenger invite link by text, email, social share, or copy. Ride credits post after the referred passenger creates an account and completes a paid ride; Waka records both the inviter and new-passenger credits in the referral ledger."; } function setReferralPanelStatus(elements, message) { if (elements?.summary) elements.summary.textContent = message; } function preserveReferralWorkspacePage(role) { if (role === "business") { state.activeTab = "passenger"; state.passengerPage = "business"; updatePassengerWorkspaceRoute("business", { replace: true }); saveState(); return; } if (role === "rider") { state.activeTab = "rider"; state.riderPage = "rewards"; updateRiderWorkspaceRoute("rewards", { replace: true }); saveState(); return; } if (role === "passenger") { state.activeTab = "passenger"; state.passengerPage = "rewards"; updatePassengerWorkspaceRoute("rewards", { replace: true }); saveState(); } } function referralActionLink(role, action, code) { const subject = role === "rider" ? "Join Waka as a rider" : role === "business" ? "Create a Waka business account" : "Try Waka"; const message = referralShareMessage(role, code); if (action === "email") return `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(message)}`; if (action === "text") return `sms:?&body=${encodeURIComponent(message)}`; return referralLinkForRole(role, code); } async function copyTextToClipboard(text) { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return true; } const textarea = document.createElement("textarea"); textarea.value = text; textarea.setAttribute("readonly", ""); textarea.style.position = "fixed"; textarea.style.left = "-9999px"; textarea.style.top = "0"; document.body.append(textarea); textarea.select(); let copied = false; try { copied = document.execCommand("copy"); } finally { textarea.remove(); } if (!copied) throw new Error("Clipboard copy is not available in this browser."); return true; } async function ensureReferralForAction(role, elements) { const cached = referralCodeCache[role]; if (cached?.code) return cached; setReferralPanelStatus(elements, "Loading your invite code..."); const loaded = await loadReferralCodeForRole(role, { force: true }); if (loaded?.code) { renderReferralPanel(role); return loaded; } const message = referralCodeErrors[role] || "Referral code is not available yet. Try again after the page finishes syncing."; setReferralPanelStatus(elements, message); throw new Error(message); } function setReferralShareTargets(role, code, elements) { const applyLinkTargets = (nextCode) => { if (!nextCode) { [elements.email, elements.text].forEach((linkElement) => { if (!linkElement) return; linkElement.href = "#"; linkElement.setAttribute("aria-disabled", "true"); }); return; } const message = referralShareMessage(role, nextCode); const subject = role === "rider" ? "Join Waka as a rider" : role === "business" ? "Create a Waka business account" : "Try Waka"; if (elements.email) { elements.email.href = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(message)}`; elements.email.removeAttribute("aria-disabled"); } if (elements.text) { elements.text.href = `sms:?&body=${encodeURIComponent(message)}`; elements.text.removeAttribute("aria-disabled"); } }; applyLinkTargets(code); } async function runReferralShareAction(role, action) { const elements = referralPanelElements(role); preserveReferralWorkspacePage(role); try { const referral = await ensureReferralForAction(role, elements); const link = referralLinkForRole(role, referral.code); const subject = role === "rider" ? "Join Waka as a rider" : role === "business" ? "Create a Waka business account" : "Try Waka"; const message = referralShareMessage(role, referral.code); if (action === "copy") { await copyTextToClipboard(link); setReferralPanelStatus(elements, `Copied invite link for ${referral.code}.`); return; } if (action === "share") { if (navigator.share) { try { await navigator.share({ title: subject, text: message, url: link }); setReferralPanelStatus(elements, `Shared invite link for ${referral.code}.`); return; } catch (error) { if (error?.name === "AbortError") { preserveReferralWorkspacePage(role); return; } } } await copyTextToClipboard(link); setReferralPanelStatus(elements, `Share sheet is not available here, so Waka copied ${referral.code}.`); return; } if (action === "email" || action === "text") { const actionLink = referralActionLink(role, action, referral.code); setReferralPanelStatus(elements, `${action === "email" ? "Opening email" : "Opening text message"} for ${referral.code}.`); window.location.href = actionLink; } } catch (error) { const fallbackCode = referralCodeCache[role]?.code; if (fallbackCode && (action === "copy" || action === "share")) { const link = referralLinkForRole(role, fallbackCode); setReferralPanelStatus(elements, link); } else { setReferralPanelStatus(elements, compactUserMessage(error, "Invite action could not be completed.")); } } finally { preserveReferralWorkspacePage(role); } } function handleReferralActionClick(event) { const target = event.target?.closest?.("[data-referral-role][data-referral-action]"); if (!target) return false; const role = String(target.dataset.referralRole || "").trim().toLowerCase(); const action = String(target.dataset.referralAction || "").trim().toLowerCase(); if (!["passenger", "rider", "business"].includes(role) || !["copy", "share", "email", "text"].includes(action)) return false; event.preventDefault(); event.stopPropagation(); if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation(); void runReferralShareAction(role, action); return true; } function renderReferralPanel(role) { const elements = referralPanelElements(role); const { panel, summary, display, copy, share, email, text, how } = elements; if (!panel) return; const onReferralPage = role === "business" ? passengerWorkspacePage() === "business" : role === "rider" ? riderWorkspacePage() === "rewards" : passengerWorkspacePage() === "rewards"; const signedIn = referralRoleSignedIn(role); panel.hidden = !signedIn || !onReferralPage; if (!signedIn || !onReferralPage) return; const referral = referralCodeCache[role]; if (!referral) { summary.textContent = role === "business" ? "Give this code to organizations creating Waka business accounts. Admin can track business referrals after verification." : role === "rider" ? "Give this code to new riders. Rewards are credited after the referred rider completes the required launch rides." : "Give this code to new passengers. Ride credits are credited after the referred passenger completes a paid ride."; if (referralCodeErrors[role]) { summary.textContent = `${referralCodeErrors[role]} Tap Copy link or Share to try loading it again.`; } display.textContent = "Loading code"; [copy, share].forEach((button) => { if (button) button.disabled = false; }); if (how) how.textContent = referralRewardExplanation(role); setReferralShareTargets(role, null, elements); if (!referralCodeErrors[role] && !referralCodeLoads[role]) { void loadReferralCodeForRole(role).then(() => renderReferralPanel(role)); } return; } summary.textContent = role === "business" ? "Business referral benefits are tracked after Waka verifies the referred organization." : role === "rider" ? "Rider rewards unlock after a referred rider completes five paid rides." : "Passenger ride credits unlock after a referred passenger completes a paid ride."; if (how) how.textContent = referralRewardExplanation(role); display.textContent = referral.code; [copy, share].forEach((button) => { if (button) button.disabled = false; }); [email, text].forEach((link) => { if (!link) return; link.removeAttribute("aria-disabled"); }); setReferralShareTargets(role, referral.code, elements); } const passengerWorkspacePages = ["request", "trips", "payment", "business", "rewards", "profile", "notices", "support"]; const passengerWorkspacePageLabels = { request: "Ride request", trips: "My trips", payment: "Payment", business: "Business", rewards: "Rewards", profile: "Profile", notices: "Notices", support: "Support" }; const riderWorkspacePages = ["overview", "initialize", "checks", "requests", "destination", "earnings", "payment", "ratings", "rewards", "notices", "support", "profile"]; const riderWorkspacePageLabels = { overview: "Overview", initialize: "Initialize rider availability", checks: "Eligibility checks", requests: "Ride requests", destination: "Destination", earnings: "Earnings", payment: "Payment account", ratings: "Ratings", rewards: "Rewards", notices: "Notices", support: "Support", profile: "Profile" }; const riderWorkspacePageSummaries = { overview: "Review rider readiness, account status, wallet or payout setup, and next steps.", initialize: "Set today's service region, preferred destinations, and online availability.", checks: "Monitor rider application review, eligibility, and background-check progress.", requests: "Review incoming passenger requests, accept the passenger fare, or send a higher counter-offer.", destination: "Choose an optional destination preference for the rider marketplace.", earnings: "Review completed rides, wallet/direct-payment status, payout status, and earnings by period.", payment: "Review rider wallet mode or update payout setup when an online provider is enabled.", ratings: "View anonymous passenger rating percentages and category counts.", rewards: "Share Waka with passengers or riders and track referral rewards.", notices: "Read Waka notices and enable phone notifications for rider updates.", support: "Send rider support requests directly to Waka admin.", profile: "Review rider identity, vehicle details, profile picture, and navigation preference." }; function availableRiderWorkspacePages(rider = currentRiderRecord()) { if (!rider) return riderWorkspacePages; if (rider.status === "approved") return riderWorkspacePages; if (rider.status === "pending" || rider.status === "background_pending") return ["checks"]; if (rider.status === "needs_correction") return ["profile"]; if (rider.needsApplication || rider.status === "profile only") return ["profile"]; if (rider.status === "declined" || rider.status === "suspended") return ["checks"]; return ["checks"]; } function passengerBusinessWorkspaceEnabled(passenger = state.passenger) { return Boolean(passenger?.accountUse === "business" || passengerBusinessAccounts(passenger).length); } function availablePassengerWorkspacePages() { return passengerWorkspacePages.filter((page) => page !== "business" || passengerBusinessWorkspaceEnabled()); } function passengerWorkspacePage() { const pages = availablePassengerWorkspacePages(); const requestedPage = requestedPassengerWorkspacePageFromLocation(); const requestedTripDeepLink = requestedPage === "trips" && Boolean(requestedRideRequestIdFromLocation()); const shouldHonorRequestedPage = !hasSignedIn("passenger") || passengerWorkspacePageSelectedInSession || requestedTripDeepLink; if (requestedPage && pages.includes(requestedPage) && shouldHonorRequestedPage) return requestedPage; if (hasSignedIn("passenger") && pages.includes(state.passengerPage)) return state.passengerPage; if (!hasSignedIn("passenger") && pages.includes(state.passengerPage)) return state.passengerPage; return "request"; } function passengerWorkspaceRouteForPage(page, requestId = "", { preferPathRoute = false } = {}) { if (!passengerWorkspacePages.includes(page)) return null; const pathTab = routePathTab(); const shouldUsePassengerPath = pathTab === "passenger" || (preferPathRoute && typeof publicHomePathIsActive === "function" && publicHomePathIsActive()); if (shouldUsePassengerPath) { const params = new URLSearchParams(window.location.search); params.set("passengerPage", page); if (requestId) params.set("requestId", requestId); if (!requestId && page !== "trips") params.delete("requestId"); const query = params.toString(); const pathname = pathTab === "passenger" ? window.location.pathname : "/passenger"; return `${pathname}${query ? `?${query}` : ""}`; } const params = new URLSearchParams(window.location.search); ["passengerPage", "passenger_page", "requestId", "rideRequestId", "ride_request_id"].forEach((key) => params.delete(key)); const cleanQuery = params.toString(); const query = requestId ? `?requestId=${encodeURIComponent(requestId)}` : ""; return `${window.location.pathname}${cleanQuery ? `?${cleanQuery}` : ""}#passenger/${page}${query}`; } function updatePassengerWorkspaceRoute(page, { replace = false, requestId = "", preferPathRoute = false } = {}) { const route = passengerWorkspaceRouteForPage(page, requestId, { preferPathRoute }); if (!route) return; const current = `${window.location.pathname}${window.location.search}${window.location.hash}`; if (route === current) return; window.history[replace ? "replaceState" : "pushState"]({ passengerPage: page, requestId }, "", route); } function setPassengerWorkspacePage(page) { if (!availablePassengerWorkspacePages().includes(page)) return; passengerWorkspacePageSelectedInSession = true; state.passengerPage = page; els.passengerWorkspaceMenu?.removeAttribute("open"); updatePassengerWorkspaceRoute(page); saveState(); renderAll(); } function riderWorkspacePage() { const pages = availableRiderWorkspacePages(); const requestedPage = requestedRiderWorkspacePageFromLocation(); if (requestedPage && pages.includes(requestedPage)) return requestedPage; if (hasSignedIn("rider") && pages.includes(state.riderPage)) { const rider = currentRiderRecord(); if (state.riderPage === "checks" && rider?.status === "approved" && isSubscriptionActive(rider)) { return pages.includes("initialize") ? "initialize" : state.riderPage; } return state.riderPage; } if (hasSignedIn("rider")) return riderWorkspaceLandingPageAfterSignIn(currentRiderRecord()); if (pages.includes(state.riderPage)) return state.riderPage; return pages.includes("overview") ? "overview" : pages[0] ?? "overview"; } function riderWorkspaceRouteForPage(page, requestId = "") { if (!riderWorkspacePages.includes(page)) return null; if (routePathTab() === "rider") { const params = new URLSearchParams(window.location.search); params.set("riderPage", page); if (requestId) params.set("requestId", requestId); if (!requestId) params.delete("requestId"); const query = params.toString(); return `${window.location.pathname}${query ? `?${query}` : ""}${window.location.hash || ""}`; } const params = new URLSearchParams(window.location.search); ["riderPage", "rider_page", "requestId", "rideRequestId", "ride_request_id"].forEach((key) => params.delete(key)); const cleanQuery = params.toString(); const query = requestId ? `?requestId=${encodeURIComponent(requestId)}` : ""; return `${window.location.pathname}${cleanQuery ? `?${cleanQuery}` : ""}#rider/${page}${query}`; } function updateRiderWorkspaceRoute(page, { replace = false, requestId = "" } = {}) { const route = riderWorkspaceRouteForPage(page, requestId); if (!route) return; const current = `${window.location.pathname}${window.location.search}${window.location.hash}`; if (route === current) return; window.history[replace ? "replaceState" : "pushState"]({ riderPage: page, requestId }, "", route); } function riderWorkspaceLandingPageAfterSignIn(rider = currentRiderRecord(), startingApplication = false, { honorRequestedPage = true } = {}) { const pages = availableRiderWorkspacePages(rider); const requestedPage = honorRequestedPage ? requestedRiderWorkspacePageFromLocation() : ""; if (requestedPage && pages.includes(requestedPage)) return requestedPage; if (startingApplication || rider?.needsApplication || rider?.status === "profile only" || rider?.status === "needs_correction") { return pages.includes("profile") ? "profile" : pages[0] ?? "overview"; } if (rider?.status === "pending" || rider?.status === "background_pending" || rider?.status === "declined" || rider?.status === "suspended") { return pages.includes("checks") ? "checks" : pages[0] ?? "overview"; } return pages.includes("initialize") ? "initialize" : pages.includes("overview") ? "overview" : pages[0] ?? "overview"; } function setRiderWorkspaceLandingAfterSignIn(startingApplication = false, { honorRequestedPage = false, replaceRoute = true, restoreStoredWorkspace = false } = {}) { if (restoreStoredWorkspace && !startingApplication && typeof restoreWorkspaceUiState === "function" && restoreWorkspaceUiState("rider", { replaceRoute })) { saveState(); return; } state.riderPage = riderWorkspaceLandingPageAfterSignIn(currentRiderRecord(), startingApplication, { honorRequestedPage }); if (replaceRoute && typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute(state.riderPage, { replace: true }); } saveState(); } function requestedRiderWorkspacePageFromLocation() { const hashParts = window.location.hash.replace(/^#\/?/, "").toLowerCase().split(/[/?&=]/).filter(Boolean); const riderIndex = hashParts.indexOf("rider"); const hashPage = riderIndex >= 0 ? hashParts[riderIndex + 1] : ""; if (riderWorkspacePages.includes(hashPage)) return hashPage; const params = new URLSearchParams(window.location.search); const page = String(params.get("riderPage") ?? params.get("rider_page") ?? "").trim().toLowerCase(); if (riderWorkspacePages.includes(page)) return page; return null; } function requestedPassengerWorkspacePageFromLocation() { const hashParts = window.location.hash.replace(/^#\/?/, "").toLowerCase().split(/[/?&=]/).filter(Boolean); const passengerIndex = hashParts.indexOf("passenger"); const hashPage = passengerIndex >= 0 ? hashParts[passengerIndex + 1] : ""; if (passengerWorkspacePages.includes(hashPage)) return hashPage; const params = new URLSearchParams(window.location.search); const page = String(params.get("passengerPage") ?? params.get("passenger_page") ?? "").trim().toLowerCase(); if (passengerWorkspacePages.includes(page)) return page; return null; } function requestedRideRequestIdFromLocation() { const params = new URLSearchParams(window.location.search); const requestId = String(params.get("requestId") ?? params.get("rideRequestId") ?? params.get("ride_request_id") ?? "").trim(); if (requestId) return requestId; const hash = window.location.hash; if (!hash.includes("?")) return null; const hashParams = new URLSearchParams(hash.slice(hash.indexOf("?") + 1)); const hashRequestId = String(hashParams.get("requestId") ?? hashParams.get("rideRequestId") ?? hashParams.get("ride_request_id") ?? "").trim(); return hashRequestId || null; } function riderPageSectionIncludes(sectionValue, page = riderWorkspacePage()) { return String(sectionValue || "").split(/\s+/).includes(page); } function setRiderWorkspacePage(page) { if (!riderWorkspacePages.includes(page)) return; const availablePages = availableRiderWorkspacePages(); if (!availablePages.includes(page)) { page = riderWorkspaceLandingPageAfterSignIn(currentRiderRecord(), false, { honorRequestedPage: false }); } state.riderPage = page; els.riderWorkspaceMenu?.removeAttribute("open"); updateRiderWorkspaceRoute(page); saveState(); renderAll(); if (page === "requests") void refreshMarketplace({ silent: true }); if (page === "notices") { void refreshAccountNotificationsFromSupabase("rider", { force: true }).then(() => renderAccountNotices("rider")); } } function passengerInitials(passenger = state.passenger) { const name = String(passenger?.name ?? passenger?.email ?? passenger?.phone ?? "Passenger").trim(); const parts = name.split(/\s+/).filter(Boolean); return (parts.length > 1 ? `${parts[0][0]}${parts[1][0]}` : parts[0]?.slice(0, 2) || "P").toUpperCase(); } function updatePassengerInitialBusinessFields() { if (!els.passengerInitialBusinessFields || !els.passengerAccountUse) return; const wantsBusiness = els.passengerAccountUse.value === "business"; els.passengerInitialBusinessFields.hidden = !wantsBusiness; const personalIdentityFields = document.querySelector("#passengerPersonalIdentityFields"); if (personalIdentityFields) personalIdentityFields.hidden = wantsBusiness; [els.passengerNationalId, els.passengerDob].forEach((input) => { if (!input) return; input.required = !wantsBusiness; if (wantsBusiness) input.value = ""; }); } function renderPassengerWorkspacePages(passengerSignedIn) { const passengerPasswordResetActive = typeof passwordResetModeActive === "function" && passwordResetModeActive("passenger"); passengerSignedIn = Boolean(passengerSignedIn && !passengerPasswordResetActive); const page = passengerWorkspacePage(); state.passengerPage = page; if (!passengerSignedIn) els.passengerWorkspaceMenu?.removeAttribute("open"); if (els.passengerPanelHeading) els.passengerPanelHeading.hidden = passengerSignedIn; if (els.passengerWorkspaceHeader) { els.passengerWorkspaceHeader.hidden = !passengerSignedIn; els.passengerWorkspaceHeader.classList.toggle("request-minimal", passengerSignedIn && page === "request"); } if (els.passengerWorkspaceTitle) els.passengerWorkspaceTitle.textContent = passengerWorkspacePageLabels[page] ?? "Ride request"; if (els.passengerWorkspaceMenu) els.passengerWorkspaceMenu.hidden = !passengerSignedIn; if (els.passengerWorkspaceNav) { els.passengerWorkspaceNav.querySelectorAll("[data-passenger-page]").forEach((button) => { const available = availablePassengerWorkspacePages().includes(button.dataset.passengerPage); button.hidden = !available; const active = button.dataset.passengerPage === page; button.classList.toggle("active", active); button.setAttribute("aria-pressed", String(active)); }); } document.querySelectorAll("[data-passenger-page-section]").forEach((section) => { section.hidden = !passengerSignedIn || section.dataset.passengerPageSection !== page; }); if (!passengerSignedIn) return; const paymentReady = paymentAccountReady("passenger", state.passenger); if (els.passengerRideGate) { els.passengerRideGate.textContent = paymentReady ? "Ready to publish." : "Add a passenger payment method under Payment before publishing."; } } function renderRiderWorkspacePages(riderSignedIn) { const riderPasswordResetActive = typeof passwordResetModeActive === "function" && passwordResetModeActive("rider"); riderSignedIn = Boolean(riderSignedIn && !riderPasswordResetActive); const page = riderWorkspacePage(); state.riderPage = page; if (riderSignedIn && typeof scheduleRiderProfileHydration === "function") { scheduleRiderProfileHydration("rider workspace render"); } if (!riderSignedIn) els.riderWorkspaceMenu?.removeAttribute("open"); if (els.riderWorkspaceHeader) els.riderWorkspaceHeader.hidden = !riderSignedIn; if (els.riderWorkspaceTitle) els.riderWorkspaceTitle.textContent = riderWorkspacePageLabels[page] ?? "Overview"; if (els.riderWorkspaceSummary) els.riderWorkspaceSummary.textContent = riderWorkspacePageSummaries[page] ?? riderWorkspacePageSummaries.overview; if (els.riderWorkspaceMenu) els.riderWorkspaceMenu.hidden = !riderSignedIn; if (els.riderWorkspaceNav) { const availablePages = availableRiderWorkspacePages(); els.riderWorkspaceNav.querySelectorAll("[data-rider-page]").forEach((button) => { button.hidden = !availablePages.includes(button.dataset.riderPage); const active = button.dataset.riderPage === page; button.classList.toggle("active", active); button.setAttribute("aria-pressed", String(active)); }); } document.querySelectorAll("[data-rider-page-section]").forEach((section) => { section.hidden = !riderSignedIn || !riderPageSectionIncludes(section.dataset.riderPageSection, page); }); if (riderSignedIn && page === "destination") renderRiderDestinationFilterControls(); } function ensureAccountChoiceState(type, signedIn) { if (signedIn) return; if (requestedTabFromLocation() !== type) return; const routeMode = requestedAccountModeFromLocation(type); if (routeMode) { state.accountMode[type] = routeMode; accountModeSelectedThisSession[type] = true; return; } if (accountModeSelectedThisSession[type]) return; state.accountMode[type] = accountMode(type); } function renderAccountWorkspaces() { const passengerPasswordResetActive = typeof passwordResetModeActive === "function" && passwordResetModeActive("passenger"); const passengerSignedIn = Boolean(state.sessions.passenger && state.passenger && !passengerPasswordResetActive); if (passengerPasswordResetActive) state.accountMode.passenger = "signin"; ensureAccountChoiceState("passenger", passengerSignedIn); if (els.passengerSignInOtpPanel) els.passengerSignInOtpPanel.hidden = !phoneOtpSignInEnabled(); renderAccountModeButtons("passenger", passengerSignedIn); if (typeof updatePasswordResetFormMode === "function") updatePasswordResetFormMode("passenger", passengerPasswordResetActive); els.passengerSignInForm.hidden = passengerPasswordResetActive ? false : passengerSignedIn || accountMode("passenger") !== "signin"; els.passengerAccountForm.hidden = passengerPasswordResetActive || passengerSignedIn || accountMode("passenger") !== "create"; if (els.passengerPasswordResetPanel) { els.passengerPasswordResetPanel.hidden = passengerPasswordResetActive ? false : els.passengerPasswordResetPanel.hidden || passengerSignedIn || accountMode("passenger") !== "signin"; } updatePassengerProfileRecoveryFormControls(pendingProfileRecoveryForRole("passenger")); els.passengerSessionCard.hidden = !passengerSignedIn; renderPassengerWorkspacePages(passengerSignedIn); if (passengerSignedIn) { els.passengerSessionTitle.textContent = state.passenger.name || "Passenger signed in"; const passengerIdentity = state.sessions.passenger.email ?? state.passenger.email ?? state.passenger.phone ?? "Secure session"; const passengerLocation = [state.passenger.city, state.passenger.country].filter(Boolean).join(", "); const paymentSummary = paymentAccountReady("passenger", state.passenger) ? "Payment ready" : "Payment method needed"; els.passengerSessionSummary.textContent = `${passengerIdentity}${passengerLocation ? ` - ${passengerLocation}` : ""}. ${paymentSummary}.`; setPassengerProfileAvatarFallback(state.passenger); if (state.passenger.profilePhotoPath) void ensurePassengerProfilePhotoUrl(state.passenger); if (els.passengerProfilePhotoStatus) { els.passengerProfilePhotoStatus.textContent = state.passenger.profilePhotoPath || state.passenger.profilePhotoName ? `Profile picture: ${state.passenger.profilePhotoName || "uploaded"}` : "Profile picture not uploaded."; } els.passengerPaymentStatus.textContent = paymentAccountSummary("passenger", state.passenger); } renderRecentAddressShortcuts(); renderBusinessAccountPanel(); updateBusinessBillingOptions(); renderAccountNotices("passenger"); renderReferralPanel("passenger"); const riderPasswordResetActive = typeof passwordResetModeActive === "function" && passwordResetModeActive("rider"); const riderSignedIn = Boolean(state.sessions.rider && state.rider && !riderPasswordResetActive); if (riderPasswordResetActive) state.accountMode.rider = "signin"; ensureAccountChoiceState("rider", riderSignedIn); if (els.riderSignInOtpPanel) els.riderSignInOtpPanel.hidden = !phoneOtpSignInEnabled(); renderAccountModeButtons("rider", riderSignedIn); if (typeof updatePasswordResetFormMode === "function") updatePasswordResetFormMode("rider", riderPasswordResetActive); const rider = currentRiderRecord(); const riderApproved = riderSignedIn && rider?.status === "approved"; const riderOperational = riderApproved && isSubscriptionActive(rider); els.riderSignInForm.hidden = riderPasswordResetActive ? false : riderSignedIn || accountMode("rider") !== "signin"; if (els.riderPasswordResetPanel) { els.riderPasswordResetPanel.hidden = riderPasswordResetActive ? false : els.riderPasswordResetPanel.hidden || riderSignedIn || accountMode("rider") !== "signin"; } renderRiderWorkspacePages(riderSignedIn); const riderPage = riderWorkspacePage(); renderRiderOverviewGrid(riderSignedIn, rider); const riderNeedsCorrection = riderSignedIn && rider?.status === "needs_correction"; const riderNeedsApplication = riderSignedIn && (rider?.needsApplication || rider?.status === "profile only"); els.riderAccountForm.hidden = (riderNeedsCorrection || riderNeedsApplication) ? riderPasswordResetActive || riderPage !== "profile" : riderPasswordResetActive || riderSignedIn || accountMode("rider") !== "create"; updateRiderCorrectionFormControls(riderNeedsCorrection, rider, riderNeedsApplication); els.riderSessionCard.hidden = !riderSignedIn || !riderPageSectionIncludes(els.riderSessionCard.dataset.riderPageSection, riderPage); els.riderPaymentForm.hidden = !riderApproved || riderPage !== "payment"; els.riderLocationForm.hidden = !riderSignedIn || riderPage !== "initialize"; if (els.riderSupportForm) els.riderSupportForm.hidden = !riderSignedIn || riderPage !== "support"; const selectedRiderRequest = riderSelectedRequestForDetail(); const canShowOfferForm = riderPage === "requests" && Boolean(selectedRiderRequest) && riderCanShowOfferControls(rider, selectedRiderRequest); const canLeaveRiderNegotiation = riderPage === "requests" && Boolean(selectedRiderRequest) && riderCanLeaveSelectedRequest(rider, selectedRiderRequest); const nonNegotiableRiderRequest = Boolean(canShowOfferForm && requestIsNonNegotiableFare(selectedRiderRequest)); const riderProposalLimitReached = Boolean(canShowOfferForm && !nonNegotiableRiderRequest && typeof riderCanSendFareProposal === "function" && !riderCanSendFareProposal(selectedRiderRequest, rider)); els.offerForm.hidden = !(canShowOfferForm || canLeaveRiderNegotiation); els.offerForm.classList.toggle("rider-leave-only", !canShowOfferForm && canLeaveRiderNegotiation); els.offerForm.classList.toggle("rider-non-negotiable-fare", nonNegotiableRiderRequest); if (els.dropRiderNegotiation) { els.dropRiderNegotiation.textContent = canShowOfferForm ? "Decline request" : "Leave request"; els.dropRiderNegotiation.dataset.requestId = selectedRiderRequest?.id ?? ""; } const riderCounterSubmit = els.offerForm.querySelector('button[type="submit"]'); if (riderCounterSubmit) riderCounterSubmit.dataset.riderCounterSubmit = "true"; els.offerForm.querySelectorAll("input, textarea, button").forEach((control) => { control.disabled = control === els.dropRiderNegotiation ? !canLeaveRiderNegotiation : control === els.counterFare || control === riderCounterSubmit ? !canShowOfferForm || nonNegotiableRiderRequest || riderProposalLimitReached : !canShowOfferForm; }); const counterFareField = els.counterFare?.closest("label"); if (counterFareField) { counterFareField.classList.add("counter-fare-field"); counterFareField.hidden = nonNegotiableRiderRequest; } if (riderCounterSubmit) riderCounterSubmit.hidden = nonNegotiableRiderRequest; if (els.acceptFare) { els.acceptFare.textContent = nonNegotiableRiderRequest ? "Accept non-negotiable fare" : "Accept passenger fare"; } const counterNoteField = els.counterNote?.closest("label"); if (counterNoteField) counterNoteField.hidden = true; if (els.counterNote) { els.counterNote.disabled = true; els.counterNote.value = ""; } els.subscriptionText.closest(".subscription-card").hidden = !riderApproved || riderPage !== "checks"; if (riderSignedIn) { els.riderSessionTitle.textContent = state.rider.name || "Rider signed in"; els.riderSessionSummary.textContent = riderWorkspaceStatusMessage(rider); els.riderPaymentStatus.textContent = paymentAccountSummary("rider", rider); if (els.startRiderStripePayoutSetup) { els.startRiderStripePayoutSetup.disabled = !riderApproved || (!directRidePaymentMode() && !hasSupabaseRuntime()); els.startRiderStripePayoutSetup.textContent = directRidePaymentMode() ? "Review wallet mode" : paymentAccountReady("rider", rider) ? "Update Stripe payout account" : "Set up Stripe payout account"; } renderRiderDailyRegionStatus(rider); } renderAccountNotices("rider"); renderReferralPanel("rider"); renderRiderTaxDocuments(); updateAccountPhoneVerificationControls(); renderRiderRouteChangeDecisionModal(); } function updatePassengerProfileRecoveryFormControls(passengerRecovery = pendingProfileRecoveryForRole("passenger")) { if (!els.passengerAccountForm) return; if (els.passengerPassword) { els.passengerPassword.required = !passengerRecovery; els.passengerPassword.placeholder = passengerRecovery ? "Already signed in; no password needed" : "Create a password"; if (passengerRecovery) els.passengerPassword.value = ""; } if (els.passengerSaveButton) { els.passengerSaveButton.textContent = passengerRecovery ? "Complete passenger profile" : "Save passenger"; } if (passengerRecovery && els.passengerStatus) { setTranslatedStatus(els.passengerStatus, "supabaseProfileMissing"); } } function updateRiderCorrectionFormControls(riderNeedsCorrection, rider = currentRiderRecord(), riderNeedsApplication = false) { if (!els.riderAccountForm) return; const existingDocuments = riderDocuments(rider); const signedInApplicationEdit = riderNeedsCorrection || riderNeedsApplication; const riderRecovery = pendingProfileRecoveryForRole("rider"); const documentRequirements = { riderLicenseDocument: "driverLicense", riderRegistrationDocument: "vehicleRegistration", riderInsuranceDocument: "insurance" }; if (els.riderPassword) { els.riderPassword.required = !signedInApplicationEdit && !riderRecovery; els.riderPassword.placeholder = riderRecovery ? "Already signed in; no password needed" : riderNeedsApplication ? "Already signed in; no password needed" : riderNeedsCorrection ? "Leave blank to keep current password" : "Create a password"; if (riderRecovery || signedInApplicationEdit) els.riderPassword.value = ""; } Object.entries(documentRequirements).forEach(([fieldKey, documentKey]) => { const input = els[fieldKey]; if (input) input.required = !riderNeedsCorrection || !existingDocuments[documentKey]; }); if (els.riderSubmitButton) { els.riderSubmitButton.textContent = riderNeedsCorrection ? "Resubmit corrected application" : riderNeedsApplication ? "Submit rider application" : "Submit for admin review"; } if (riderNeedsApplication && els.riderStatus) { els.riderStatus.textContent = "Create or sign in with a separate Waka Cameroon rider account before submitting a rider application."; } else if (riderNeedsCorrection && els.riderStatus) { els.riderStatus.textContent = `Admin requested corrections before review continues.${rider?.reviewNote ? ` Note: ${rider.reviewNote}` : ""}`; } } function updatePassengerCityOptions() { const country = els.passengerCountry.value; populateSelect(els.passengerCity, cityNames(country), cityNames(country)[0]); populateSelect(els.pickupArea, areas(country, els.passengerCity.value).map((area) => area.name), areas(country, els.passengerCity.value)[0]?.name); populateSelect(els.destinationArea, areas(country, els.passengerCity.value).map((area) => area.name), areas(country, els.passengerCity.value)[1]?.name ?? areas(country, els.passengerCity.value)[0]?.name); updateRidePaymentOptions(country); updateFareGuidance(); } function updatePassengerActiveCityOptions() { const country = els.passengerActiveCountry.value; populateSelect(els.passengerActiveCity, cityNames(country), cityNames(country)[0]); updateRidePaymentOptions(country); } function updatePickupOptions() { populateSelect( els.pickupArea, areas(els.passengerCountry.value, els.passengerCity.value).map((area) => area.name), areas(els.passengerCountry.value, els.passengerCity.value)[0]?.name ); populateSelect( els.destinationArea, areas(els.passengerCountry.value, els.passengerCity.value).map((area) => area.name), areas(els.passengerCountry.value, els.passengerCity.value)[1]?.name ?? areas(els.passengerCountry.value, els.passengerCity.value)[0]?.name ); updateFareGuidance(); } function tabFromRouteValue(value) { const normalized = String(value ?? "").toLowerCase().trim(); const tab = workspaceTabs.find((item) => ( normalized === item || normalized.startsWith(`${item}-`) || normalized.startsWith(`${item}/`) || normalized.startsWith(`${item}:`) )); return availableWorkspaceTab(tab); } function routePathTab() { const path = window.location.pathname.toLowerCase(); const segment = path.replace(/\/+$/, "").split("/").pop(); const shellRole = String(document.documentElement.dataset.wakaShell || document.body?.dataset.wakaShell || "").toLowerCase(); const shellTab = tabFromRouteValue(shellRole); if (shellTab) return shellTab; if (segment === "passenger" || segment === "passenger.html" || path.startsWith("/passenger/")) { return availableWorkspaceTab("passenger"); } if (segment === "rider" || segment === "rider.html" || path.startsWith("/rider/")) { return availableWorkspaceTab("rider"); } if (segment === "admin" || segment === "admin.html" || path.startsWith("/admin/")) { return availableWorkspaceTab("admin"); } return null; } function routePathSegments() { return window.location.pathname .toLowerCase() .replace(/\/+$/, "") .split("/") .filter(Boolean); } function normalizedAccountRouteMode(value) { const mode = String(value ?? "").toLowerCase().trim(); if (["create", "signup", "register", "join", "apply"].includes(mode)) return "create"; if (["signin", "sign-in", "login", "log-in"].includes(mode)) return "signin"; return ""; } function requestedAccountModeFromLocation(type) { if (!["passenger", "rider"].includes(type)) return ""; const pathSegments = routePathSegments(); if (pathSegments[0] === type) { const pathMode = normalizedAccountRouteMode(pathSegments[1]); if (pathMode) return pathMode; } const params = new URLSearchParams(window.location.search); const queryMode = normalizedAccountRouteMode( params.get("account") ?? params.get("accountMode") ?? params.get("mode") ?? params.get("action") ); if (queryMode && requestedTabFromLocation() === type) return queryMode; const hashValue = window.location.hash.replace(/^#\/?/, "").toLowerCase(); if (hashValue.startsWith(`${type}/`) || hashValue.startsWith(`${type}:`)) { const hashMode = normalizedAccountRouteMode(hashValue.split(/[/:?&=]/)[1]); if (hashMode) return hashMode; } return ""; } function requestedTabFromLocation() { const pathTab = routePathTab(); if (pathTab) return pathTab; const hashTab = tabFromRouteValue(window.location.hash.replace(/^#\/?/, "").split(/[?&=]/)[0]); if (hashTab) return hashTab; const params = new URLSearchParams(window.location.search); const explicitTab = tabFromRouteValue(params.get("tab") ?? params.get("role") ?? params.get("workspace")); if (explicitTab) return explicitTab; return workspaceTabs.find((tab) => params.has(tab) && availableWorkspaceTab(tab)) ?? null; } function updateWorkspaceHash(tab) { if (!workspaceTabs.includes(tab)) return; const currentHashTab = tabFromRouteValue(window.location.hash.replace(/^#\/?/, "").split(/[?&=]/)[0]); if (window.location.hash === `#${tab}` || currentHashTab === tab) return; if (routePathTab() === tab) return; window.history.replaceState(null, "", `${window.location.pathname}${window.location.search}#${tab}`); } function accountModeRoute(type, mode) { if (!["passenger", "rider"].includes(type)) return ""; return mode === "create" ? `/${type}/create` : `/${type}`; } function updateAccountModeRoute(type, mode) { const path = accountModeRoute(type, mode); if (!path || window.location.pathname === path) return; window.history.pushState(null, "", path); } function applyRouteTab() { const tab = requestedTabFromLocation(); if (tab && tab !== state.activeTab) switchTab(tab, { updateUrl: false }); const accountRouteRole = tab ?? state.activeTab; const accountRouteMode = requestedAccountModeFromLocation(accountRouteRole); if (accountRouteMode && ["passenger", "rider"].includes(accountRouteRole) && !roleHasSignedInAccount(accountRouteRole)) { state.accountMode[accountRouteRole] = accountRouteMode; accountModeSelectedThisSession[accountRouteRole] = true; saveState(); } if ((tab ?? state.activeTab) === "rider") { const requestedPage = requestedRiderWorkspacePageFromLocation(); const requestedRequestId = requestedRideRequestIdFromLocation(); if (requestedPage && availableRiderWorkspacePages().includes(requestedPage)) { state.riderPage = requestedPage; saveState(); } if (requestedRequestId) { state.riderPage = "requests"; state.selectedRequestId = requestedRequestId; saveState(); } } if ((tab ?? state.activeTab) === "passenger") { const requestedPage = requestedPassengerWorkspacePageFromLocation(); const requestedRequestId = requestedRideRequestIdFromLocation(); const passengerSignedIn = hasSignedIn("passenger"); const requestedTripDeepLink = requestedPage === "trips" && Boolean(requestedRequestId); const shouldHonorRequestedPage = !passengerSignedIn || passengerWorkspacePageSelectedInSession || requestedTripDeepLink; if (requestedPage && availablePassengerWorkspacePages().includes(requestedPage) && shouldHonorRequestedPage) { state.passengerPage = requestedPage; saveState(); } else if (passengerSignedIn && !availablePassengerWorkspacePages().includes(state.passengerPage)) { state.passengerPage = "request"; saveState(); } if (requestedRequestId) { state.passengerPage = "trips"; state.selectedRequestId = requestedRequestId; saveState(); } } if ((tab ?? state.activeTab) === "admin" && typeof applyRequestedAdminWorkspacePageFromLocation === "function") { const routeApplied = applyRequestedAdminWorkspacePageFromLocation(); if (routeApplied && typeof ensureAdminWorkspaceData === "function") void ensureAdminWorkspaceData(state.adminPage); } renderEntryExperience(); renderAll(); } function roleHasSignedInAccount(role) { if (!availableWorkspaceTab(role)) return false; if (role === "passenger") return Boolean(state.sessions.passenger && state.passenger); if (role === "rider") return Boolean(state.sessions.rider && state.rider); if (role === "admin") return adminShellAvailable() && Boolean(state.adminSession); return false; } function preferredSignedInTab() { if (roleHasSignedInAccount(state.activeTab)) return state.activeTab; if (roleHasSignedInAccount("passenger")) return "passenger"; if (roleHasSignedInAccount("rider")) return "rider"; if (roleHasSignedInAccount("admin")) return "admin"; return null; } function publicHomePathIsActive() { if (runtimeRole !== "public") return false; const pathname = window.location.pathname.replace(/\/+$/, "") || "/"; return pathname === "/" || pathname === "/index.html"; } function shouldShowRoleEntry() { if (runtimeRole !== "public" || !els.roleEntry) return false; if (requestedTabFromLocation()) return false; if (publicHomePathIsActive()) { const signedInPassengerWorkspace = roleHasSignedInAccount("passenger") && state.activeTab === "passenger" && state.showRoleEntry === false; if (signedInPassengerWorkspace) return false; return true; } if (preferredSignedInTab()) return Boolean(state.showRoleEntry); return true; } function renderEntryExperience() { const roleEntryVisible = shouldShowRoleEntry(); if (els.roleEntry) els.roleEntry.hidden = !roleEntryVisible; if (els.workspace) els.workspace.hidden = roleEntryVisible; if (els.roleTabs) els.roleTabs.hidden = true; } function setAccountMode(type, mode, options = {}) { if (!["passenger", "rider"].includes(type)) return; const { updateUrl = true } = options; accountModeSelectedThisSession[type] = true; state.accountMode[type] = mode === "create" ? "create" : "signin"; state.activeTab = type; state.showRoleEntry = false; if (updateUrl) updateAccountModeRoute(type, state.accountMode[type]); renderEntryExperience(); saveState(); renderAll(); } function accountMode(type) { const mode = state.accountMode?.[type]; return mode === "signin" || mode === "create" ? mode : "signin"; } function resetAccountChoice(type) { if (!["passenger", "rider"].includes(type)) return; const routeMode = requestedAccountModeFromLocation(type); accountModeSelectedThisSession[type] = Boolean(routeMode); state.accountMode[type] = routeMode || "signin"; } function renderAccountModeButtons(type, signedIn) { const stage = type === "passenger" ? els.passengerAccountStage : els.riderAccountStage; if (stage) { stage.hidden = true; stage.dataset.mode = accountMode(type); } document.querySelectorAll(`[data-account-type="${type}"][data-account-mode]`).forEach((button) => { const active = button.dataset.accountMode === accountMode(type); button.classList.toggle("active", active); button.setAttribute("aria-pressed", String(active)); }); } function switchTab(tab, options = {}) { tab = availableWorkspaceTab(tab); if (!tab) return; const { updateUrl = true, preserveEntry = false, resetAccountMode = false } = options; state.activeTab = tab; if (resetAccountMode) resetAccountChoice(tab); if (!preserveEntry) state.showRoleEntry = false; renderEntryExperience(); document.querySelectorAll(".tab-button").forEach((button) => { button.classList.toggle("active", button.dataset.tab === tab); }); document.querySelectorAll(".tab-panel").forEach((panel) => { panel.classList.toggle("active", panel.id === `${tab}-panel`); }); if (updateUrl) updateWorkspaceHash(tab); saveState(); renderAll(); } function riderSelectedRequestForDetail() { if (activeRole() !== "rider" || riderWorkspacePage() !== "requests") return null; const request = selectedRequest(); if (request && roleCanSeeRequest(request)) return request; if (!state.rider) return null; return state.requests.find((item) => requestIsActiveForCurrentRider(item)) ?? null; } function riderRequestDetailOpen() { return Boolean(riderSelectedRequestForDetail()); } function selectedWorkspaceRequest() { if (activeRole() === "rider") return riderSelectedRequestForDetail(); const selected = selectedRequest(); if (selected) return selected; if (activeRole() !== "passenger") return null; const selectedRaw = stateLookupIndexes().requestMap.get(state.selectedRequestId) ?? null; if (selectedRaw && requestBelongsToPassenger(selectedRaw) && selectedRaw.status === "completed" && (canRateRequest(selectedRaw) || existingRatingForRequest(selectedRaw))) { return selectedRaw; } const pendingRide = passengerPendingRide(); if (!pendingRide) return null; state.selectedRequestId = pendingRide.id; rememberWorkspaceUiState("passenger", { page: "trips", selectedRequestId: pendingRide.id }); saveState(); return pendingRide; } function riderActivePickupDisplayText(request) { const directAddress = String(request?.pickupFormattedAddress ?? request?.pickupDescription ?? "").trim(); const isGenericCurrent = directAddress && (pickupUsesCurrentLocationText(directAddress) || pickupUsesGpsFallbackText(directAddress)); if (directAddress && !isGenericCurrent) return directAddress; const displayText = requestPickupDisplayText(request, ""); if (displayText && !pickupUsesCurrentLocationText(displayText) && !pickupUsesGpsFallbackText(displayText)) return displayText; const areaText = compactLocationQuery([request?.pickupArea, request?.city, request?.country]); if (areaText) return requestPickupGps(request) ? `Verified pickup near ${areaText}` : areaText; return requestPickupGps(request) ? "Verified GPS pickup" : "Pickup"; } function riderActiveRouteDisplayText(request) { const displayRequest = activeRole() === "rider" ? riderVisibleRouteRequest(request) : request; return `${riderActivePickupDisplayText(request)} to ${requestDestinationDisplayText(displayRequest)}`; } function riderMarketplaceRouteDistanceChip(request) { const pickup = proximityChip(request) ?? "Pickup ETA: estimating"; const destination = destinationDriveChip(request) ?? "Destination drive: estimating"; return `${pickup} / ${destination}`; } function resetRideDockPanels({ restore = true } = {}) { passengerRideDockRequestId = null; passengerRideDockOpenPanel = null; riderRideDockRequestId = null; riderRideDockOpenPanel = null; if (restore && typeof restoreRideToolElementsToChatPanel === "function") restoreRideToolElementsToChatPanel(); } function returnRiderToMarketplace({ replace = true, refresh = false } = {}) { state.activeTab = "rider"; state.showRoleEntry = false; state.riderPage = "requests"; state.selectedRequestId = null; resetRideDockPanels(); if (typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute("requests", { replace, requestId: "" }); } saveState(); renderAll(); window.setTimeout(() => { document.querySelector("#requestsBoard, #marketPanel") ?.scrollIntoView({ behavior: "smooth", block: "start" }); }, 0); if (refresh) void refreshMarketplace({ silent: true }); } function renderRiderRequestDetailPanel(request = riderSelectedRequestForDetail()) { if (!els.riderRequestDetailPanel) return; const isVisible = activeRole() === "rider" && riderWorkspacePage() === "requests" && Boolean(request); const activeTrip = isVisible && requestIsActiveForCurrentRider(request); els.riderRequestDetailPanel.hidden = !isVisible || activeTrip; if (!isVisible) return; if (activeTrip) { if (els.riderRequestDetailStatus) { els.riderRequestDetailStatus.textContent = ""; els.riderRequestDetailStatus.hidden = true; } return; } if (els.riderRequestDetailTitle) { const titleBlock = els.riderRequestDetailTitle.parentElement; if (titleBlock) titleBlock.hidden = false; els.riderRequestDetailTitle.textContent = "Respond to selected request"; } if (els.riderRequestDetailStatus) { els.riderRequestDetailStatus.textContent = ""; els.riderRequestDetailStatus.hidden = true; } } function destinationFilterOptions(values, anyLabel) { return [ { value: "", label: anyLabel }, ...values.map((value) => ({ value, label: value })) ]; } let riderDestinationFilterDraft = null; function normalizedRiderDestinationFilterDraft(value = {}) { const filter = normalizeRiderMarketplaceDestinationFilter({ enabled: true, consent: value.consent === true, country: value.country, city: value.city, area: value.area, query: value.query, appliedAt: new Date().toISOString() }); return { consent: value.consent === true, country: filter.country, city: filter.city, area: filter.area, query: String(value.query ?? "").trim() }; } function rememberRiderDestinationFilterDraft(draft = riderDestinationFilterDraftValues()) { riderDestinationFilterDraft = normalizedRiderDestinationFilterDraft(draft); return riderDestinationFilterDraft; } function rememberRiderDestinationFilterControlDraft() { return rememberRiderDestinationFilterDraft(riderDestinationFilterDraftValues({ preferStoredDraft: false })); } function riderDestinationFilterDraftValues({ preferStoredDraft = true } = {}) { const filter = riderMarketplaceDestinationFilter(); return { consent: els.riderDestinationFilterConsent?.checked ?? riderDestinationFilterDraft?.consent ?? filter.consent, country: els.riderDestinationFilterCountry?.value ?? (preferStoredDraft ? riderDestinationFilterDraft?.country : null) ?? filter.country, city: els.riderDestinationFilterCity?.value ?? (preferStoredDraft ? riderDestinationFilterDraft?.city : null) ?? filter.city, area: els.riderDestinationFilterArea?.value ?? (preferStoredDraft ? riderDestinationFilterDraft?.area : null) ?? filter.area, query: String(els.riderDestinationFilterText?.value ?? (preferStoredDraft ? riderDestinationFilterDraft?.query : null) ?? filter.query ?? "").trim() }; } function destinationFilterCitySourceCountry(country) { return country || selectedRiderCountry(); } function destinationFilterAreaSourceCity(country, city) { const sourceCountry = destinationFilterCitySourceCountry(country); if (city && cityNames(sourceCountry).includes(city)) return city; const riderCity = selectedRiderCity(); if (sourceCountry === selectedRiderCountry() && cityNames(sourceCountry).includes(riderCity)) return riderCity; return defaultLaunchCity(sourceCountry); } function populateRiderDestinationFilterOptions(draft = riderDestinationFilterDraftValues()) { if (!els.riderDestinationFilterCountry || !els.riderDestinationFilterCity || !els.riderDestinationFilterArea) return; const countriesList = enabledLaunchCountries(); const country = countriesList.includes(draft.country) ? draft.country : ""; const cityCountry = destinationFilterCitySourceCountry(country); const cityValues = cityNames(cityCountry); const city = cityValues.includes(draft.city) ? draft.city : ""; const areaCity = destinationFilterAreaSourceCity(country, city); const areaValues = areas(cityCountry, areaCity).map((area) => area.name); const area = areaValues.includes(draft.area) ? draft.area : ""; rememberRiderDestinationFilterDraft({ ...draft, country, city, area }); populateSelectOptions(els.riderDestinationFilterCountry, destinationFilterOptions(countriesList, "Any country"), country); populateSelectOptions(els.riderDestinationFilterCity, destinationFilterOptions(cityValues, "Any state / city"), city); populateSelectOptions(els.riderDestinationFilterArea, destinationFilterOptions(areaValues, "Any town / area"), area); } function renderRiderDestinationFilterControls() { if (!els.riderDestinationFilterPanel) return; const filter = riderMarketplaceDestinationFilter(); const draft = riderDestinationFilterDraft ?? normalizedRiderDestinationFilterDraft(filter); populateRiderDestinationFilterOptions(draft); if (els.riderDestinationFilterConsent) els.riderDestinationFilterConsent.checked = draft.consent; if (els.riderDestinationFilterText) els.riderDestinationFilterText.value = draft.query; if (els.riderDestinationFilterStatus) { els.riderDestinationFilterStatus.textContent = riderMarketplaceDestinationFilterSummary(filter); } } function refreshRiderDestinationFilterOptions() { populateRiderDestinationFilterOptions(rememberRiderDestinationFilterDraft()); } function applyRiderDestinationFilter() { const draft = riderDestinationFilterDraftValues(); if (!draft.consent) { if (els.riderDestinationFilterStatus) els.riderDestinationFilterStatus.textContent = "Check the consent box before applying a destination preference."; return; } let country = draft.country; let city = draft.city; let area = draft.area; const query = draft.query; if (!country && (city || area)) country = selectedRiderCountry(); if (!city && area) city = destinationFilterAreaSourceCity(country, city); const nextFilter = normalizeRiderMarketplaceDestinationFilter({ enabled: true, consent: true, country, city, area, query, appliedAt: new Date().toISOString() }); if (!riderMarketplaceDestinationFilterIsActive(nextFilter)) { if (els.riderDestinationFilterStatus) els.riderDestinationFilterStatus.textContent = "Choose a country, state, town, or destination text before applying."; return; } state.riderMarketplaceDestinationFilter = nextFilter; riderDestinationFilterDraft = normalizedRiderDestinationFilterDraft(nextFilter); returnRiderToMarketplace({ replace: true, refresh: true }); } function clearRiderDestinationFilter() { state.riderMarketplaceDestinationFilter = normalizeRiderMarketplaceDestinationFilter(); riderDestinationFilterDraft = null; state.selectedRequestId = null; saveState(); renderAll(); if (els.riderDestinationFilterStatus) { els.riderDestinationFilterStatus.textContent = "Cleared. Ride requests will show all nearby rides again."; } void refreshMarketplace({ silent: true }); } function showRoleEntryScreen() { state.showRoleEntry = true; state.activeTab = defaultRuntimeTab(); if (window.location.hash) window.history.replaceState({}, "", window.location.pathname || "./"); saveState(); renderAll(); } function isPassengerNegotiationRequest(request) { return Boolean(activeRole() === "passenger" && request && requestBelongsToPassenger(request) && requestIsNegotiableFare(request) && request.status === "open" && !selectedRiderIdForRequest(request)); } function isPassengerNonNegotiableWaitingRequest(request) { return Boolean(activeRole() === "passenger" && request && requestBelongsToPassenger(request) && requestIsNonNegotiableFare(request) && request.status === "open" && !selectedRiderIdForRequest(request)); } function isPassengerWaitingForRiderRequest(request) { return Boolean(isPassengerNegotiationRequest(request) || isPassengerNonNegotiableWaitingRequest(request)); } function passengerPendingRatingRequest() { if (activeRole() !== "passenger" || !state.passenger) return null; return state.requests .filter((request) => requestBelongsToPassenger(request) && canRateRequest(request)) .sort((a, b) => new Date(b.completedAt ?? b.updatedAt ?? b.createdAt ?? 0).getTime() - new Date(a.completedAt ?? a.updatedAt ?? a.createdAt ?? 0).getTime())[0] ?? null; } function openPassengerRideRating(request) { if (!request?.id || !requestBelongsToPassenger(request)) return; state.selectedRequestId = request.id; passengerRideDockRequestId = request.id; passengerRideDockOpenPanel = "rating"; rememberWorkspaceUiState("passenger", { page: "trips", selectedRequestId: request.id }); saveState(); renderAll(); window.setTimeout(() => { document.querySelector("#chatPanel, #rideActionPanel") ?.scrollIntoView({ behavior: "smooth", block: "start" }); }, 0); } function passengerRatingPromptKey(request) { return `passenger-rating-${request?.id ?? "ride"}-${selectedRiderIdForRequest(request) ?? "rider"}`; } function rememberPassengerRatingPromptDecision(request) { const key = passengerRatingPromptKey(request); state.notificationPopupIds = [...new Set([...(state.notificationPopupIds ?? []), key])].slice(-140); saveState(); } function passengerRatingPromptVisible(request) { const key = passengerRatingPromptKey(request); return [...document.querySelectorAll("[data-passenger-rating-prompt-key]")] .some((node) => node.dataset.passengerRatingPromptKey === key); } function showPassengerRatingPrompt(request) { if (!request?.id || !canRateRequest(request) || existingRatingForRequest(request)) return; const key = passengerRatingPromptKey(request); if ((state.notificationPopupIds ?? []).includes(key) || passengerRatingPromptVisible(request)) return; const riderName = selectedRiderFirstNameForRequest(request); const route = `${requestPickupDisplayText(request)} to ${requestDestinationDisplayText(request)}`; const node = document.createElement("div"); node.className = "notice-popup notice-popup-actionable"; node.dataset.passengerRatingPromptKey = key; node.innerHTML = ` Rate ${escapeHtml(riderName)}

Completed ride: ${escapeHtml(route)}

`; node.querySelector(".passenger-rating-now")?.addEventListener("click", () => { rememberPassengerRatingPromptDecision(request); node.remove(); openPassengerRideRating(request); }); node.querySelector(".passenger-rating-later")?.addEventListener("click", () => { rememberPassengerRatingPromptDecision(request); node.remove(); }); document.body.append(node); } function renderPassengerPendingRatingCard(request) { const node = document.createElement("article"); node.className = "market-card passenger-rating-prompt-card"; const riderName = selectedRiderFirstNameForRequest(request); node.innerHTML = `
Pending rider rating Rate ${escapeHtml(riderName)} ${escapeHtml(requestPickupDisplayText(request))} to ${escapeHtml(requestDestinationDisplayText(request))}

Share feedback for ${escapeHtml(riderName)} on this completed ride, or come back later from this trip.

`; node.querySelector("button")?.addEventListener("click", () => openPassengerRideRating(request)); return node; } function passengerNegotiationModeActive() { const request = selectedWorkspaceRequest(); return Boolean(isPassengerNegotiationRequest(request) && roleCanSeeRequest(request)); } function selectedPassengerNegotiationOffer(request, offers = visibleOffersForRole(request)) { if (!isPassengerNegotiationRequest(request) || !state.passengerSelectedOfferId) return null; return offers.find((offer) => offer.id === state.passengerSelectedOfferId) ?? null; } function passengerOfferResponseModeActive() { const request = selectedRequest(); return Boolean(passengerNegotiationModeActive() && selectedPassengerNegotiationOffer(request)); } function scrollPassengerOffersIntoView() { window.setTimeout(() => { document.querySelector("#offersBoard")?.scrollIntoView({ behavior: "smooth", block: "start" }); }, 0); } function openPassengerOfferResponse(offer) { if (!offer?.id) return; state.passengerSelectedOfferId = offer.id; saveState(); renderAll(); scrollPassengerOffersIntoView(); } function returnPassengerToOfferList() { if (!state.passengerSelectedOfferId) return; state.passengerSelectedOfferId = null; saveState(); renderAll(); scrollPassengerOffersIntoView(); } function renderRoleWorkspace() { const role = activeRole(); const rider = currentRiderRecord(); const riderOperational = role !== "rider" || riderCanSeeRequests(rider); els.marketPanel.dataset.role = role; els.boardGrid.classList.remove("role-passenger", "role-rider", "role-admin", "rider-marketplace-mode", "rider-detail-mode", "passenger-negotiation-mode", "passenger-offer-response-mode", "passenger-active-ride-mode", "passenger-non-negotiable-mode"); els.boardGrid.classList.add(`role-${role}`); const passengerSignedIn = roleHasSignedInAccount("passenger"); const riderSignedIn = roleHasSignedInAccount("rider"); const adminSignedIn = roleHasSignedInAccount("admin"); const signedInForActiveRole = role === "passenger" ? passengerSignedIn : role === "rider" ? riderSignedIn : adminSignedIn; const activeRolePasswordReset = typeof passwordResetModeActive === "function" && passwordResetModeActive(role); const passengerTripsPage = passengerWorkspacePage() === "trips"; const riderRequestsPage = riderWorkspacePage() === "requests"; if (els.seedDemo) els.seedDemo.hidden = !demoToolsAllowed(); if (els.clearDemo) els.clearDemo.hidden = !demoToolsAllowed(); els.marketPanel.hidden = activeRolePasswordReset || (role === "passenger" && (!passengerSignedIn || !passengerTripsPage)) || (role === "rider" && (!riderSignedIn || !riderRequestsPage)) || (role === "admin" && !adminSignedIn); const roleAuthEntry = (role === "passenger" || role === "rider") && (!signedInForActiveRole || activeRolePasswordReset); const signedInSinglePanel = els.marketPanel.hidden && signedInForActiveRole && !activeRolePasswordReset; els.workspace?.classList.toggle("account-only", roleAuthEntry); els.workspace?.classList.toggle("account-entry", roleAuthEntry); els.workspace?.classList.toggle("single-panel", signedInSinglePanel); els.workspace?.classList.toggle("password-reset-entry", activeRolePasswordReset); if (els.marketPanel.hidden) { if (els.riderRequestDetailPanel) els.riderRequestDetailPanel.hidden = true; if (els.openRiderDestinationFilter) els.openRiderDestinationFilter.hidden = true; if (els.riderAppliedDestinationSummary) els.riderAppliedDestinationSummary.hidden = true; return; } document.querySelectorAll(".role-market-section").forEach((section) => { const allowedRoles = (section.dataset.roles ?? "").split(/\s+/); section.hidden = !allowedRoles.includes(role); }); const riderDetailRequest = role === "rider" ? riderSelectedRequestForDetail() : null; const riderDetailOpen = Boolean(riderDetailRequest); const passengerWorkspaceRequest = role === "passenger" ? selectedWorkspaceRequest() : null; const passengerNegotiationMode = role === "passenger" && isPassengerNegotiationRequest(passengerWorkspaceRequest); const passengerOfferResponseMode = passengerNegotiationMode && passengerOfferResponseModeActive(); const passengerNonNegotiableMode = role === "passenger" && isPassengerNonNegotiableWaitingRequest(passengerWorkspaceRequest); const passengerWaitingDockMode = role === "passenger" && passengerRideDockMode(passengerWorkspaceRequest); els.boardGrid.classList.toggle("rider-marketplace-mode", role === "rider" && !riderDetailOpen); els.boardGrid.classList.toggle("rider-detail-mode", role === "rider" && riderDetailOpen); els.boardGrid.classList.toggle("passenger-negotiation-mode", passengerNegotiationMode); els.boardGrid.classList.toggle("passenger-offer-response-mode", passengerOfferResponseMode); els.boardGrid.classList.toggle("passenger-non-negotiable-mode", passengerNonNegotiableMode); renderRiderRequestDetailPanel(riderDetailRequest); const passengerTrackableRide = role === "passenger" && state.requests.some((request) => requestBelongsToPassenger(request) && selectedRiderIdForRequest(request) && ["matched", "arrived", "in_progress"].includes(request.status)); els.boardGrid.classList.toggle("passenger-active-ride-mode", passengerTrackableRide); const riderActiveTrip = role === "rider" && state.requests.some((request) => requestIsActiveForCurrentRider(request)); els.cityMap.hidden = role === "admin" || role === "rider" || passengerTrackableRide || riderActiveTrip; els.marketFilters.hidden = role === "admin" || role === "rider" || (role === "rider" && !riderOperational); els.refreshMarket.hidden = role === "passenger" || passengerTrackableRide || riderActiveTrip || !canRefreshMarketplace(); if (els.openRiderDestinationFilter) { els.openRiderDestinationFilter.hidden = !(role === "rider" && riderRequestsPage && !riderDetailOpen && !riderActiveTrip); els.openRiderDestinationFilter.textContent = riderMarketplaceDestinationFilterIsActive() ? "Edit destination" : "Destination"; } if (els.riderAppliedDestinationSummary) { const showAppliedDestinationSummary = role === "rider" && riderRequestsPage && !riderDetailOpen && riderMarketplaceDestinationFilterIsActive(); els.riderAppliedDestinationSummary.hidden = !showAppliedDestinationSummary; els.riderAppliedDestinationSummary.textContent = showAppliedDestinationSummary ? riderMarketplaceDestinationFilterSummary() : ""; } els.refreshMarket.disabled = marketRefreshInFlight; els.refreshMarket.textContent = lastMarketRefreshAt ? `Refresh market (${lastMarketRefreshAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })})` : "Refresh market"; if (role === "passenger") { if (els.chatPanel) els.chatPanel.hidden = passengerNegotiationMode && !passengerWaitingDockMode; els.requestBoardTitle.textContent = passengerNegotiationMode ? "Your request" : "My ride requests"; els.offerBoardTitle.textContent = passengerOfferResponseMode ? "Respond to rider offer" : passengerNegotiationMode ? "Choose rider offer" : "Offers for my request"; if (els.offersBoard && passengerNonNegotiableMode) els.offersBoard.hidden = true; return; } if (role === "rider") { const vehicleName = "car"; const riderActiveDetailTrip = Boolean(riderDetailRequest && requestIsActiveForCurrentRider(riderDetailRequest)); els.requestBoardTitle.textContent = riderActiveDetailTrip ? "Active ride" : `Incoming ${vehicleName} requests`; els.offerBoardTitle.textContent = `My ${vehicleName} offers`; if (els.requestsBoard) els.requestsBoard.hidden = riderDetailOpen && !riderActiveDetailTrip; if (els.offersBoard) els.offersBoard.hidden = !riderDetailOpen || riderActiveDetailTrip; if (els.chatPanel) els.chatPanel.hidden = !riderActiveDetailTrip; els.marketLocation.textContent = riderCurrentFreshGps(rider) ? "Live location active" : "Rider workspace"; if (!riderOperational) { els.selectedSummary.textContent = riderWorkspaceStatusMessage(rider); } else if (riderMarketplaceDestinationFilterIsActive()) { els.selectedSummary.textContent = riderMarketplaceDestinationFilterSummary(); } else { els.selectedSummary.textContent = riderServiceAreaSummary(rider); } return; } els.marketLocation.textContent = "Admin workspace"; els.selectedSummary.textContent = state.adminSession ? "View passengers, riders, approvals, subscriptions, and marketplace activity" : "Admin sign-in required for full passenger and rider visibility"; } function renderMap() { document.querySelectorAll(".map-pin").forEach((pin) => pin.remove()); if (activeRole() === "admin") return; const { country, city } = activeMarketLocation(); els.marketLocation.textContent = `${city}, ${country}`; visibleRequestsForRole().forEach((item) => { const point = findArea(item.country, item.city, item.pickupArea); placePin(point, item.id === state.selectedRequestId ? "S" : "R", item.id === state.selectedRequestId ? "pin-selected" : "pin-request"); }); state.riders .filter((rider) => rider.country === country && rider.city === city && rider.status === "approved") .filter((rider) => activeRole() === "passenger" || rider.id === state.rider?.id) .filter((rider) => state.filter === "all" || rider.vehicle === state.filter) .forEach((rider) => { placePin(findArea(rider.country, rider.city, rider.area), "C", "pin-rider"); }); } function placePin(point, label, className) { if (!point) return; const pin = document.createElement("div"); pin.className = `map-pin ${className}`; pin.style.left = `${point.x}%`; pin.style.top = `${point.y}%`; pin.title = point.name; pin.innerHTML = `${label}`; els.cityMap.append(pin); } function renderRiderBusyDayControls(container) { if (activeRole() !== "rider" || !container || riderWorkspacePage() !== "requests") return; if (state.requests.some((request) => requestIsActiveForCurrentRider(request))) return; const controls = document.createElement("article"); controls.className = "notice-item rider-workload-controls"; controls.innerHTML = `
Marketplace workload

${riderFocusModeActive() ? "Focus mode is holding new ride updates in a queue while you review the current request." : "Normal mode opens urgent ride updates when you are not already reviewing another request."}

`; controls.querySelectorAll("[data-rider-workload-mode]").forEach((button) => { button.addEventListener("click", () => setRiderWorkloadMode(button.dataset.riderWorkloadMode)); }); controls.querySelector("[data-rider-destination-filter]")?.addEventListener("click", () => setRiderWorkspacePage("destination")); container.append(controls); const queue = riderDecisionQueueItems(); if (!queue.length) return; const queuePanel = document.createElement("article"); queuePanel.className = "notice-item rider-decision-queue"; queuePanel.innerHTML = ` Queued ride updates

${queue.length} update${queue.length === 1 ? "" : "s"} waiting while you finish the current decision.

`; queue.forEach((item) => { const request = stateLookupIndexes().requestMap.get(item.requestId); const row = document.createElement("div"); row.className = "queued-ride-update"; row.innerHTML = `
${escapeHtml(item.title)} ${escapeHtml(request ? `${requestPickupTownText(request)} to ${requestDestinationDisplayText(request)}` : item.requestId)} - ${formatDateTime(item.createdAt)}

${escapeHtml(item.body || "Ride update is ready to review.")}

`; row.querySelector("[data-view-queued-ride]")?.addEventListener("click", () => viewRiderDecisionQueueItem(item.id)); row.querySelector("[data-dismiss-queued-ride]")?.addEventListener("click", () => dismissRiderDecisionQueueItem(item.id)); queuePanel.append(row); }); container.append(queuePanel); } function renderRequests() { const visible = visibleRequestsForRole(); const fareBoostFocus = passengerFareBoostFocusSnapshot(els.requestList); rememberPassengerFareDraftInputs(); els.requestList.innerHTML = ""; renderRiderBusyDayControls(els.requestList); if (!visible.length) { if (activeRole() === "passenger") { const pendingRating = passengerPendingRatingRequest(); if (pendingRating) { els.requestList.append(renderPassengerPendingRatingCard(pendingRating)); showPassengerRatingPrompt(pendingRating); } else { els.requestList.append(emptyState(state.passenger ? "No active ride requests. Cancelled and completed rides are kept out of this active list." : "Sign in or create a passenger account to see your ride requests.")); } } else if (activeRole() === "rider") { const rider = currentRiderRecord(); const activeRide = riderActiveImmediateRide(rider); const message = !rider ? "Create or sign in as a rider to see nearby passenger requests." : !hasSignedIn("rider") ? "Sign in as a rider to see nearby passenger requests." : rider.status !== "approved" ? "Admin approval is required before ride requests are shown." : !isSubscriptionActive(rider) ? "Your trial or subscription must be active before ride requests are shown." : !paymentAccountReady("rider", rider) ? "Save your rider payment account before receiving requests." : !riderCurrentFreshGps(rider) ? "Activate rider availability from the Initialize rider availability menu before requests appear." : activeRide ? "Complete or cancel your active immediate ride before taking another immediate request." : riderMarketplaceDestinationFilterIsActive() ? "No rides match this destination preference right now. Clear the destination filter to see all nearby requests." : `No matching immediate requests within about ${riderPickupMaxEtaMinutes} minutes, or scheduled requests within about ${scheduledRiderPickupMaxEtaMinutes} minutes, of your active live location and ${riderDestinationScopeLabel()} yet.`; els.requestList.append(emptyState(message)); } } visible.forEach((item) => { const node = els.requestTemplate.content.firstElementChild.cloneNode(true); const button = node.querySelector(".card-select"); const isRiderIncomingRequest = activeRole() === "rider" && item.status === "open" && !requestIsActiveForCurrentRider(item); const isPassengerTripCard = activeRole() === "passenger" && requestBelongsToPassenger(item) && passengerActiveRideRequestStatuses().includes(item.status); const isMatchedPassengerTrip = isPassengerTripCard && selectedRiderIdForRequest(item); const isPassengerNegotiationTrip = isPassengerNegotiationRequest(item); const isPassengerNonNegotiableWaitingTrip = isPassengerNonNegotiableWaitingRequest(item); const isRiderActiveTrip = activeRole() === "rider" && requestIsActiveForCurrentRider(item); const isCompactTripCard = isPassengerTripCard || isRiderActiveTrip; const pendingRouteChange = pendingRouteChangeForRequest(item); const routeDisplayItem = activeRole() === "rider" ? riderVisibleRouteRequest(item) : item; const passengerLabel = activeRole() === "rider" && item.status === "open" ? "Passenger" : passengerFirstNameForRequest(item); node.classList.toggle("selected", item.id === state.selectedRequestId); node.classList.toggle("active-trip-card", isCompactTripCard); node.classList.toggle("passenger-negotiation-card", isPassengerNegotiationTrip); node.classList.toggle("non-negotiable-waiting-card", isPassengerNonNegotiableWaitingTrip); node.classList.toggle("rider-request-card", isRiderIncomingRequest); const pickupLabel = isRiderActiveTrip ? riderActivePickupDisplayText(item) : requestPickupDisplayText(item); const destinationLabel = requestDestinationDisplayText(routeDisplayItem); const riderPostPickupTrip = isRiderActiveTrip && ["in_progress", "completed"].includes(item.status); node.querySelector(".card-kicker").textContent = isRiderIncomingRequest ? (isScheduledRequest(item) ? "Scheduled request" : "Incoming request") : `${item.vehicle.toUpperCase()} ${isScheduledRequest(item) ? "scheduled" : "request"}`; node.querySelector("strong").textContent = isRiderIncomingRequest ? `Pickup: ${requestPickupTownText(item)}` : riderPostPickupTrip ? `${rideStatusLabel(item)} with ${passengerLabel}` : isMatchedPassengerTrip ? `${rideStatusLabel(item)} with ${selectedRiderFirstNameForRequest(item)}` : `${pickupLabel} to ${destinationLabel}`; node.querySelector("small").textContent = isRiderIncomingRequest ? `${passengerLabel} offered ${formatMoney(item.fareOffer)}` : isRiderActiveTrip ? `Fare ${formatMoney(agreedFareForRequest(item), item.country)} - ${scheduleChip(item)}` : isMatchedPassengerTrip ? `Fare ${formatMoney(agreedFareForRequest(item), item.country)} - ${scheduleChip(item)}` : isPassengerNonNegotiableWaitingTrip ? `Non-negotiable fare - offer ${formatMoney(item.fareOffer, item.country)} - ${scheduleChip(item)}` : `${passengerLabel} offered ${formatMoney(item.fareOffer)} - ${scheduleChip(item)}`; const summary = node.querySelector("p"); if (isRiderIncomingRequest) { summary.hidden = true; summary.textContent = ""; node.querySelector(".chip-row").innerHTML = [ isScheduledRequest(item) ? scheduleChip(item) : null, riderReopenedFareChip(item), riderMarketplaceFareChangeChip(item), fareModeChipText(item), `${normalizeRideStops(routeDisplayItem.rideStops).length} stop${normalizeRideStops(routeDisplayItem.rideStops).length === 1 ? "" : "s"}`, riderMarketplaceRouteDistanceChip(item) ].filter(Boolean).filter((value) => value !== "0 stops").map(chip).join(""); const actions = node.querySelector(".request-card-actions"); if (actions) { actions.hidden = false; actions.innerHTML = ""; const view = document.createElement("button"); view.type = "button"; view.className = "secondary-action compact-action"; view.textContent = "View"; view.addEventListener("click", (event) => { event.stopPropagation(); selectRequest(item.id); }); const ignore = document.createElement("button"); ignore.type = "button"; ignore.className = "ghost-action danger compact-action"; ignore.textContent = "Decline"; ignore.addEventListener("click", (event) => { event.stopPropagation(); void ignoreRiderMarketplaceRequest(item.id); }); actions.append(view, ignore); } } else { const actions = node.querySelector(".request-card-actions"); if (actions) actions.hidden = true; const stopsSummary = rideStopsSummary(routeDisplayItem.rideStops); const waitingForOffers = isPassengerTripCard && !isMatchedPassengerTrip && !isPassengerNonNegotiableWaitingTrip && !offersForRequest(item.id).length; const waitingForAcceptance = waitingForOffers || isPassengerNonNegotiableWaitingTrip; node.classList.toggle("waiting-for-offers-card", waitingForOffers); summary.hidden = isMatchedPassengerTrip || riderPostPickupTrip; summary.textContent = isMatchedPassengerTrip || riderPostPickupTrip ? "" : isPassengerTripCard ? offersForRequest(item.id).length ? "Review rider offers and choose when ready." : isPassengerNonNegotiableWaitingTrip ? "Waiting for rider to accept..." : "Waiting for rider offers..." : activeRole() === "rider" && normalizeRideStops(routeDisplayItem.rideStops).length && requestIsActiveForCurrentRider(item) ? `${pickupLabel}. ${stopsSummary}.` : pickupLabel; summary.classList.toggle("waiting-offers-status", waitingForAcceptance); if (waitingForAcceptance) summary.setAttribute("aria-live", "polite"); else summary.removeAttribute("aria-live"); const tripChips = isPassengerTripCard ? isPassengerNonNegotiableWaitingTrip ? [ rideStatusLabel(item), fareModeChipText(item), `Offer ${formatMoney(item.fareOffer, item.country)}`, destinationDriveChip(item), normalizeRideStops(item.rideStops).length ? `${normalizeRideStops(item.rideStops).length} stop${normalizeRideStops(item.rideStops).length === 1 ? "" : "s"}` : null ] : [ rideStatusLabel(item), isMatchedPassengerTrip ? riderApproachChip(item) : `${offersForRequest(item.id).length} offers`, !isMatchedPassengerTrip ? passengerOfferFareChangeChip(item) : null, !isMatchedPassengerTrip ? fareModeChipText(item) : null, `${isMatchedPassengerTrip ? "Fare" : "Offer"} ${formatMoney(isMatchedPassengerTrip ? agreedFareForRequest(item) : item.fareOffer, item.country)}`, destinationDriveChip(item), normalizeRideStops(item.rideStops).length ? `${normalizeRideStops(item.rideStops).length} stop${normalizeRideStops(item.rideStops).length === 1 ? "" : "s"}` : null, Number(item.cancellationFeeAmount ?? 0) > 0 ? `Cancel fee ${formatMoney(item.cancellationFeeAmount, item.country)}` : null ] : isRiderActiveTrip ? [ rideStatusLabel(item), pendingRouteChange ? "Route change pending" : null, item.status === "in_progress" ? nextRideLeg(item).label : (proximityChip(item) ?? "Pickup ETA pending"), `Fare ${formatMoney(agreedFareForRequest(item), item.country)}`, destinationDriveChip(item) ] : [ activeRole() === "rider" ? paymentLabel(item.paymentPreference) : null, `${offersForRequest(item.id).length} offers`, rideStatusLabel(item), reopenedRequestChip(item), riderReopenedFareChip(item), riderApproachChip(item), proximityChip(item), destinationDriveChip(item), pickupGpsQualityChip(item), confirmationChip(item), item.businessAccountId ? "Business ride" : null, `Vehicle designation: ${carTypePreferenceLabel(item.carTypePreference)}`, normalizeRideStops(routeDisplayItem.rideStops).length ? `${normalizeRideStops(routeDisplayItem.rideStops).length} stop${normalizeRideStops(routeDisplayItem.rideStops).length === 1 ? "" : "s"}` : null, Number(item.cancellationFeeAmount ?? 0) > 0 ? `Cancel fee: ${formatMoney(item.cancellationFeeAmount, item.country)}` : null ]; node.querySelector(".chip-row").innerHTML = tripChips.filter(Boolean).map(chip).join(""); } if ((isMatchedPassengerTrip || isRiderActiveTrip) && typeof appendContactActions === "function") { appendContactActions(node, item); } if (isMatchedPassengerTrip) { renderPassengerApproachTracker(node, item, riderApproachModel(item)); } const usePassengerRideDock = passengerRideDockMode(item); const useRiderRideDock = riderRideDockMode(item); if (!isPassengerNegotiationTrip && !usePassengerRideDock && !useRiderRideDock) { renderRideGuidance(node, item); renderScheduledRideActions(node, item); } if (isPassengerNegotiationTrip) { renderPassengerNegotiationQuickActions(node, item); } else if (!usePassengerRideDock && !useRiderRideDock) { renderRideLifecycleActions(node, item); } renderPassengerFareBoost(node, item); button.addEventListener("click", () => selectRequest(item.id)); els.requestList.append(node); }); els.requestCount.textContent = `${visible.length}`; restorePassengerFareBoostFocus(fareBoostFocus, els.requestList); } function canBoostPassengerFare(request) { return Boolean(activeRole() === "passenger" && request?.status === "open" && requestBelongsToPassenger(request) && requestIsNegotiableFare(request) && request.id === state.selectedRequestId); } function passengerFareBoostGuidance(request) { const offerCount = offersForRequest(request.id).length; const ageMinutes = Math.floor((Date.now() - new Date(request.createdAt ?? Date.now()).getTime()) / 60000); const startingFareNotice = "Updating this changes your starting fare in the marketplace. Riders who have not started negotiating this request will use the new amount as their starting point."; if (isPassengerNegotiationRequest(request)) { if (offerCount > 0) { return `${offerCount} rider offer${offerCount === 1 ? "" : "s"} received. ${startingFareNotice}`; } return `Optional. Keep waiting or raise the fare before choosing a rider. ${startingFareNotice}`; } if (offerCount > 0) { return `${offerCount} rider offer${offerCount === 1 ? "" : "s"} received. ${startingFareNotice}`; } if (ageMinutes >= 5) { return `No rider offer yet. A higher fare can reopen visibility for riders who declined the earlier price. ${startingFareNotice}`; } if (ageMinutes >= 2) { return `Waka is still checking nearby riders. You can wait or increase the fare before a rider is chosen. ${startingFareNotice}`; } return `Open requests can be boosted before a rider is chosen. ${startingFareNotice}`; } function passengerFareBoostPanelOpen(request) { return Boolean(request?.id && passengerFareBoostOpenRequestId === request.id); } function togglePassengerFareBoostPanel(request) { if (!canBoostPassengerFare(request)) return; passengerFareBoostOpenRequestId = passengerFareBoostPanelOpen(request) ? null : request.id; renderAll(); if (passengerFareBoostOpenRequestId === request.id) { window.setTimeout(() => { const input = [...document.querySelectorAll(".fare-boost-input[data-request-id]")] .find((item) => item.dataset.requestId === request.id); if (input instanceof HTMLInputElement) { input.focus(); input.select(); } }, 0); } } function renderPassengerFareBoost(node, request) { if (!canBoostPassengerFare(request)) return; if (!passengerFareBoostPanelOpen(request)) return; const form = document.createElement("form"); form.className = "fare-boost-form"; form.classList.toggle("compact-fare-boost-form", isPassengerNegotiationRequest(request)); const draftValue = passengerFareBoostDrafts.has(request.id) ? passengerFareBoostDrafts.get(request.id) : String(request.fareOffer ?? ""); const isNegotiating = isPassengerNegotiationRequest(request); form.innerHTML = `

${escapeHtml(passengerFareBoostGuidance(request))}

`; const input = form.querySelector(".fare-boost-input"); input?.addEventListener("focus", () => rememberPassengerFareBoostFocus(input)); input?.addEventListener("pointerdown", () => rememberPassengerFareBoostFocus(input)); input?.addEventListener("click", () => rememberPassengerFareBoostFocus(input)); input?.addEventListener("input", () => { passengerFareBoostDrafts.set(request.id, input.value); rememberPassengerFareBoostFocus(input); }); input?.addEventListener("change", () => { passengerFareBoostDrafts.set(request.id, input.value); rememberPassengerFareBoostFocus(input); }); form.addEventListener("submit", (event) => updatePassengerFareOffer(event, request.id)); node.append(form); } function passengerOfferCounterDraftKey(offer, request) { return `${request?.id || "request"}:${offer?.id || "offer"}`; } function addActionButton(container, label, className, handler) { const button = document.createElement("button"); button.className = className; button.type = "button"; button.textContent = label; button.addEventListener("click", handler); container.append(button); } function actionLinkShouldUseNavigationHandler(href) { return /^waka-nav:/i.test(href) || /^intent:/i.test(href) || /^google\.navigation:/i.test(href) || /^waze:/i.test(href) || /^https:\/\/waze\.com\/ul/i.test(href) || /^https:\/\/www\.google\.com\/maps\//i.test(href); } function addActionLink(container, label, className, href) { if (!href) return; if (actionLinkShouldUseNavigationHandler(href)) { addActionButton(container, label, className, () => openNavigationUrl(href, { auto: true })); return; } const link = document.createElement("a"); link.className = className; link.href = href; link.target = "_blank"; link.rel = "noopener"; link.textContent = label; container.append(link); } async function rejectRiderOffer(offer) { if (!offer?.id) return; try { const updatedRequest = await rejectRiderOfferInSupabase(offer.id); state.rejectedOfferIds = [...new Set([...(state.rejectedOfferIds ?? []), offer.id])].slice(-500); state.offers = state.offers.filter((item) => item.id !== offer.id); if (state.passengerSelectedOfferId === offer.id) state.passengerSelectedOfferId = null; if (updatedRequest?.id) state.requests = upsertById(state.requests, updatedRequest); pushSystemChat(offer.requestId, "Passenger ended this rider negotiation. The ride request remains open for other riders."); saveState(); renderAll(); void refreshMarketplace({ silent: true, reason: "passenger_rejected_offer" }); } catch (error) { state.rejectedOfferIds = [...new Set([...(state.rejectedOfferIds ?? []), offer.id])].slice(-500); if (state.passengerSelectedOfferId === offer.id) state.passengerSelectedOfferId = null; saveState(); renderAll(); void showWakaGoodAlert(`Could not reject this offer: ${error.message}`); } } async function passengerCounterRiderOffer(event, offer, request) { event.preventDefault(); const form = event.currentTarget; const status = form.querySelector(".offer-counter-status"); const input = form.querySelector(".offer-counter-input"); const submit = form.querySelector("button[type='submit']"); const draftKey = passengerOfferCounterDraftKey(offer, request); const rawFareDraft = String(input?.value ?? passengerOfferCounterDrafts.get(draftKey) ?? ""); passengerOfferCounterDrafts.set(draftKey, rawFareDraft); const nextFare = Number(rawFareDraft.replace(/[^\d.]/g, "")); const minimumNextFare = Number(request.fareOffer ?? 0); if (!Number.isInteger(nextFare)) { status.textContent = "Use a whole-dollar counter-offer."; return; } if (!nextFare || nextFare <= minimumNextFare) { status.textContent = `Enter any whole-dollar fare higher than ${formatMoney(minimumNextFare, request.country)}.`; return; } if (typeof passengerCanSendFareProposal === "function" && !passengerCanSendFareProposal(request)) { status.textContent = fareProposalLimitMessage("passenger", request); return; } try { if (submit) submit.disabled = true; status.textContent = "Sending counter-offer..."; const savedRequest = await updateRideRequestFareInSupabase(request.id, nextFare); state.requests = state.requests.map((item) => item.id === request.id ? { ...item, ...(savedRequest ?? {}), fareOffer: nextFare, fareHistory: savedRequest?.fareHistory ?? savedRequest?.fare_history ?? fareProposalHistoryWithNextFare(item, item.fareOffer, nextFare) } : item); state.rejectedOfferIds = (state.rejectedOfferIds ?? []).filter((id) => id !== offer.id); passengerOfferCounterDrafts.delete(draftKey); passengerOfferCounterLastFocus = null; if (state.passengerSelectedOfferId === offer.id) state.passengerSelectedOfferId = null; if (input instanceof HTMLInputElement) input.blur(); pushSystemChat(request.id, `Passenger countered by increasing the fare offer to ${formatMoney(nextFare, request.country)}. Riders can accept or counter higher.`); saveState(); renderAll(); void refreshMarketplace({ silent: true }); } catch (error) { status.textContent = error.message; } finally { if (submit) submit.disabled = false; } } function riderApproachTrackPercent(model) { if (!model || !Number.isFinite(Number(model.distanceKm))) return 8; const distanceKm = Math.max(0, Number(model.distanceKm)); const rangeKm = 5; return Math.max(8, Math.min(92, Math.round((1 - Math.min(distanceKm, rangeKm) / rangeKm) * 84 + 8))); } function requestMapPoint(request, areaName, fallbackX, fallbackY) { const area = findArea(request?.country, request?.city, areaName); return { x: Number.isFinite(Number(area?.x)) ? area.x : fallbackX, y: Number.isFinite(Number(area?.y)) ? area.y : fallbackY }; } function riderApproachMapPoint(request, model) { const pickup = requestMapPoint(request, request?.pickupArea, 58, 52); const destination = requestMapPoint(request, request?.destinationArea, 82, 48); const progress = riderApproachTrackPercent(model) / 100; return { x: Math.max(8, Math.min(92, pickup.x - (pickup.x - 12) * (1 - progress))), y: Math.max(10, Math.min(88, pickup.y - (pickup.y - 14) * (1 - progress))), pickup, destination }; } function approachMapMarker(point, label, type) { const gps = normalizeGpsPoint(point); if (!gps) return null; return { latitude: gps.latitude, longitude: gps.longitude, label, type }; } function passengerApproachTileMapEnabled() { const tilesAvailable = typeof workspaceMapTileMapsAvailable === "function" ? workspaceMapTileMapsAvailable() : String(appConfig?.mapboxAccessToken || "").trim().length > 0; return Boolean( configFlagEnabled(appConfig?.passengerApproachTileMapEnabled) && tilesAvailable ); } function passengerApproachTrackerVisible(request) { return Boolean(selectedRiderIdForRequest(request) && ["matched", "arrived"].includes(request?.status)); } const passengerApproachRouteTraceRefreshMs = 2 * 60 * 1000; const passengerApproachRouteTraceCache = new Map(); const passengerApproachRouteTraceInFlight = new Set(); const passengerApproachRouteTraceLastFetchAt = new Map(); function passengerApproachRouteTraceEnabled() { return typeof fetchRouteEstimateFromEdge === "function" && typeof routeEstimatesEnabled === "function" && routeEstimatesEnabled() && passengerApproachTileMapEnabled(); } function passengerApproachRouteTraceKey(request, rider, pickup) { if (!request?.id || !rider || !pickup) return ""; return [ request.id, Number(rider.latitude).toFixed(3), Number(rider.longitude).toFixed(3), Number(pickup.latitude).toFixed(3), Number(pickup.longitude).toFixed(3) ].join(":"); } function passengerApproachRouteTracePolyline(key) { return passengerApproachRouteTraceCache.get(key)?.routePolyline || ""; } async function maybeRefreshPassengerApproachRouteTrace(request, rider, pickup) { if (!passengerApproachRouteTraceEnabled()) return; const key = passengerApproachRouteTraceKey(request, rider, pickup); if (!key || passengerApproachRouteTraceCache.has(key) || passengerApproachRouteTraceInFlight.has(key)) return; const now = Date.now(); const lastFetchAt = passengerApproachRouteTraceLastFetchAt.get(key) || 0; if (lastFetchAt && now - lastFetchAt < passengerApproachRouteTraceRefreshMs) return; passengerApproachRouteTraceLastFetchAt.set(key, now); passengerApproachRouteTraceInFlight.add(key); try { const estimate = await fetchRouteEstimateFromEdge({ origin: { address: `${selectedRiderFirstNameForRequest(request)} live location`, latitude: rider.latitude, longitude: rider.longitude }, destination: { address: request.pickupDescription || "Pickup location", latitude: pickup.latitude, longitude: pickup.longitude, city: request.city, country: request.country }, stops: [], travelMode: "DRIVE" }); const routePolyline = String(estimate?.routePolyline || ""); if (routePolyline) { passengerApproachRouteTraceCache.set(key, { routePolyline, updatedAt: new Date().toISOString() }); if (typeof renderAll === "function") renderAll(); } } catch (error) { if (typeof logClientWarning === "function") { logClientWarning("Passenger approach road trace could not be loaded; direct approach line remains visible.", error); } } finally { passengerApproachRouteTraceInFlight.delete(key); } } function passengerApproachLiveMapModel(request, model) { const rider = approachMapMarker(model?.riderGps ?? riderApproachGps(request), "R", "rider"); const pickup = approachMapMarker(model?.pickupGps ?? requestPickupGps(request), "P", "pickup"); if (!rider || !pickup) return null; const destination = approachMapMarker(model?.destinationGps ?? requestDestinationGps(request), "D", "destination"); const routePoints = [rider, pickup].filter(Boolean); const traceKey = passengerApproachRouteTraceKey(request, rider, pickup); const routePathPoints = typeof workspaceMapRoutePathPointsFromPolyline === "function" ? workspaceMapRoutePathPointsFromPolyline(passengerApproachRouteTracePolyline(traceKey)) : []; maybeRefreshPassengerApproachRouteTrace(request, rider, pickup); const markers = [rider, pickup, destination].filter(Boolean); const centerPoints = routePathPoints.length ? routePathPoints : routePoints; return { center: typeof workspaceMapCenterForPoints === "function" ? workspaceMapCenterForPoints(centerPoints) : rider, markers, routePoints, routePathPoints, zoom: 15 }; } function renderPassengerApproachTracker(node, request, model) { if (!passengerApproachTrackerVisible(request)) return; const tracker = document.createElement("div"); tracker.className = "approach-tracker"; const track = document.createElement("div"); track.className = "approach-track"; track.setAttribute("aria-hidden", "true"); const riderDot = document.createElement("span"); riderDot.className = "approach-dot rider-dot"; riderDot.style.left = `${riderApproachTrackPercent(model)}%`; const pickupDot = document.createElement("span"); pickupDot.className = "approach-dot pickup-dot"; const labelRow = document.createElement("div"); labelRow.className = "approach-labels"; const riderLabel = document.createElement("span"); riderLabel.textContent = selectedRiderFirstNameForRequest(request); const pickupLabel = document.createElement("span"); pickupLabel.textContent = "Pickup"; labelRow.append(riderLabel, pickupLabel); const meta = document.createElement("small"); if (model) { const updated = model.capturedAt ? `Updated ${formatGpsAgeLabel({ capturedAt: model.capturedAt })}.` : "Refreshes automatically."; meta.textContent = `${formatPickupEta(model.etaMinutes)}. ${model.isLive ? "Live rider location." : "Rider location estimate."} ${updated} Map updates automatically.`; } else { meta.textContent = "Waiting for the rider's live GPS update."; } const mapPoint = riderApproachMapPoint(request, model); const liveMapModel = passengerApproachLiveMapModel(request, model); const useMapboxApproachTiles = passengerApproachTileMapEnabled(); const map = document.createElement("div"); map.className = liveMapModel ? "approach-map approach-live-map" : "approach-map"; map.setAttribute("aria-label", "Matched rider approach map"); if (!liveMapModel) { const routeLine = document.createElement("span"); routeLine.className = "approach-map-route"; routeLine.setAttribute("aria-hidden", "true"); const riderPin = document.createElement("span"); riderPin.className = "approach-map-pin approach-map-rider"; riderPin.style.left = `${mapPoint.x}%`; riderPin.style.top = `${mapPoint.y}%`; riderPin.textContent = "R"; const pickupPin = document.createElement("span"); pickupPin.className = "approach-map-pin approach-map-pickup"; pickupPin.style.left = `${mapPoint.pickup.x}%`; pickupPin.style.top = `${mapPoint.pickup.y}%`; pickupPin.textContent = "P"; const destinationPin = document.createElement("span"); destinationPin.className = "approach-map-pin approach-map-destination"; destinationPin.style.left = `${mapPoint.destination.x}%`; destinationPin.style.top = `${mapPoint.destination.y}%`; destinationPin.textContent = "D"; map.append(routeLine, riderPin, pickupPin, destinationPin); } track.append(riderDot, pickupDot); tracker.append(map, track, labelRow, meta); node.append(tracker); if (liveMapModel && typeof renderInlineWorkspaceMap === "function") { window.requestAnimationFrame(() => renderInlineWorkspaceMap(map, liveMapModel, { useMapboxTiles: useMapboxApproachTiles })); } } function renderRideGuidance(node, request) { if (!request || !roleCanSeeRequest(request) || !["passenger", "rider"].includes(activeRole())) return; const guidance = document.createElement("div"); guidance.className = "ride-guidance"; const copy = document.createElement("div"); const title = document.createElement("strong"); const detail = document.createElement("span"); const actions = document.createElement("div"); actions.className = "review-actions"; if (activeRole() === "rider") { const matchedToRider = requestIsActiveForCurrentRider(request); if (!matchedToRider) return; if (pendingRouteChangeForRequest(request)) { renderRouteChangeRequestPanel(node, request); return; } if (riderShouldHoldNextRideNavigation(request)) { title.textContent = selectedRiderIdForRequest(request) === state.rider?.id ? "Next ride queued" : "Next request available"; detail.textContent = selectedRiderIdForRequest(request) === state.rider?.id ? "This pickup is saved for after the current trip. Finish the active ride before opening the next pickup navigation." : "You can review or offer because you are near drop-off. Current trip navigation stays active until the ride is complete."; copy.append(title, detail); guidance.append(copy); node.append(guidance); return; } const model = pickupProximityModel(request); const pickupText = requestPickupDisplayText(request); if (request.status === "in_progress") { const leg = nextRideLeg(request); title.textContent = leg.type === "destination" ? "Destination navigation" : leg.label; detail.textContent = `${leg.destination}. ${rideStopProgressText(request)}`; addActionLink(actions, riderContinueNavigationLabel(leg), "secondary-action map-action", nextRideLegNavigationUrl(request)); } else { title.textContent = request.status === "arrived" ? "Ready at pickup" : "Pickup navigation"; detail.textContent = model ? `${formatPickupEta(model.etaMinutes)} from your live location.` : "Open navigation to the verified pickup point."; if (request.status === "matched") { addActionLink(actions, "Navigate to pickup", "secondary-action map-action", riderPickupNavigationUrl(request)); } } } else { if (!requestBelongsToPassenger(request)) return; const model = riderApproachModel(request); const selectedRiderId = selectedRiderIdForRequest(request); const reopenedAfterRiderCancel = passengerRequestHasRiderCancelReopenNotice(request); title.textContent = selectedRiderId ? "Track rider" : reopenedAfterRiderCancel ? "Rider cancelled" : "Ride request"; detail.textContent = selectedRiderId ? model ? `${selectedRiderFirstNameForRequest(request)}: ${formatPickupEta(model.etaMinutes)} away. ${model.isLive ? "Live GPS" : model.source}.` : `${selectedRiderFirstNameForRequest(request)} selected; approach pending.` : reopenedAfterRiderCancel ? "Your request is open again for other nearby riders. You do not need to create a new ride request." : `Request is open for nearby riders. Destination: ${requestDestinationDisplayText(request)}.`; } copy.append(title, detail); guidance.append(copy); if (actions.children.length) guidance.append(actions); node.append(guidance); } function renderScheduledRideActions(node, request) { if (!isScheduledRequest(request) || request.id !== state.selectedRequestId) return; const actions = document.createElement("div"); actions.className = "review-actions"; if (activeRole() === "passenger" && requestBelongsToPassenger(request) && request.status === "matched") { addActionButton(actions, "Request rider confirmation", "secondary-action", () => requestScheduledRideConfirmation(request.id)); addActionButton(actions, "Release rider and reopen", "ghost-action danger", () => releaseScheduledRide(request.id, "Passenger released the rider and reopened the scheduled ride.")); } if (activeRole() === "rider" && selectedRiderIdForRequest(request) === state.rider?.id && request.riderConfirmationStatus === "requested") { addActionButton(actions, "Confirm scheduled ride", "secondary-action", () => confirmScheduledRide(request.id)); addActionButton(actions, "Cannot keep plan", "ghost-action danger", () => releaseScheduledRide(request.id, "Rider cannot keep the scheduled ride. Passenger can choose another rider.")); } if (actions.children.length) node.append(actions); } function riderContinueNavigationLabel(nextLeg) { if (nextLeg?.type === "destination") return "Continue to Destination"; if (nextLeg?.type === "stop") return `Continue to ${nextLeg.label}`; return `Navigate to ${nextLeg?.label ?? "pickup"}`; } function renderRideLifecycleActions(node, request) { if (!canSeeRideLifecycleActions(request)) return; const panel = document.createElement("div"); panel.className = "ride-guidance ride-action-panel"; const copy = document.createElement("div"); const title = document.createElement("strong"); const detail = document.createElement("span"); const actions = document.createElement("div"); actions.className = "review-actions"; title.textContent = "Ride actions"; detail.textContent = rideLifecycleActionSummary(request); if (canCancelBeforeStart(request)) { const label = activeRole() === "rider" ? "Cancel before start" : "Cancel ride"; addActionButton(actions, label, "ghost-action danger", () => cancelRideBeforeStart(request.id)); } if (canCancelInProgress(request)) { addActionButton(actions, activeRole() === "passenger" ? "Cancel ride" : "End ride early", "ghost-action danger", () => cancelRideInProgress(request.id)); } const riderOwnsActiveRequest = activeRole() === "rider" && selectedRiderIdForRequest(request) === state.rider?.id; if (riderOwnsActiveRequest && request.status === "matched") { addActionLink(actions, "Navigate to pickup", "secondary-action map-action", riderPickupNavigationUrl(request)); addActionButton(actions, "Arrived at pickup", "secondary-action", () => changeRideLifecycle(request.id, "arrive")); } if (riderOwnsActiveRequest && request.status === "arrived") { addActionButton(actions, "Picked up passenger", "secondary-action", async () => { const changed = await changeRideLifecycle(request.id, "start"); if (changed) { const updated = state.requests.find((item) => item.id === request.id) ?? request; openNavigationUrl(nextRideLegNavigationUrl(updated), { auto: true }); } }); } const nextLeg = nextRideLeg(request); if (riderOwnsActiveRequest && request.status === "in_progress") { addActionLink(actions, riderContinueNavigationLabel(nextLeg), "secondary-action map-action", nextRideLegNavigationUrl(request)); } if (riderOwnsActiveRequest && request.status === "in_progress" && nextLeg.type === "stop") { addActionButton(actions, `Arrived at ${nextLeg.label}`, "secondary-action", () => changeRideLifecycle(request.id, "stop")); } const canComplete = request.status === "in_progress" && riderOwnsActiveRequest && nextLeg.type === "destination"; if (canComplete) { addActionButton(actions, "Complete ride at drop-off", "secondary-action", () => changeRideLifecycle(request.id, "complete")); } if (activeRole() === "passenger" && !actions.children.length) return; copy.append(title, detail); panel.append(copy); if (actions.children.length) panel.append(actions); node.append(panel); } function renderPassengerNegotiationQuickActions(node, request) { if (!isPassengerNegotiationRequest(request)) return; const actions = document.createElement("div"); actions.className = "passenger-negotiation-quick-actions"; if (!passengerRideDockMode(request) && canCancelBeforeStart(request)) { addActionButton(actions, "Cancel request", "ghost-action danger compact-action", () => cancelRideBeforeStart(request.id)); } const boost = document.createElement("button"); boost.type = "button"; boost.className = "secondary-action icon-action passenger-offer-update-action"; boost.title = passengerFareBoostPanelOpen(request) ? "Hide offer editor" : "Update starting fare"; boost.setAttribute("aria-label", boost.title); boost.setAttribute("aria-pressed", passengerFareBoostPanelOpen(request) ? "true" : "false"); boost.textContent = "$"; boost.addEventListener("click", () => togglePassengerFareBoostPanel(request)); actions.append(boost); if (actions.children.length) node.append(actions); } function renderRouteChangeRequestPanel(node, request) { const change = pendingRouteChangeForRequest(request); if (!change) return; const panel = document.createElement("div"); panel.className = "ride-guidance ride-action-panel"; const copy = document.createElement("div"); const title = document.createElement("strong"); const detail = document.createElement("span"); const actions = document.createElement("div"); actions.className = "review-actions"; const label = routeChangeTypeLabel(change.type); const addOn = formatMoney(change.additionalFare, request.country); const total = formatMoney(change.totalFare, request.country); const proposedRequest = requestWithPendingRouteChange(request, change); const destination = requestDestinationDisplayText(proposedRequest); const stops = normalizeRideStops(proposedRequest.rideStops); const stopText = stops.length ? `${stops.length} stop${stops.length === 1 ? "" : "s"}: ${stops.join("; ")}` : "No added stops."; const delta = change.routeDelta; const routeLine = delta ? ` Added drive: ${formatRouteDistanceForRequest(Number(delta.addedMiles ?? 0), request)}${Number(delta.addedMinutes ?? 0) > 0 ? `, Google traffic change about ${Math.ceil(Number(delta.addedMinutes))} minutes` : ""}.` : ""; const changeSummary = change.type === "add_stop" ? "Passenger requested an added stop before continuing this ride." : "Passenger requested a final destination change."; title.textContent = activeRole() === "rider" ? `Acknowledge ${label}` : `${label} pending`; detail.textContent = activeRole() === "rider" ? `${changeSummary} Proposed route: ${destination}. ${stopText}.${routeLine} Added fare: ${addOn}. New ride total: ${total}. Acknowledge to update the route before proceeding, or decline to keep the current route.` : `Waiting for rider approval. Proposed route: ${destination}. ${stopText}.${routeLine} Added fare if accepted: ${addOn}. New ride total: ${total}.`; if (activeRole() === "rider" && riderIdentityMatches(selectedRiderIdForRequest(request))) { addActionButton(actions, "Acknowledge and update route", "secondary-action", () => acceptRouteChangeRequest(change.id)); addActionButton(actions, "Decline route change", "ghost-action danger", () => declineRouteChangeRequest(change.id)); } copy.append(title, detail); panel.append(copy); if (actions.children.length) panel.append(actions); node.append(panel); } function riderRouteChangeDecisionContext() { if (activeRole() !== "rider" || !state.rider?.id) return null; const activeRequests = state.requests .filter((request) => requestIsActiveForCurrentRider(request)) .sort((left, right) => (left.id === state.selectedRequestId ? -1 : 0) - (right.id === state.selectedRequestId ? -1 : 0)); for (const request of activeRequests) { const change = pendingRouteChangeForRequest(request); if (change) return { request, change }; } return null; } function renderRiderRouteChangeDecisionModal() { const context = riderRouteChangeDecisionContext(); const existing = document.querySelector(".route-change-decision-backdrop"); if (!context) { existing?.remove(); return; } const { request, change } = context; if (existing?.dataset.changeId === change.id) return; existing?.remove(); const label = routeChangeTypeLabel(change.type); const addOn = formatMoney(change.additionalFare, request.country); const total = formatMoney(change.totalFare, request.country); const stops = normalizeRideStops(change.rideStops); const destination = change.destinationFormattedAddress || change.destination || requestDestinationDisplayText(request); const stopLine = stops.length ? `${stops.length} stop${stops.length === 1 ? "" : "s"} on route: ${stops.join("; ")}` : "No added stop."; const delta = change.routeDelta; const distanceLine = delta ? `Added drive: ${formatRouteDistanceForRequest(Number(delta.addedMiles ?? 0), request)}${Number(delta.addedMinutes ?? 0) > 0 ? `, traffic change about ${Math.ceil(Number(delta.addedMinutes))} minutes` : ""}.` : ""; const backdrop = document.createElement("div"); backdrop.className = "route-change-decision-backdrop"; backdrop.dataset.changeId = change.id; backdrop.setAttribute("role", "dialog"); backdrop.setAttribute("aria-modal", "true"); backdrop.innerHTML = `
Route change request

${escapeHtml(change.type === "add_stop" ? "Passenger requested a stop" : "Passenger changed destination")}

${escapeHtml(change.type === "add_stop" ? "Accept to add the stop before continuing. Decline to keep the current route." : "Accept to update the final destination. Decline to keep the current route.")}

Destination
${escapeHtml(destination)}
Stops
${escapeHtml(stopLine)}
Fare
${escapeHtml(`Added ${addOn}. New total ${total}.`)}
${distanceLine ? `
Drive
${escapeHtml(distanceLine)}
` : ""}

`; const status = backdrop.querySelector(".route-change-decision-status"); const buttons = [...backdrop.querySelectorAll("button")]; const setBusy = (message) => { buttons.forEach((button) => { button.disabled = true; }); status.textContent = message; }; const stillPending = () => pendingRouteChangeForRequest(stateLookupIndexes().requestMap.get(request.id) ?? request)?.id === change.id; backdrop.querySelector(".route-change-accept")?.addEventListener("click", async () => { setBusy("Accepting route change..."); await acceptRouteChangeRequest(change.id); if (!stillPending()) backdrop.remove(); else buttons.forEach((button) => { button.disabled = false; }); }); backdrop.querySelector(".route-change-decline")?.addEventListener("click", async () => { setBusy("Declining route change..."); await declineRouteChangeRequest(change.id); if (!stillPending()) backdrop.remove(); else buttons.forEach((button) => { button.disabled = false; }); }); document.body.append(backdrop); playNearbyRideCue(); } function renderRideTipForm(node, request) { if (!canTipRequest(request)) return; const form = document.createElement("form"); form.className = "fare-boost-form"; form.innerHTML = `

Tips go to the rider after Stripe processing fees; Waka does not add a ride fee to tips.

`; form.addEventListener("submit", (event) => submitRideTip(event, request.id)); node.append(form); } function renderDestinationUpdateForm(node, request) { if (!canUpdateRideDestination(request)) return; const form = document.createElement("form"); form.className = "fare-boost-form destination-update-form"; form.dataset.routeChangeRequestId = passengerDestinationUpdateDraftKey(request); form.dataset.requestId = request.id; const destinationSuggestionId = `destinationUpdateSuggestions-${request.id}`; const stopSuggestionId = `stopsUpdateSuggestions-${request.id}`; const draft = passengerDestinationUpdateDraftForRequest(request); const windowText = request.status === "in_progress" ? "Pickup has started. Ask the rider verbally before sending; the rider must approve in the app." : routeChangeNeedsRiderApproval(request) ? "Matched rider must approve route changes. Waka calculates the added fare before sending." : "Open request route changes update the offered fare automatically."; form.innerHTML = `

${windowText}

`; setupDestinationUpdateAutocomplete(form, request); form.addEventListener("submit", (event) => submitDestinationUpdate(event, request.id)); node.append(form); } function passengerRideDockMode(request) { return Boolean(activeRole() === "passenger" && request && requestBelongsToPassenger(request) && ((selectedRiderIdForRequest(request) && ["matched", "arrived", "in_progress", "completed"].includes(request.status)) || isPassengerWaitingForRiderRequest(request))); } function passengerRideDockTools(request) { if (!passengerRideDockMode(request)) return []; const tools = []; const addTool = (id, label, icon, title) => tools.push({ id, label, icon, title }); const completed = request.status === "completed"; if (isPassengerWaitingForRiderRequest(request)) { if (canUpdateRideDestination(request) || pendingRouteChangeForRequest(request)) addTool("route", "Route", "R", "Change destination or add a stop"); if (canCancelBeforeStart(request)) addTool("cancel", "Cancel", "X", "Cancel ride"); return tools; } if (completed) { if (canRateRequest(request) || existingRatingForRequest(request)) addTool("rating", "Rate", "*", "Rating"); addTool("payment", "Fare", "$", "Payment and fare summary"); if (canReportOnRequest(request)) addTool("support", "Support", "!", "Support or report issue"); addTool("contact", "Contact", "C", "Contact rider"); return tools; } addTool("contact", "Contact", "C", "Contact rider"); if (canUpdateRideDestination(request) || pendingRouteChangeForRequest(request)) addTool("route", "Route", "R", "Add stop or change destination"); if (canReportOnRequest(request)) addTool("support", "Support", "!", "Support or report issue"); if (canCancelBeforeStart(request) || canCancelInProgress(request)) addTool("cancel", "Cancel", "X", "Cancel ride"); return tools; } function passengerRideDockCurrentPanel(request) { if (!passengerRideDockMode(request)) { passengerRideDockRequestId = null; passengerRideDockOpenPanel = null; return null; } if (passengerRideDockRequestId !== request.id) { passengerRideDockRequestId = request.id; passengerRideDockOpenPanel = null; } const toolIds = new Set(passengerRideDockTools(request).map((tool) => tool.id)); if (!toolIds.has(passengerRideDockOpenPanel)) passengerRideDockOpenPanel = null; return passengerRideDockOpenPanel; } function setPassengerRideDockPanel(request, panelId) { if (!passengerRideDockMode(request)) return; passengerRideDockRequestId = request.id; passengerRideDockOpenPanel = passengerRideDockOpenPanel === panelId ? null : panelId; renderAll(); } function renderPassengerRideToolHeader(node, titleText, detailText) { const panel = document.createElement("div"); panel.className = "ride-tool-panel"; const title = document.createElement("strong"); const detail = document.createElement("span"); title.textContent = titleText; detail.textContent = detailText; panel.append(title, detail); node.append(panel); return panel; } function renderPassengerRideDockButtons(node, request, activePanel) { const tools = passengerRideDockTools(request); if (!tools.length) return; const dock = document.createElement("div"); dock.className = "passenger-ride-dock"; dock.setAttribute("aria-label", "Ride tools"); tools.forEach((tool) => { const button = document.createElement("button"); button.type = "button"; button.className = "ride-tool-button"; button.classList.toggle("active", activePanel === tool.id); button.title = tool.title; button.setAttribute("aria-pressed", activePanel === tool.id ? "true" : "false"); button.innerHTML = ` ${escapeHtml(tool.label)} `; button.addEventListener("click", () => setPassengerRideDockPanel(request, tool.id)); dock.append(button); }); node.append(dock); return dock; } function renderPassengerRideCancelPanel(node, request) { const panel = renderPassengerRideToolHeader(node, "Cancel ride", rideLifecycleActionSummary(request)); const actions = document.createElement("div"); actions.className = "review-actions"; if (canCancelBeforeStart(request)) { addActionButton(actions, "Cancel ride", "ghost-action danger", () => cancelRideBeforeStart(request.id)); } else if (canCancelInProgress(request)) { addActionButton(actions, "Cancel ride", "ghost-action danger", () => cancelRideInProgress(request.id)); } if (actions.children.length) panel.append(actions); } function renderPassengerRidePaymentPanel(node, request) { const total = formatMoney(agreedFareForRequest(request), request.country); const method = paymentLabel(request.paymentPreference); renderPassengerRideToolHeader( node, "Payment summary", `Final matched fare: ${total}. Payment method: ${method}. Destination: ${requestDestinationDisplayText(request)}.` ); renderRideTipForm(node, request); } function renderPassengerRideDock(node, request, destinationUpdateFocus = null) { const activePanel = passengerRideDockCurrentPanel(request); node.innerHTML = ""; const shell = document.createElement("div"); shell.className = "passenger-ride-tool-shell"; const content = document.createElement("div"); content.className = "passenger-ride-tool-content"; content.dataset.passengerRideToolContent = "true"; if (!activePanel) content.hidden = true; if (activePanel === "route") { renderPassengerRideToolHeader( content, request.status === "in_progress" ? "Update route" : "Route change", "Change destination or add one stop. Waka recalculates the added fare before sending it to the rider." ); renderRouteChangeRequestPanel(content, request); renderDestinationUpdateForm(content, request); restorePassengerDestinationUpdateFocus(destinationUpdateFocus, content); } else if (activePanel === "support") { renderPassengerRideToolHeader(content, "Support", "Contact Waka or report a ride issue."); } else if (activePanel === "cancel") { renderPassengerRideCancelPanel(content, request); } else if (activePanel === "payment") { renderPassengerRidePaymentPanel(content, request); } else if (activePanel === "rating") { const riderName = selectedRiderFirstNameForRequest(request); renderPassengerRideToolHeader( content, `Rate ${riderName}`, `${requestPickupDisplayText(request)} to ${requestDestinationDisplayText(request)}` ); } else if (activePanel === "contact") { renderPassengerRideToolHeader(content, "Contact", "Call or message the rider from this ride."); } renderPassengerRideDockButtons(shell, request, activePanel); shell.append(content); node.append(shell); return request; } function riderRideDockMode(request) { return Boolean(activeRole() === "rider" && request && riderIdentityMatches(selectedRiderIdForRequest(request)) && ["matched", "arrived", "in_progress", "completed"].includes(request.status)); } function riderRideDockTools(request) { if (!riderRideDockMode(request)) return []; const tools = []; const addTool = (id, label, icon, title) => tools.push({ id, label, icon, title }); const completed = request.status === "completed"; const pendingChange = pendingRouteChangeForRequest(request); if (completed) { addTool("fare", "Fare", "$", "Fare summary"); if (canReportOnRequest(request)) addTool("support", "Support", "!", "Support or report issue"); addTool("contact", "Contact", "C", "Contact passenger"); return tools; } if (pendingChange) { addTool("route", "Route", "R", "Accept or decline requested route change"); addTool("contact", "Contact", "C", "Contact passenger"); if (canReportOnRequest(request)) addTool("support", "Support", "!", "Support or report issue"); return tools; } if (request.status === "matched" || request.status === "in_progress") { addTool("navigate", "Navigate", "N", request.status === "matched" ? "Navigate to pickup" : "Navigate to next stop or destination"); } addTool("progress", "Progress", "P", "Update pickup, stop, or completion status"); if (pendingRouteChangeForRequest(request)) addTool("route", "Route", "R", "Review requested route change"); addTool("contact", "Contact", "C", "Contact passenger"); if (canReportOnRequest(request)) addTool("support", "Support", "!", "Support or report issue"); if (canCancelBeforeStart(request) || canCancelInProgress(request)) addTool("cancel", "Cancel", "X", "Cancel this ride"); return tools; } function riderRideDockCurrentPanel(request) { if (!riderRideDockMode(request)) { riderRideDockRequestId = null; riderRideDockOpenPanel = null; return null; } if (riderRideDockRequestId !== request.id) { riderRideDockRequestId = request.id; riderRideDockOpenPanel = null; } const toolIds = new Set(riderRideDockTools(request).map((tool) => tool.id)); if (!toolIds.has(riderRideDockOpenPanel)) riderRideDockOpenPanel = null; return riderRideDockOpenPanel; } function setRiderRideDockPanel(request, panelId) { if (!riderRideDockMode(request)) return; riderRideDockRequestId = request.id; riderRideDockOpenPanel = riderRideDockOpenPanel === panelId ? null : panelId; renderAll(); } function renderRiderRideDockButtons(node, request, activePanel) { const tools = riderRideDockTools(request); if (!tools.length) return null; const dock = document.createElement("div"); dock.className = "passenger-ride-dock rider-ride-dock"; dock.setAttribute("aria-label", "Rider ride tools"); tools.forEach((tool) => { const button = document.createElement("button"); button.type = "button"; button.className = "ride-tool-button"; button.classList.toggle("active", activePanel === tool.id); button.title = tool.title; button.setAttribute("aria-pressed", activePanel === tool.id ? "true" : "false"); button.innerHTML = ` ${escapeHtml(tool.label)} `; button.addEventListener("click", () => setRiderRideDockPanel(request, tool.id)); dock.append(button); }); node.append(dock); return dock; } function renderRiderRideNavigationPanel(node, request) { if (pendingRouteChangeForRequest(request)) { renderPassengerRideToolHeader(node, "Route change pending", "Accept or decline the passenger route change before opening navigation."); renderRouteChangeRequestPanel(node, request); return; } const nextLeg = nextRideLeg(request); const isPickupLeg = request.status === "matched"; const label = isPickupLeg ? "pickup" : nextLeg.label; const panel = renderPassengerRideToolHeader( node, "Navigation", isPickupLeg ? `Open navigation to ${riderActivePickupDisplayText(request)}.` : `Open navigation to ${label}.` ); const actions = document.createElement("div"); actions.className = "review-actions"; if (isPickupLeg) { addActionLink(actions, "Navigate to pickup", "secondary-action map-action", riderPickupNavigationUrl(request)); } else if (request.status === "in_progress") { addActionLink(actions, riderContinueNavigationLabel(nextLeg), "secondary-action map-action", nextRideLegNavigationUrl(request)); } if (actions.children.length) panel.append(actions); } function renderRiderRideProgressPanel(node, request) { const panel = renderPassengerRideToolHeader(node, "Ride progress", rideLifecycleActionSummary(request)); const actions = document.createElement("div"); actions.className = "review-actions"; appendRiderRideProgressActions(actions, request); if (actions.children.length) panel.append(actions); } function appendRiderRideProgressActions(actions, request) { const nextLeg = nextRideLeg(request); if (request.status === "matched") { addActionButton(actions, "Arrived at pickup", "secondary-action", () => changeRideLifecycle(request.id, "arrive")); } else if (request.status === "arrived") { addActionButton(actions, "Picked up passenger", "secondary-action", async () => { const changed = await changeRideLifecycle(request.id, "start"); if (changed) { const updated = state.requests.find((item) => item.id === request.id) ?? request; openNavigationUrl(nextRideLegNavigationUrl(updated), { auto: true }); } }); } else if (request.status === "in_progress" && nextLeg.type === "stop") { addActionLink(actions, riderContinueNavigationLabel(nextLeg), "secondary-action map-action", nextRideLegNavigationUrl(request)); addActionButton(actions, `Arrived at ${nextLeg.label}`, "secondary-action", () => changeRideLifecycle(request.id, "stop")); } else if (request.status === "in_progress" && nextLeg.type === "destination") { addActionLink(actions, riderContinueNavigationLabel(nextLeg), "secondary-action map-action", nextRideLegNavigationUrl(request)); addActionButton(actions, "Complete ride at drop-off", "secondary-action", () => changeRideLifecycle(request.id, "complete")); } return actions.children.length > 0; } function renderRiderRideNextProgressAction(node, request) { if (!riderRideDockMode(request) || request.status === "completed") return; if (pendingRouteChangeForRequest(request)) { renderRouteChangeRequestPanel(node, request); return; } const panel = document.createElement("div"); panel.className = "ride-tool-panel rider-progress-action-panel"; const title = document.createElement("strong"); title.textContent = "Next ride step"; const detail = document.createElement("span"); detail.textContent = rideLifecycleActionSummary(request); const actions = document.createElement("div"); actions.className = "review-actions"; if (!appendRiderRideProgressActions(actions, request)) return; panel.append(title, detail, actions); node.append(panel); } function renderRiderRideCancelPanel(node, request) { const panel = renderPassengerRideToolHeader(node, "Cancel ride", rideLifecycleActionSummary(request)); const actions = document.createElement("div"); actions.className = "review-actions"; if (canCancelBeforeStart(request)) { addActionButton(actions, "Cancel before start", "ghost-action danger", () => cancelRideBeforeStart(request.id)); } else if (canCancelInProgress(request)) { addActionButton(actions, "End ride early", "ghost-action danger", () => cancelRideInProgress(request.id)); } if (actions.children.length) panel.append(actions); } function renderRiderRideFarePanel(node, request) { renderPassengerRideToolHeader( node, "Fare summary", `Final matched fare: ${formatMoney(agreedFareForRequest(request), request.country)}. Route: ${riderActiveRouteDisplayText(request)}.` ); } function renderRiderRideDock(node, request) { const activePanel = riderRideDockCurrentPanel(request); node.innerHTML = ""; const shell = document.createElement("div"); shell.className = "passenger-ride-tool-shell rider-ride-tool-shell"; const content = document.createElement("div"); content.className = "passenger-ride-tool-content rider-ride-tool-content"; content.dataset.riderRideToolContent = "true"; if (!activePanel) content.hidden = true; if (activePanel === "navigate") { renderRiderRideNavigationPanel(content, request); } else if (activePanel === "progress") { renderRiderRideProgressPanel(content, request); } else if (activePanel === "route") { renderPassengerRideToolHeader(content, "Route update", "Review the passenger route-change request before changing course."); renderRouteChangeRequestPanel(content, request); } else if (activePanel === "contact") { renderPassengerRideToolHeader(content, "Contact", "Call or message the passenger from this ride."); } else if (activePanel === "support") { renderPassengerRideToolHeader(content, "Support", "Contact Waka or report a ride issue."); } else if (activePanel === "cancel") { renderRiderRideCancelPanel(content, request); } else if (activePanel === "fare") { renderRiderRideFarePanel(content, request); } renderRiderRideNextProgressAction(shell, request); renderRiderRideDockButtons(shell, request, activePanel); shell.append(content); node.append(shell); return request; } function renderPersistentRideActions(request = selectedRequest()) { if (!els.rideActionPanel) return null; if (isPassengerNegotiationRequest(request) && !passengerRideDockMode(request)) { restoreRideToolElementsToChatPanel(); els.rideActionPanel.innerHTML = ""; return null; } const actionRequest = activeRideForRole(request); if (!actionRequest) resetRideDockPanels({ restore: false }); const focusedDestinationUpdateForm = passengerDestinationUpdateFocusedFormForRequest(actionRequest, els.rideActionPanel); if (focusedDestinationUpdateForm && canUpdateRideDestination(actionRequest)) { rememberPassengerDestinationUpdateDraft(focusedDestinationUpdateForm); return actionRequest; } const destinationUpdateFocus = passengerDestinationUpdateFocusedFieldSnapshot(els.rideActionPanel); restoreRideToolElementsToChatPanel(); els.rideActionPanel.innerHTML = ""; if (actionRequest && canSeeRideLifecycleActions(actionRequest)) { if (passengerRideDockMode(actionRequest)) { return renderPassengerRideDock(els.rideActionPanel, actionRequest, destinationUpdateFocus); } if (riderRideDockMode(actionRequest)) { return renderRiderRideDock(els.rideActionPanel, actionRequest); } renderRideLifecycleActions(els.rideActionPanel, actionRequest); renderRouteChangeRequestPanel(els.rideActionPanel, actionRequest); renderDestinationUpdateForm(els.rideActionPanel, actionRequest); renderRideTipForm(els.rideActionPanel, actionRequest); restorePassengerDestinationUpdateFocus(destinationUpdateFocus, els.rideActionPanel); return actionRequest; } if (activeRole() === "rider") { restorePassengerDestinationUpdateFocus(destinationUpdateFocus, els.rideActionPanel); return null; } const panel = document.createElement("div"); panel.className = "ride-guidance ride-action-panel"; const copy = document.createElement("div"); const title = document.createElement("strong"); const detail = document.createElement("span"); title.textContent = "Ride actions"; detail.textContent = activeRole() === "rider" ? "Cancel appears after a passenger chooses your offer. Complete appears once the ride is in progress." : "Ride updates and support actions appear here after you choose a rider."; copy.append(title, detail); panel.append(copy); els.rideActionPanel.append(panel); restorePassengerDestinationUpdateFocus(destinationUpdateFocus, els.rideActionPanel); return null; } function riderOfferFareTrail(offer) { const entries = Array.isArray(offer?.fareHistory) ? offer.fareHistory : []; const normalized = entries .map((entry) => ({ fare: Number(entry?.fare ?? entry?.amount ?? entry?.fare_xaf ?? entry), createdAt: entry?.createdAt ?? entry?.created_at ?? offer?.createdAt ?? null })) .filter((entry) => Number.isFinite(entry.fare) && entry.fare > 0) .sort((a, b) => new Date(a.createdAt ?? 0).getTime() - new Date(b.createdAt ?? 0).getTime()); if (!normalized.length && Number.isFinite(Number(offer?.fare))) { normalized.push({ fare: Number(offer.fare), createdAt: offer.createdAt ?? null }); } return normalized.reduce((trail, entry) => { const previous = trail[trail.length - 1]; if (!previous || Number(previous.fare) !== Number(entry.fare)) trail.push(entry); return trail; }, []); } function riderOfferFareTrailText(offer, request) { return riderOfferFareTrail(offer) .map((entry) => formatMoney(entry.fare, request?.country)) .join(" -> "); } function renderOffers() { const request = selectedWorkspaceRequest(); const visibleOffers = visibleOffersForRole(request); const isPassengerNegotiation = isPassengerNegotiationRequest(request); let selectedPassengerOffer = isPassengerNegotiation ? selectedPassengerNegotiationOffer(request, visibleOffers) : null; if (state.passengerSelectedOfferId && (!isPassengerNegotiation || !selectedPassengerOffer)) { state.passengerSelectedOfferId = null; selectedPassengerOffer = null; saveState(); } const offersToRender = selectedPassengerOffer ? [selectedPassengerOffer] : visibleOffers; const counterFocus = passengerOfferCounterFocusSnapshot(els.offerList); rememberPassengerFareDraftInputs(); els.offerList.innerHTML = ""; const hidePassengerClosedOffers = activeRole() === "passenger" && request && request.status !== "open"; const hidePassengerNonNegotiableOffers = activeRole() === "passenger" && isPassengerNonNegotiableWaitingRequest(request); const hidePassengerEmptyNegotiatedOffers = activeRole() === "passenger" && isPassengerNegotiationRequest(request) && !visibleOffers.length && !selectedPassengerOffer; const hideRiderListModeOffers = activeRole() === "rider" && riderWorkspacePage() === "requests" && !riderRequestDetailOpen(); const hideRiderActiveTripOffers = activeRole() === "rider" && request && requestIsActiveForCurrentRider(request); if (els.offersBoard) els.offersBoard.hidden = hidePassengerClosedOffers || hidePassengerNonNegotiableOffers || hidePassengerEmptyNegotiatedOffers || hideRiderListModeOffers || hideRiderActiveTripOffers; if (hidePassengerClosedOffers || hidePassengerNonNegotiableOffers || hidePassengerEmptyNegotiatedOffers || hideRiderListModeOffers || hideRiderActiveTripOffers) { els.offerCount.textContent = "0"; return; } if (!request || !roleCanSeeRequest(request)) { els.offerList.append(emptyState(activeRole() === "rider" ? "Select a nearby ride request to see or update your offer." : "Select one of your ride requests to see rider offers.")); } else if (activeRole() === "passenger" && request.status !== "open") { els.offerList.append(emptyState("This ride is matched or closed, so rider proposals are no longer shown.")); } else if (!visibleOffers.length) { els.offerList.append(emptyState(activeRole() === "rider" ? "You have not sent an offer for this request yet." : "No rider offers yet. Waka keeps checking automatically while this request is open.")); } const riderMap = stateLookupIndexes().riderMap; if (selectedPassengerOffer) { const rider = riderMap.get(selectedPassengerOffer.riderId); const header = document.createElement("div"); header.className = "passenger-offer-detail-heading"; const copy = document.createElement("div"); copy.innerHTML = ` Respond to ${escapeHtml(firstNameOnly(rider?.name, "rider"))} ${escapeHtml(formatMoney(selectedPassengerOffer.fare, request?.country))} offer selected. Accept, counter higher, reject, or return to all rider offers. `; const back = document.createElement("button"); back.type = "button"; back.className = "ghost-action"; back.textContent = `Back to all proposals (${visibleOffers.length})`; back.addEventListener("click", returnPassengerToOfferList); header.append(copy, back); els.offerList.append(header); } offersToRender.forEach((offer) => { const rider = riderMap.get(offer.riderId); const expiredOffer = offerIsExpired(offer, request); const node = els.offerTemplate.content.firstElementChild.cloneNode(true); node.classList.toggle("passenger-negotiation-offer", isPassengerNegotiation); node.classList.toggle("passenger-offer-detail-card", Boolean(selectedPassengerOffer)); const riderOfferTrail = riderOfferFareTrail(offer); const riderOfferTrailText = riderOfferFareTrailText(offer, request); node.querySelector(".card-kicker").textContent = activeRole() === "rider" ? "My fare offers" : isPassengerNegotiation ? "Rider offer" : offer.type === "accepted" ? "Accepted passenger fare" : "Counter-offer"; node.querySelector("strong").textContent = activeRole() === "rider" ? `You offered ${riderOfferTrailText || formatMoney(offer.fare, request?.country)}` : `${firstNameOnly(rider?.name, "Rider")} asks ${formatMoney(offer.fare)}`; const pickupEta = offerDistanceChip(offer, request); node.querySelector("small").textContent = activeRole() === "rider" ? `Latest ${formatMoney(offer.fare, request?.country)} - ${formatDate(offer.createdAt)}` : isPassengerNegotiation ? [rider?.vehicle ?? "Vehicle", pickupEta].filter(Boolean).join(" - ") : `${rider?.vehicle ?? "Vehicle"} - live location used for matching - rating ${rider?.rating ?? "new"}`; const offerNote = node.querySelector("p"); offerNote.textContent = offer.note || (isPassengerNegotiation ? "" : "No extra note."); offerNote.hidden = (activeRole() === "rider" && !offer.note) || (isPassengerNegotiation && !offer.note); const offerChips = activeRole() === "rider" ? [ offer.type === "accepted" ? "Accepted passenger fare" : "Counter-offer", `${riderOfferTrail.length} amount${riderOfferTrail.length === 1 ? "" : "s"}`, pickupEta, offerExpiryChip(offer, request) ] : isPassengerNegotiation ? [ offerStatusChip(offer, request), offerFareDeltaChip(offer, request), offerExpiryChip(offer, request) ] : [ offerStatusChip(offer, request), isSubscriptionActive(rider) ? "Access active" : "Not eligible", offerFareDeltaChip(offer, request), offerExpiryChip(offer, request), pickupEta, "Phone hidden", formatDate(offer.createdAt) ]; node.querySelector(".chip-row").innerHTML = offerChips.filter(Boolean).map(chip).join(""); const choose = node.querySelector(".choose-offer"); choose.hidden = activeRole() !== "passenger"; choose.disabled = activeRole() !== "passenger" || request.status !== "open" || !requestBelongsToPassenger(request) || expiredOffer; choose.textContent = expiredOffer ? "Offer expired" : selectedPassengerOffer ? "Accept rider fare" : isPassengerNegotiation ? "Respond" : "Accept this offer"; choose.addEventListener("click", () => { if (isPassengerNegotiation && !selectedPassengerOffer) { openPassengerOfferResponse(offer); return; } chooseOffer(offer.id); }); if (activeRole() === "passenger" && requestBelongsToPassenger(request) && request.status === "open" && (!isPassengerNegotiation || selectedPassengerOffer)) { const actions = document.createElement("div"); actions.className = "offer-negotiation-actions"; const counterForm = document.createElement("form"); counterForm.className = "offer-counter-form"; const counterFloorFare = Number(request.fareOffer ?? 0); const minimumNextFare = Math.floor(counterFloorFare) + 1; const draftKey = passengerOfferCounterDraftKey(offer, request); const draftValue = passengerOfferCounterDrafts.get(draftKey) ?? ""; const passengerProposalRemaining = typeof passengerFareProposalAttemptCount === "function" ? fareProposalAttemptsRemaining(passengerFareProposalAttemptCount(request)) : fareProposalAttemptLimit; const passengerProposalLimitReached = passengerProposalRemaining <= 0; counterForm.innerHTML = `

${passengerProposalLimitReached ? escapeHtml(fareProposalLimitMessage("passenger", request)) : `${isPassengerNegotiation ? `Whole dollars above ${escapeHtml(formatMoney(counterFloorFare, request.country))}.` : `Enter any whole-dollar fare higher than ${escapeHtml(formatMoney(counterFloorFare, request.country))}.`} ${passengerProposalRemaining} passenger proposal${passengerProposalRemaining === 1 ? "" : "s"} remaining.`}

`; const counterInput = counterForm.querySelector(".offer-counter-input"); counterInput?.addEventListener("focus", () => rememberPassengerOfferCounterFocus(counterInput)); counterInput?.addEventListener("pointerdown", () => rememberPassengerOfferCounterFocus(counterInput)); counterInput?.addEventListener("click", () => rememberPassengerOfferCounterFocus(counterInput)); counterInput?.addEventListener("input", () => { passengerOfferCounterDrafts.set(draftKey, counterInput.value); rememberPassengerOfferCounterFocus(counterInput); }); counterInput?.addEventListener("change", () => { passengerOfferCounterDrafts.set(draftKey, counterInput.value); rememberPassengerOfferCounterFocus(counterInput); }); counterForm.addEventListener("submit", (event) => passengerCounterRiderOffer(event, offer, request)); const reject = document.createElement("button"); reject.type = "button"; reject.className = "ghost-action danger"; reject.textContent = "Reject offer"; reject.addEventListener("click", () => rejectRiderOffer(offer)); actions.append(counterForm, reject); node.append(actions); } els.offerList.append(node); if (selectedPassengerOffer) { const backToProposals = document.createElement("button"); backToProposals.type = "button"; backToProposals.className = "secondary-action passenger-offer-detail-return"; backToProposals.textContent = `Back to all proposals (${visibleOffers.length})`; backToProposals.addEventListener("click", returnPassengerToOfferList); els.offerList.append(backToProposals); } }); els.offerCount.textContent = `${visibleOffers.length}`; restorePassengerOfferCounterFocus(counterFocus, els.offerList); } function renderOfferRequestContext(request) { if (!els.offerRequestContext) return; if (activeRole() !== "rider") { els.offerRequestContext.textContent = "Riders see selected request details here before sending a fare offer."; return; } if (!request || !roleCanSeeRequest(request)) { els.offerRequestContext.textContent = "Select a nearby request first."; return; } if (requestIsActiveForCurrentRider(request)) { const nextStop = request.status === "in_progress" ? `Destination: ${requestDestinationDisplayText(request)}` : `Pickup: ${riderActivePickupDisplayText(request)}`; els.offerRequestContext.textContent = `Matched to you. ${nextStop}.`; return; } const distance = proximityChip(request) ?? "Distance and pickup ETA not estimated"; const destinationDistance = destinationDriveChip(request) ?? "Trip distance to destination: estimating"; const stops = normalizeRideStops(request.rideStops); const stopsText = stops.length ? ` Planned stops: ${stops.length}.` : ""; const proposalText = requestIsNegotiableFare(request) && typeof riderFareProposalAttemptCount === "function" ? ` Rider proposals remaining: ${fareProposalAttemptsRemaining(riderFareProposalAttemptCount(request, currentRiderRecord()))}.` : ""; els.offerRequestContext.textContent = `${requestPickupTownText(request)} to ${requestDestinationDisplayText(request)}. ${destinationDistance}. ${fareModeChipText(request)}. Offer ${formatMoney(request.fareOffer)}. ${distance}.${stopsText}${proposalText}`; } function renderSelectedSummary() { if (activeRole() === "admin") { renderOfferRequestContext(null); els.selectedSummary.textContent = state.adminSession ? "View passengers, riders, approvals, subscriptions, and marketplace activity" : "Admin sign-in required for full passenger and rider visibility"; return; } const request = selectedWorkspaceRequest(); if (!request || !roleCanSeeRequest(request)) { renderOfferRequestContext(null); els.selectedSummary.textContent = activeRole() === "rider" ? `Marketplace: choose a nearby ${currentRiderRecord()?.vehicle ?? "vehicle"} request to review or decline` : "Publish or select one of your ride requests"; return; } const approach = riderApproachChip(request); const isPassengerMatchedRide = activeRole() === "passenger" && requestBelongsToPassenger(request) && passengerActiveRideRequestStatuses().includes(request.status) && selectedRiderIdForRequest(request); const pickupText = activeRole() === "rider" && !requestIsActiveForCurrentRider(request) ? requestPickupTownText(request) : requestPickupDisplayText(request); if (isPassengerNegotiationRequest(request)) { const offerCount = offersForRequest(request.id).length; const selectedOffer = selectedPassengerNegotiationOffer(request, offersForRequest(request.id)); const selectedRider = selectedOffer ? stateLookupIndexes().riderMap.get(selectedOffer.riderId) : null; els.selectedSummary.textContent = selectedOffer ? `Respond to ${firstNameOnly(selectedRider?.name, "rider")} - ${formatMoney(selectedOffer.fare, request.country)} offer` : offerCount ? `Choose rider offer - ${offerCount} offer${offerCount === 1 ? "" : "s"} available` : "Waiting for rider offers"; } else if (isPassengerMatchedRide) { els.selectedSummary.textContent = rideStatusLabel(request); } else if (isPassengerNonNegotiableWaitingRequest(request)) { els.selectedSummary.textContent = `Non-negotiable fare - offer ${formatMoney(request.fareOffer, request.country)} - waiting for a rider to accept`; } else if (activeRole() === "rider" && requestIsActiveForCurrentRider(request)) { els.selectedSummary.textContent = rideStatusLabel(request); } else { els.selectedSummary.textContent = `${pickupText} to ${requestDestinationDisplayText(request)} - ${fareModeChipText(request)} - offer ${formatMoney(request.fareOffer)} - ${scheduleChip(request)}${approach ? ` - ${approach}` : ""}`; } if (els.counterFare) { const counterFloorFare = Number(request.fareOffer ?? 0); const minimumCounterFare = Math.floor(counterFloorFare) + 1; els.counterFare.min = String(minimumCounterFare); els.counterFare.step = "1"; els.counterFare.placeholder = `Higher than ${formatMoney(counterFloorFare, request.country)}`; } renderOfferRequestContext(request); } function renderSafetyReportForm(request) { const canReport = canReportOnRequest(request); const disabledReason = !request ? "Select a ride before contacting Waka." : activeRole() === "rider" ? "Contact Waka opens after the passenger chooses your offer." : "Contact Waka opens after choosing a rider."; const target = canReport ? reportTargetForRequest(request) : null; const reporterRole = activeRole() === "rider" ? "Rider" : "Passenger"; els.safetyReportForm.hidden = !canReport; els.safetyReportCategory.disabled = !canReport; els.safetyReportSeverity.disabled = !canReport; els.safetyReportDetails.disabled = !canReport; els.safetyReportForm.querySelector("button").disabled = !canReport; els.safetyReportStatus.textContent = canReport ? `${reporterRole} report about ${target.name}. Admin reviews safety, no-show, route, payment, identity, and behavior concerns. Use this to contact Waka for support too.` : disabledReason; } function renderRideRatingForm(request) { if (!els.rideRatingForm) return; const target = ratingTargetForRequest(request); const canRate = canRateRequest(request); const existing = existingRatingForRequest(request); const ratingControls = [ els.rideRatingScore, els.rideRatingSafety, els.rideRatingPunctuality, els.rideRatingCommunication, els.rideRatingVehicle, els.rideRatingComment ].filter(Boolean); els.rideRatingForm.hidden = !canRate && !existing; ratingControls.forEach((control) => { control.disabled = !canRate; }); els.rideRatingForm.querySelector("button").disabled = !canRate; if (existing) { const setScore = (element, value) => { if (element) element.value = String(value ?? existing.score ?? 5); }; setScore(els.rideRatingScore, existing.score); setScore(els.rideRatingSafety, existing.safetyScore); setScore(els.rideRatingPunctuality, existing.punctualityScore); setScore(els.rideRatingCommunication, existing.communicationScore); setScore(els.rideRatingVehicle, existing.vehicleScore); if (els.rideRatingComment) els.rideRatingComment.value = existing.comment ?? ""; } if (!request) { els.rideRatingStatus.textContent = "Ratings open after selecting a completed ride."; } else if (existing) { els.rideRatingStatus.textContent = "Rating already submitted for this ride."; } else if (canRate) { els.rideRatingStatus.textContent = `Rating for ${target.name}. Completed ride: ${requestPickupDisplayText(request)} to ${requestDestinationDisplayText(request)}.`; } else if (request?.status === "completed" && requestBelongsToPassenger(request) && !target) { els.rideRatingStatus.textContent = "Waka is loading the matched rider details before this rating can be submitted."; } else { els.rideRatingStatus.textContent = "Ratings open after the ride is marked complete."; } } function chatDeliveryLabel(message) { if (message.sender !== activeRole() || message.sender === "system") return ""; if (message.deliveryStatus === "sending") return "Sending"; if (message.deliveryStatus === "failed") return "Failed to send"; return "Sent"; } function restoreRideToolElementsToChatPanel() { if (!els.chatPanel) return; [els.chatThread, els.chatForm, els.safetyReportForm, els.rideRatingForm] .filter(Boolean) .forEach((element) => els.chatPanel.append(element)); } function placeRideDockElements(activePanel, contentSelector) { restoreRideToolElementsToChatPanel(); if (!els.rideActionPanel) return; const content = els.rideActionPanel.querySelector(contentSelector); if (!content) return; const moveToContent = (element) => { if (element) { content.hidden = false; content.append(element); } }; if (activePanel === "contact") { moveToContent(els.chatThread); moveToContent(els.chatForm); } else if (activePanel === "support") { moveToContent(els.safetyReportForm); } else if (activePanel === "rating") { moveToContent(els.rideRatingForm); } } function placePassengerRideDockElements(activePanel) { placeRideDockElements(activePanel, "[data-passenger-ride-tool-content]"); } function placeRiderRideDockElements(activePanel) { placeRideDockElements(activePanel, "[data-rider-ride-tool-content]"); } function renderChat() { const request = selectedWorkspaceRequest(); const chatFocus = chatInputFocusSnapshot(); restoreRideToolElementsToChatPanel(); const isOpen = canChatOnRequest(request); const passengerDockMode = passengerRideDockMode(request); const passengerDockPanel = passengerDockMode ? passengerRideDockCurrentPanel(request) : null; const riderDockMode = riderRideDockMode(request); const riderDockPanel = riderDockMode ? riderRideDockCurrentPanel(request) : null; const rideDockMode = passengerDockMode || riderDockMode; const activeDockPanel = passengerDockMode ? passengerDockPanel : riderDockPanel; const chatHeading = els.chatPanel?.querySelector(".board-heading h2"); if (chatHeading) chatHeading.textContent = rideDockMode ? "Ride tools" : "Post-selection chat"; els.chatPanel?.classList.toggle("passenger-ride-tool-mode", rideDockMode); if (isPassengerNegotiationRequest(request) && !passengerDockMode) { if (chatHeading) chatHeading.textContent = "Post-selection chat"; els.chatPanel?.classList.remove("passenger-ride-tool-mode"); els.chatStatus.textContent = "Locked"; els.chatInput.disabled = true; els.chatForm.querySelector("button").disabled = true; els.chatInput.placeholder = "Chat opens after choosing a rider"; els.chatThread.innerHTML = ""; els.chatThread.hidden = false; els.chatForm.hidden = false; restoreRideToolElementsToChatPanel(); if (els.rideActionPanel) els.rideActionPanel.innerHTML = ""; renderSafetyReportForm(null); renderRideRatingForm(null); restoreRideToolElementsToChatPanel(); return; } els.chatStatus.textContent = rideDockMode ? rideStatusLabel(request) : isOpen ? "Open" : "Locked"; const showDockContact = !rideDockMode || activeDockPanel === "contact"; els.chatInput.disabled = !isOpen || !showDockContact; els.chatForm.querySelector("button").disabled = !isOpen || !showDockContact; els.chatInput.placeholder = isOpen ? "Write to the selected rider or passenger" : "Chat opens only after passenger chooses a rider"; els.chatThread.innerHTML = ""; els.chatThread.hidden = !showDockContact; els.chatForm.hidden = !showDockContact; const lifecycleRequest = renderPersistentRideActions(request); renderSafetyReportForm(reportableRideForRole(request) ?? lifecycleRequest ?? request); renderRideRatingForm(lifecycleRequest ?? request); if (passengerDockMode) placePassengerRideDockElements(passengerDockPanel); else if (riderDockMode) placeRiderRideDockElements(riderDockPanel); else restoreRideToolElementsToChatPanel(); if (passengerDockMode) { if (els.safetyReportForm) els.safetyReportForm.hidden = passengerDockPanel !== "support" || els.safetyReportForm.hidden; if (els.rideRatingForm) els.rideRatingForm.hidden = passengerDockPanel !== "rating" || els.rideRatingForm.hidden; if (!showDockContact) return; } if (riderDockMode) { if (els.safetyReportForm) els.safetyReportForm.hidden = riderDockPanel !== "support" || els.safetyReportForm.hidden; if (els.rideRatingForm) els.rideRatingForm.hidden = true; if (!showDockContact) return; } if (!request || !roleCanSeeRequest(request)) { els.chatThread.append(emptyState(activeRole() === "rider" ? "Select a nearby request first." : "Select one of your requests first.")); return; } const messages = state.chats.filter((message) => message.requestId === request.id); if (!isOpen) { els.chatThread.append(emptyState(activeRole() === "rider" ? "Chat opens only if the passenger chooses your offer." : "Chat is locked until you choose a rider offer.")); return; } if (typeof appendContactActions === "function") { appendContactActions(els.chatThread, request); } if (!messages.length) { els.chatThread.append(emptyState(`Chat is open. Confirm pickup details and ${paymentLabel(request.paymentPreference).toLowerCase()} before the ride starts.`)); restoreChatInputFocus(chatFocus, request, isOpen && showDockContact); return; } messages.forEach((message) => { const bubble = document.createElement("div"); const status = chatDeliveryLabel(message); bubble.className = `chat-bubble ${message.sender === activeRole() ? "self" : ""}${message.deliveryStatus === "failed" ? " failed" : ""}`; bubble.innerHTML = ` ${escapeHtml(message.text)} ${status ? `${escapeHtml(status)}` : ""} `; els.chatThread.append(bubble); }); restoreChatInputFocus(chatFocus, request, isOpen && showDockContact); } function offersForRequest(requestId) { return stateLookupIndexes().offersByRequestId.get(requestId) ?? []; } function selectRequest(id) { const previousRequestId = state.selectedRequestId; state.selectedRequestId = id; if (activeRole() === "passenger" && previousRequestId !== id) state.passengerSelectedOfferId = null; if (activeRole() === "rider") { clearRiderDecisionQueueForRequest(id); clearRequestMarketplaceFareChange(id); state.riderPage = "requests"; if (typeof updateRiderWorkspaceRoute === "function") updateRiderWorkspaceRoute("requests", { requestId: id }); } saveState(); renderAll(); if (activeRole() === "rider") { window.setTimeout(() => { document.querySelector("#riderRequestDetailPanel, #marketPanel") ?.scrollIntoView({ behavior: "smooth", block: "start" }); }, 0); } } function getCurrentGpsPoint() { if (!navigator.geolocation) { return Promise.reject(new Error("GPS is not available in this browser.")); } return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( (position) => { const point = gpsPointFromPosition(position); if (!point) { reject(new Error("GPS returned an invalid location.")); return; } resolve(point); }, () => reject(new Error("GPS permission was denied or the location could not be found.")), { enableHighAccuracy: true, timeout: 15000, maximumAge: 5000 } ); }); } function passengerPickupAutoReady() { return autoPickupGpsEnabled() && Boolean(els.pickupUseCurrentLocation?.checked) && activeRole() === "passenger" && Boolean(state.passenger) && hasSignedIn("passenger") && els.rideRequestForm && !els.rideRequestForm.hidden; } function passengerWantsCurrentPickup() { return Boolean(els.pickupUseCurrentLocation?.checked); } function pickupAddressLooksTooBroadForPublish(value) { const text = String(value ?? "").replace(/\s+/g, " ").trim(); if (!text || /\d/.test(text)) return false; if (/\b(road|rd|street|st|avenue|ave|drive|dr|lane|ln|boulevard|blvd|court|ct|circle|cir|highway|hwy|way|place|pl|terrace|ter|pike|parkway|pkwy|route|rte)\b/i.test(text)) { return false; } return /,/.test(text) && /\b(united states|usa|cameroon|nigeria|ghana|canada|united kingdom|uk|maryland|district of columbia|virginia|washington dc)\b/i.test(text); } function exactPickupAddressIssue(value = els.pickupDescription?.value) { const text = String(value ?? "").replace(/\s+/g, " ").trim(); if (pickupUsesGpsFallbackText(text)) { return "Confirm the exact pickup address before publishing. Use current location again so Waka can find the nearest street address, or type the full pickup address."; } if (!text || text === "Capturing current location..." || pickupUsesCurrentLocationText(text)) { return "Confirm the exact pickup address before publishing. Use current location again so Waka can find the nearest street address, or type the full pickup address."; } if (text.length < 10) { return "Enter the exact pickup address before publishing."; } if (/^(here|near me|my location|pickup location|same place)$/i.test(text)) { return "Enter the exact pickup address before publishing."; } if (pickupAddressLooksTooBroadForPublish(text)) { return "Enter the full street pickup address before publishing."; } return ""; } function waitForPassengerUiPaintBeforeAlert() { return new Promise((resolve) => { if (typeof window.requestAnimationFrame === "function") { window.requestAnimationFrame(() => window.requestAnimationFrame(resolve)); return; } window.setTimeout(resolve, 0); }); } async function capturePassengerPickupGps(options = {}) { const automatic = Boolean(options.automatic); if (automatic && !passengerPickupAutoReady()) return pendingPickupGps; if (passengerPickupGpsPromise) return passengerPickupGpsPromise; try { if (els.pickupGpsStatus) { els.pickupGpsStatus.textContent = automatic ? "Refreshing exact pickup location..." : "Capturing exact pickup location..."; } passengerPickupGpsPromise = getCurrentGpsPoint(); pendingPickupGps = await passengerPickupGpsPromise; const qualityIssue = pickupGpsQualityIssue(pendingPickupGps); if (els.pickupGpsStatus) { els.pickupGpsStatus.textContent = qualityIssue ? qualityIssue : passengerPickupGpsReadyLabel(pendingPickupGps); } updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(100); return pendingPickupGps; } catch (error) { pendingPickupGps = null; if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = error.message; updateFareGuidance(); return null; } finally { passengerPickupGpsPromise = null; } } async function ensurePassengerPickupGpsForPublish() { if (!passengerWantsCurrentPickup()) return null; if (!pendingPickupGps || pickupGpsQualityIssue(pendingPickupGps)) { await capturePassengerPickupGps({ automatic: false }); } return pendingPickupGps; } function clearPassengerPickupGps() { pendingPickupGps = null; selectedCurrentPickupGps = null; if (els.pickupUseCurrentLocation) els.pickupUseCurrentLocation.checked = false; if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = "Exact pickup location is off."; setPassengerRiderAvailabilityMessage("Enter a pickup address or check exact current location to see nearby rider availability."); updateFareGuidance(); } function stopAutomaticPassengerPickupGps() { if (passengerPickupGpsWatchId != null && navigator.geolocation?.clearWatch) { navigator.geolocation.clearWatch(passengerPickupGpsWatchId); } passengerPickupGpsWatchId = null; passengerPickupGpsWatchStartedAt = 0; window.clearTimeout(passengerPickupGpsWatchFallbackTimer); passengerPickupGpsWatchFallbackTimer = null; } function ensurePassengerPickupGpsAutoCapture() { if (!passengerPickupAutoReady()) { stopAutomaticPassengerPickupGps(); return; } if (!navigator.geolocation) { if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = "GPS is not available in this browser."; return; } if (passengerPickupGpsWatchId != null) { if (!pendingPickupGps && els.pickupGpsStatus && passengerPickupGpsWatchStartedAt && Date.now() - passengerPickupGpsWatchStartedAt > 17000) { els.pickupGpsStatus.textContent = "Exact pickup location has not returned yet. Allow Location or type the pickup address."; } return; } if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = "Capturing exact pickup location..."; passengerPickupGpsWatchStartedAt = Date.now(); window.clearTimeout(passengerPickupGpsWatchFallbackTimer); passengerPickupGpsWatchFallbackTimer = window.setTimeout(() => { if (!pendingPickupGps && els.pickupGpsStatus && passengerPickupGpsWatchId != null) { els.pickupGpsStatus.textContent = "Exact pickup location has not returned yet. Allow Location or type the pickup address."; } }, 17000); passengerPickupGpsWatchId = navigator.geolocation.watchPosition( (position) => { const point = gpsPointFromPosition(position); if (!point) return; pendingPickupGps = point; window.clearTimeout(passengerPickupGpsWatchFallbackTimer); passengerPickupGpsWatchFallbackTimer = null; const qualityIssue = pickupGpsQualityIssue(point); if (els.pickupGpsStatus) { els.pickupGpsStatus.textContent = qualityIssue ? qualityIssue : passengerPickupGpsReadyLabel(point); } updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(100); }, (error) => { const denied = Number(error?.code) === 1; if (els.pickupGpsStatus) { els.pickupGpsStatus.textContent = denied ? "GPS permission is blocked. Tap the browser location icon to allow it, or type the pickup address." : "Exact pickup location could not refresh. Type the pickup address or try again."; } stopAutomaticPassengerPickupGps(); }, { enableHighAccuracy: true, timeout: 15000, maximumAge: 5000 } ); if (!pendingPickupGps || pickupGpsQualityIssue(pendingPickupGps)) { void capturePassengerPickupGps({ automatic: true }); } } function destinationAutocompleteReady() { return placesAutocompleteEnabled() && hasSupabaseRuntime() && Boolean(state.passenger) && hasSignedIn("passenger"); } function pickupAutocompleteReady() { return destinationAutocompleteReady(); } function addressSearchLimitsRelaxedForTesting() { return configFlagEnabled(appConfig.relaxAddressSearchLimitsForTesting) && /\b(staging|pilot|test|preview)\b/i.test(String(appConfig.projectName || "")); } function addressSearchRateLimitPauseLimitMs() { return addressSearchLimitsRelaxedForTesting() ? testingAddressSearchRateLimitPauseMs : addressSearchRateLimitPauseMs; } function pickupSessionToken() { if (!pickupAutocompleteSessionToken) { pickupAutocompleteSessionToken = makeId("place-session"); } return pickupAutocompleteSessionToken; } function destinationSessionToken() { if (!destinationAutocompleteSessionToken) { destinationAutocompleteSessionToken = makeId("place-session"); } return destinationAutocompleteSessionToken; } function rideStopSessionToken() { if (!rideStopAutocompleteSessionToken) { rideStopAutocompleteSessionToken = makeId("place-session"); } return rideStopAutocompleteSessionToken; } function clearPickupPlaceSelection(message = "") { selectedPickupPlace = null; if (!pickupUsesCurrentLocationText(els.pickupDescription?.value) && !pickupUsesGpsFallbackText(els.pickupDescription?.value)) selectedCurrentPickupGps = null; if (els.pickupPlaceStatus && message) els.pickupPlaceStatus.textContent = message; } function clearDestinationPlaceSelection(message = "") { selectedDestinationPlace = null; if (els.destinationPlaceStatus && message) els.destinationPlaceStatus.textContent = message; } function passengerFacingPlaceErrorMessage(error, fallback) { const message = String(error?.message || "").trim(); if (!message) return fallback; if (/edge function returned a non-2xx status code/i.test(message)) return fallback; if (/api is not activated|enable this api|google cloud console|gmp-get-started|geocoding api|google reverse geocoding|google maps server key/i.test(message)) { return "Pickup street-address lookup is not enabled for Waka's map service yet. Type the full pickup address for now, or try current location again after Google Geocoding is enabled."; } return message .replace(/destination searches/gi, "address searches") .replace(/destination search/gi, "address search"); } function hideDestinationSuggestions() { if (!els.destinationSuggestions) return; els.destinationSuggestions.hidden = true; els.destinationSuggestions.replaceChildren(); } function hideRideStopSuggestions() { hideInlinePlaceSuggestions(els.rideStopSuggestions); } function hidePickupSuggestions() { if (!els.pickupSuggestions) return; els.pickupSuggestions.hidden = true; els.pickupSuggestions.replaceChildren(); } function recentAddressStorageKey(type) { const passengerId = state.passenger?.id || state.sessions.passenger?.userId || state.sessions.passenger?.email || "guest"; return `${storageKey}:recent-${type}:${passengerId}`; } function recentAddressStorageDisabled() { return hasSupabaseRuntime() || strictProductionModeEnabled(); } function clearRecentAddressHistory() { try { Object.keys(localStorage) .filter((key) => key.startsWith(`${storageKey}:recent-`)) .forEach((key) => localStorage.removeItem(key)); } catch { // Address history is a convenience only; production privacy wins. } } function readRecentAddresses(type) { if (recentAddressStorageDisabled()) { clearRecentAddressHistory(); return []; } try { const value = JSON.parse(localStorage.getItem(recentAddressStorageKey(type))); if (!Array.isArray(value)) return []; const items = value .filter((item) => item?.address) .filter((item) => !pickupUsesGpsFallbackText(item.address) && !pickupUsesGpsFallbackText(item.displayName)) .slice(0, 8); if (items.length !== value.length) saveRecentAddresses(type, items); return items; } catch { return []; } } function saveRecentAddresses(type, addresses = []) { if (recentAddressStorageDisabled()) { clearRecentAddressHistory(); return; } try { localStorage.setItem(recentAddressStorageKey(type), JSON.stringify(addresses.slice(0, 8))); } catch { // Address history is a convenience only; ride publishing must not depend on storage. } } function rememberRecentAddress(type, place) { if (recentAddressStorageDisabled()) return; const address = String(place?.formattedAddress || place?.address || "").trim(); if (!address || pickupUsesCurrentLocationText(address) || pickupUsesGpsFallbackText(address)) return; const entry = { address, displayName: String(place?.displayName || address).trim(), placeId: place?.placeId ?? null, latitude: Number.isFinite(Number(place?.latitude)) ? Number(place.latitude) : null, longitude: Number.isFinite(Number(place?.longitude)) ? Number(place.longitude) : null, savedAt: new Date().toISOString() }; const existing = readRecentAddresses(type) .filter((item) => String(item.address).trim().toLowerCase() !== address.toLowerCase()); saveRecentAddresses(type, [entry, ...existing]); } function rememberRideRouteAddresses(request) { if (request?.pickupDescription && !pickupUsesGpsFallbackText(request.pickupDescription)) { rememberRecentAddress("pickup", { address: request.pickupDescription, displayName: request.pickupDescription, latitude: request.pickupLatitude, longitude: request.pickupLongitude }); } if (request?.destination) { rememberRecentAddress("destination", { address: request.destinationFormattedAddress || request.destination, displayName: request.destination, placeId: request.destinationPlaceId, latitude: request.destinationLatitude, longitude: request.destinationLongitude }); } } function normalizeRecentAddressText(value) { return String(value ?? "").replace(/\s+/g, " ").trim().toLowerCase(); } function recentAddressStillSelected(type, item) { const value = type === "pickup" ? els.pickupDescription?.value : els.destination?.value; const normalizedValue = normalizeRecentAddressText(value); return Boolean(normalizedValue) && [item?.address, item?.displayName].some((label) => normalizeRecentAddressText(label) === normalizedValue); } function bestRecentAddressSuggestion(suggestions = [], item) { const normalizedAddress = normalizeRecentAddressText(item?.address); const normalizedDisplay = normalizeRecentAddressText(item?.displayName); return suggestions.find((suggestion) => { const text = normalizeRecentAddressText(suggestion?.text); const main = normalizeRecentAddressText(suggestion?.mainText); return text === normalizedAddress || text === normalizedDisplay || (normalizedAddress && text.includes(normalizedAddress)) || (normalizedDisplay && text.includes(normalizedDisplay)) || (normalizedDisplay && main === normalizedDisplay); }) ?? suggestions[0] ?? null; } async function confirmRecentAddressPlace(type, item) { if (!destinationAutocompleteReady()) return null; const input = String(item?.address || item?.displayName || "").trim(); if (input.length < 3) return null; const country = state.passenger?.country ?? selectedPassengerCountry(); const city = state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(country); const payload = await fetchPlaceAutocomplete({ action: "autocomplete", input, sessionToken: type === "pickup" ? pickupSessionToken() : destinationSessionToken(), country, city }); const suggestion = bestRecentAddressSuggestion(payload?.suggestions ?? [], item); if (!suggestion?.placeId) return null; const details = type === "pickup" ? await fetchPickupPlaceDetails(suggestion) : await fetchDestinationPlaceDetails(suggestion); const place = normalizedPlaceSelection({ ...details?.place, displayName: details?.place?.displayName || suggestion.mainText || suggestion.text }); return place?.placeId ? place : null; } function trackRecentAddressConfirmation(promise) { if (!promise?.finally) return promise; recentAddressConfirmationPromises.add(promise); promise.finally(() => recentAddressConfirmationPromises.delete(promise)); return promise; } async function waitForRecentAddressConfirmations() { const pending = [...recentAddressConfirmationPromises]; if (!pending.length) return; if (els.fareGuidance) els.fareGuidance.textContent = "Finishing recent address confirmation..."; await Promise.allSettled(pending); } async function applyRecentAddress(type, item) { const place = normalizedPlaceSelection({ placeId: item.placeId ?? null, displayName: item.displayName || item.address, formattedAddress: item.address, latitude: item.latitude, longitude: item.longitude }); clearLowFareReview(); if (type === "pickup") { if (els.pickupUseCurrentLocation) els.pickupUseCurrentLocation.checked = false; selectedCurrentPickupGps = null; selectedPickupPlace = place; els.pickupDescription.value = item.address; hidePickupSuggestions(); if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = `Confirming recent pickup: ${item.displayName || item.address}`; } else { selectedDestinationPlace = place; els.destination.value = item.address; hideDestinationSuggestions(); if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = `Confirming recent destination: ${item.displayName || item.address}`; } renderRecentAddressShortcuts(); if (els.fareGuidance) els.fareGuidance.textContent = "Confirming recent address for fare estimate..."; try { const confirmedPlace = place?.placeId ? place : await confirmRecentAddressPlace(type, item); if (confirmedPlace && recentAddressStillSelected(type, item)) { rememberRecentAddress(type, confirmedPlace); if (type === "pickup") { selectedPickupPlace = confirmedPlace; els.pickupDescription.value = confirmedPlace.formattedAddress || confirmedPlace.displayName || item.address; if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = `Selected recent pickup: ${confirmedPlace.displayName || confirmedPlace.formattedAddress}`; } else { selectedDestinationPlace = confirmedPlace; els.destination.value = confirmedPlace.formattedAddress || confirmedPlace.displayName || item.address; if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = `Selected recent destination: ${confirmedPlace.displayName || confirmedPlace.formattedAddress}`; } } else if (type === "pickup") { if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = `Selected recent pickup: ${item.displayName || item.address}`; } else if (els.destinationPlaceStatus) { els.destinationPlaceStatus.textContent = `Selected recent destination: ${item.displayName || item.address}`; } } catch (error) { if (type === "pickup" && els.pickupPlaceStatus) { els.pickupPlaceStatus.textContent = passengerFacingPlaceErrorMessage( error, "Selected recent pickup. Fare will use the saved address text." ); } else if (type === "destination" && els.destinationPlaceStatus) { els.destinationPlaceStatus.textContent = passengerFacingPlaceErrorMessage( error, "Selected recent destination. Fare will use the saved address text." ); } } updateFareGuidance(); if (type === "pickup") schedulePassengerNearbyRiderCountsRefresh(100); } function renderRecentAddressList(container, type, currentValue) { if (!container) return; container.replaceChildren(); const value = String(currentValue || "").trim(); const items = readRecentAddresses(type) .filter((item) => item?.address && item.address !== value) .slice(0, 4); if (!state.passenger || !hasSignedIn("passenger") || value || !items.length) { container.hidden = true; return; } for (const item of items) { const button = document.createElement("button"); button.type = "button"; button.className = "recent-address-button"; button.textContent = item.displayName || item.address; button.title = item.address; button.addEventListener("click", () => trackRecentAddressConfirmation(applyRecentAddress(type, item))); container.append(button); } container.hidden = false; } function renderRecentAddressShortcuts() { clearStaleGpsPickupFallbackText({ status: false }); renderRecentAddressList(els.pickupHistory, "pickup", els.pickupDescription?.value); renderRecentAddressList(els.destinationHistory, "destination", els.destination?.value); } function clearStaleGpsPickupFallbackText({ status = true } = {}) { if (!els.pickupDescription || !pickupUsesGpsFallbackText(els.pickupDescription.value)) return false; els.pickupDescription.value = ""; selectedPickupPlace = null; hidePickupSuggestions(); if (status && els.pickupPlaceStatus) { els.pickupPlaceStatus.textContent = "Waka cleared an old GPS-coordinate pickup label. Use current location again to find the street address, or type the full pickup address."; } updateFareGuidance(); return true; } async function fetchPlaceAutocomplete(body) { if (!destinationAutocompleteReady()) throw new Error("Destination autocomplete needs passenger sign-in and Supabase."); const action = String(body?.action || "").toLowerCase(); const cacheKey = action === "autocomplete" ? JSON.stringify({ action, input: String(body.input || "").trim().toLowerCase(), country: String(body.country || "").trim().toLowerCase(), city: String(body.city || "").trim().toLowerCase() }) : ""; if (cacheKey && placeAutocompleteCache.has(cacheKey)) { return placeAutocompleteCache.get(cacheKey); } const localPauseMs = addressSearchRateLimitPauseLimitMs(); if (action === "autocomplete" && localPauseMs > 0 && Date.now() < placesAutocompleteRateLimitedUntil) { throw new Error("Address search is temporarily paused for this account. Enter the full address manually."); } const functionName = placesAutocompleteFunctionName(); const token = await currentSupabaseAccessToken(); if (!token) throw new Error("Passenger sign-in is required for destination suggestions."); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${functionName}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify(body) }), "Fetching destination suggestions", optionalSupabaseRequestTimeoutMs ); const payload = await response.json().catch(() => null); if (!response.ok) { const message = payload?.error || "Address autocomplete failed."; if (/too many|limit reached|rate limit/i.test(message)) { placesAutocompleteRateLimitedUntil = localPauseMs > 0 ? Date.now() + localPauseMs : 0; } throw new Error(message); } if (cacheKey) { placeAutocompleteCache.set(cacheKey, payload); while (placeAutocompleteCache.size > 80) { const oldestKey = placeAutocompleteCache.keys().next().value; if (!oldestKey) break; placeAutocompleteCache.delete(oldestKey); } } return payload; } function destinationPlaceDetailsCacheKey(placeId) { return String(placeId ?? "").trim(); } function rememberDestinationPlaceDetails(placeId, payload) { const key = destinationPlaceDetailsCacheKey(placeId); if (!key || !payload) return; if (destinationPlaceDetailsCache.has(key)) destinationPlaceDetailsCache.delete(key); destinationPlaceDetailsCache.set(key, payload); while (destinationPlaceDetailsCache.size > placeDetailsCacheLimit) { const oldestKey = destinationPlaceDetailsCache.keys().next().value; if (!oldestKey) break; destinationPlaceDetailsCache.delete(oldestKey); } } async function fetchDestinationPlaceDetails(suggestion) { const placeId = destinationPlaceDetailsCacheKey(suggestion?.placeId); if (!placeId) throw new Error("Selected destination did not include a place id."); const cached = destinationPlaceDetailsCache.get(placeId); if (cached) return cached; const payload = await fetchPlaceAutocomplete({ action: "details", placeId, sessionToken: destinationSessionToken() }); rememberDestinationPlaceDetails(placeId, payload); return payload; } async function fetchPickupPlaceDetails(suggestion) { const placeId = destinationPlaceDetailsCacheKey(suggestion?.placeId); if (!placeId) throw new Error("Selected pickup did not include a place id."); const cached = destinationPlaceDetailsCache.get(placeId); if (cached) return cached; const payload = await fetchPlaceAutocomplete({ action: "details", placeId, sessionToken: pickupSessionToken() }); rememberDestinationPlaceDetails(placeId, payload); return payload; } async function fetchPickupReverseGeocode(point) { const gps = normalizeGpsPoint(point); if (!gps) throw new Error("A valid GPS location is required."); return fetchPlaceAutocomplete({ action: "reverse-geocode", latitude: gps.latitude, longitude: gps.longitude, country: state.passenger?.country ?? selectedPassengerCountry(), city: state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(selectedPassengerCountry()) }); } function renderPickupSuggestions(suggestions = []) { if (!els.pickupSuggestions) return; els.pickupSuggestions.replaceChildren(); const cleanSuggestions = suggestions.filter((suggestion) => suggestion?.placeId && suggestion?.text); if (!cleanSuggestions.length) { els.pickupSuggestions.hidden = true; return; } for (const suggestion of cleanSuggestions) { const button = document.createElement("button"); button.type = "button"; button.className = "place-suggestion"; button.setAttribute("role", "option"); const main = document.createElement("span"); main.className = "place-suggestion-main"; main.textContent = suggestion.mainText || suggestion.text; const secondary = document.createElement("span"); secondary.className = "place-suggestion-secondary"; secondary.textContent = suggestion.secondaryText || ""; button.append(main, secondary); wirePlaceSuggestionButton(button, () => selectPickupSuggestion(suggestion)); els.pickupSuggestions.append(button); } els.pickupSuggestions.hidden = false; } function renderDestinationSuggestions(suggestions = []) { if (!els.destinationSuggestions) return; els.destinationSuggestions.replaceChildren(); const cleanSuggestions = suggestions.filter((suggestion) => suggestion?.placeId && suggestion?.text); if (!cleanSuggestions.length) { els.destinationSuggestions.hidden = true; return; } for (const suggestion of cleanSuggestions) { const button = document.createElement("button"); button.type = "button"; button.className = "place-suggestion"; button.setAttribute("role", "option"); const main = document.createElement("span"); main.className = "place-suggestion-main"; main.textContent = suggestion.mainText || suggestion.text; const secondary = document.createElement("span"); secondary.className = "place-suggestion-secondary"; secondary.textContent = suggestion.secondaryText || ""; button.append(main, secondary); wirePlaceSuggestionButton(button, () => selectDestinationSuggestion(suggestion)); els.destinationSuggestions.append(button); } els.destinationSuggestions.hidden = false; } function wirePlaceSuggestionButton(button, handler) { let handled = false; const choose = (event) => { event.preventDefault(); event.stopPropagation(); if (handled) return; handled = true; handler(); }; button.addEventListener("pointerdown", choose); button.addEventListener("mousedown", choose); button.addEventListener("click", choose); } function hideInlinePlaceSuggestions(container) { if (!container) return; container.hidden = true; container.replaceChildren(); } function renderInlinePlaceSuggestions(container, suggestions = [], onSelect) { if (!container) return; container.replaceChildren(); const cleanSuggestions = suggestions.filter((suggestion) => suggestion?.placeId && suggestion?.text); if (!cleanSuggestions.length) { container.hidden = true; return; } for (const suggestion of cleanSuggestions) { const button = document.createElement("button"); button.type = "button"; button.className = "place-suggestion"; button.setAttribute("role", "option"); const main = document.createElement("span"); main.className = "place-suggestion-main"; main.textContent = suggestion.mainText || suggestion.text; const secondary = document.createElement("span"); secondary.className = "place-suggestion-secondary"; secondary.textContent = suggestion.secondaryText || ""; button.append(main, secondary); wirePlaceSuggestionButton(button, () => onSelect(suggestion)); container.append(button); } container.hidden = false; } function destinationUpdateStatus(form, message) { const status = form.querySelector(".destination-update-status"); if (status && message) status.textContent = message; } function activeStopLineInfo(textarea) { const value = textarea?.value ?? ""; const cursor = textarea?.selectionStart ?? value.length; const lineStart = value.lastIndexOf("\n", Math.max(0, cursor - 1)) + 1; const nextBreak = value.indexOf("\n", cursor); const lineEnd = nextBreak === -1 ? value.length : nextBreak; return { lineStart, lineEnd, text: value.slice(lineStart, lineEnd).trim() }; } function replaceActiveStopLine(textarea, nextValue) { if (!textarea) return; const { lineStart, lineEnd } = activeStopLineInfo(textarea); const replacement = String(nextValue ?? "").trim(); textarea.value = `${textarea.value.slice(0, lineStart)}${replacement}${textarea.value.slice(lineEnd)}`; const cursor = lineStart + replacement.length; textarea.setSelectionRange(cursor, cursor); } function rideStopsStatusText(stops = rideStopsFormValue()) { const normalized = normalizeRideStops(stops); if (!normalized.length) { return rideStopsInputEnabled() ? "Type one stop address per line. Waka will route stops in the order shown." : "Stops are optional. Use + Add stop only when the ride needs stops before the final destination."; } if (normalized.length >= rideStopsMaxCount) { return `${normalized.length} stops added. Maximum reached; Waka will price the route in this stop order.`; } const remaining = rideStopsMaxCount - normalized.length; return `${normalized.length} stop${normalized.length === 1 ? "" : "s"} added. Press Enter to add the next stop; ${remaining} more allowed.`; } function setRideStopsInputEnabled(enabled, { focus = false } = {}) { if (!els.rideStops) return; const nextEnabled = Boolean(enabled); els.rideStops.dataset.enabled = nextEnabled ? "true" : "false"; els.rideStops.disabled = !nextEnabled; setRideRequestOptionPanel("stops", nextEnabled); if (els.rideStopsField) els.rideStopsField.hidden = !nextEnabled; if (els.clearRideStops) els.clearRideStops.hidden = !nextEnabled; if (els.addRideStop) els.addRideStop.setAttribute("aria-expanded", String(nextEnabled)); if (!nextEnabled) { els.rideStops.value = ""; hideRideStopSuggestions(); } if (els.rideStopsStatus) els.rideStopsStatus.textContent = rideStopsStatusText(); if (focus && nextEnabled) { window.setTimeout(() => { els.rideStops.focus(); const end = els.rideStops.value.length; els.rideStops.setSelectionRange(end, end); }, 0); } } function initializeRideStopsInput() { setRideStopsInputEnabled(Boolean(normalizeRideStops(els.rideStops?.value).length)); } function handleAddRideStop(event) { event?.preventDefault?.(); if (!rideStopsInputEnabled()) { setRideStopsInputEnabled(true, { focus: true }); return; } const stops = normalizeRideStops(els.rideStops.value); if (stops.length >= rideStopsMaxCount) { if (els.rideStopsStatus) els.rideStopsStatus.textContent = rideStopsStatusText(); els.rideStops.focus(); return; } if (els.rideStops.value.trim() && !els.rideStops.value.endsWith("\n")) { els.rideStops.value = `${els.rideStops.value.trimEnd()}\n`; } if (els.rideStopsStatus) els.rideStopsStatus.textContent = "Type the next stop address. Stops stay in this line-by-line order."; els.rideStops.focus(); const end = els.rideStops.value.length; els.rideStops.setSelectionRange(end, end); } function clearRideStopsInput(event) { event?.preventDefault?.(); setRideStopsInputEnabled(false); clearLowFareReview(); updateFareGuidance(); } function handleRideStopsInput() { if (!rideStopsInputEnabled() && normalizeRideStops(els.rideStops?.value).length) { setRideStopsInputEnabled(true); } clearLowFareReview(); if (els.rideStopsStatus) els.rideStopsStatus.textContent = rideStopsStatusText(); updateFareGuidance(); scheduleRideStopAutocomplete(); } async function choosePrePublishStopSuggestion(suggestion) { try { if (els.rideStopsStatus) els.rideStopsStatus.textContent = "Confirming stop address..."; const payload = await fetchDestinationPlaceDetails(suggestion); const place = normalizedPlaceSelection({ ...payload?.place, displayName: payload?.place?.displayName || suggestion.mainText || suggestion.text }); if (!place?.placeId) throw new Error("Selected stop did not return a place id."); replaceActiveStopLine(els.rideStops, place.formattedAddress || place.displayName || suggestion.text); rememberSelectedStopPlace(place); rememberRecentAddress("destination", place); hideRideStopSuggestions(); rideStopAutocompleteSessionToken = null; if (els.rideStopsStatus) els.rideStopsStatus.textContent = `Selected stop: ${place.displayName || place.formattedAddress}. ${rideStopsStatusText()}`; renderRecentAddressShortcuts(); clearLowFareReview(); updateFareGuidance(); } catch (error) { if (els.rideStopsStatus) { els.rideStopsStatus.textContent = passengerFacingPlaceErrorMessage( error, "Could not confirm that stop suggestion. Enter the full stop address." ); } } } function scheduleRideStopAutocomplete() { window.clearTimeout(rideStopAutocompleteTimer); if (!els.rideStops) return; if (!rideStopsInputEnabled()) { hideRideStopSuggestions(); if (els.rideStopsStatus) els.rideStopsStatus.textContent = rideStopsStatusText(); return; } if (!destinationAutocompleteReady()) { hideRideStopSuggestions(); if (els.rideStopsStatus && placesAutocompleteEnabled()) { els.rideStopsStatus.textContent = "Sign in as a passenger to use stop suggestions."; } return; } const input = activeStopLineInfo(els.rideStops).text; if (input.length < 3) { hideRideStopSuggestions(); if (els.rideStopsStatus) els.rideStopsStatus.textContent = rideStopsStatusText(); return; } const requestId = ++rideStopAutocompleteRequestId; rideStopAutocompleteTimer = window.setTimeout(async () => { try { if (els.rideStopsStatus) els.rideStopsStatus.textContent = "Searching stop addresses..."; const payload = await fetchPlaceAutocomplete({ action: "autocomplete", input, sessionToken: rideStopSessionToken(), country: state.passenger?.country ?? selectedPassengerCountry(), city: state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(selectedPassengerCountry()) }); if (requestId !== rideStopAutocompleteRequestId) return; renderInlinePlaceSuggestions(els.rideStopSuggestions, payload?.suggestions ?? [], choosePrePublishStopSuggestion); if (els.rideStopsStatus) { els.rideStopsStatus.textContent = (payload?.suggestions ?? []).length ? "Choose the matching stop from the suggestions." : "No stop suggestion found; continue with the full address."; } } catch (error) { hideRideStopSuggestions(); if (els.rideStopsStatus) { els.rideStopsStatus.textContent = passengerFacingPlaceErrorMessage( error, "Stop suggestions are unavailable right now. Enter the full stop address." ); } } }, 350); } function setupDestinationUpdateAutocomplete(form, request) { const destinationInput = form.querySelector(".destination-update-input"); const destinationSuggestions = form.querySelector(".destination-update-suggestions"); const stopsInput = form.querySelector(".stops-update-input"); const stopSuggestions = form.querySelector(".stops-update-suggestions"); const country = request?.country ?? state.passenger?.country ?? selectedPassengerCountry(); const city = request?.city ?? state.passenger?.city ?? defaultLaunchCity(country); let destinationTimer = null; let destinationRequestId = 0; let stopTimer = null; let stopRequestId = 0; const draft = passengerDestinationUpdateDraftForRequest(request); const currentDestinationPlace = normalizedPlaceSelection({ placeId: request?.destinationPlaceId, displayName: request?.destination, formattedAddress: request?.destinationFormattedAddress || request?.destination, latitude: request?.destinationLatitude, longitude: request?.destinationLongitude }); form.__destinationUpdatePlace = destinationPlaceMatchesInput(draft.destinationPlace, destinationInput.value) ? draft.destinationPlace : destinationPlaceMatchesInput(currentDestinationPlace, destinationInput.value) ? currentDestinationPlace : null; const chooseDestination = async (suggestion) => { try { destinationUpdateStatus(form, "Confirming destination address..."); const payload = await fetchDestinationPlaceDetails(suggestion); const place = normalizedPlaceSelection({ ...payload?.place, displayName: payload?.place?.displayName || suggestion.mainText || suggestion.text }); if (!place?.placeId) throw new Error("Selected destination did not return a place id."); form.__destinationUpdatePlace = place; destinationInput.value = place.formattedAddress || place.displayName || suggestion.text; markPassengerDestinationUpdateEditing(form); rememberRecentAddress("destination", place); hideInlinePlaceSuggestions(destinationSuggestions); destinationAutocompleteSessionToken = null; destinationUpdateStatus(form, `Selected destination: ${place.displayName || place.formattedAddress}`); } catch (error) { destinationUpdateStatus(form, passengerFacingPlaceErrorMessage( error, "Could not confirm that address suggestion. Enter the full destination address." )); } }; const chooseStop = async (suggestion) => { try { destinationUpdateStatus(form, "Confirming stop address..."); const payload = await fetchDestinationPlaceDetails(suggestion); const place = normalizedPlaceSelection({ ...payload?.place, displayName: payload?.place?.displayName || suggestion.mainText || suggestion.text }); if (!place?.placeId) throw new Error("Selected stop did not return a place id."); replaceActiveStopLine(stopsInput, place.formattedAddress || place.displayName || suggestion.text); rememberSelectedStopPlace(place); markPassengerDestinationUpdateEditing(form); rememberRecentAddress("destination", place); hideInlinePlaceSuggestions(stopSuggestions); destinationAutocompleteSessionToken = null; destinationUpdateStatus(form, `Selected stop: ${place.displayName || place.formattedAddress}`); } catch (error) { destinationUpdateStatus(form, passengerFacingPlaceErrorMessage( error, "Could not confirm that stop suggestion. Enter the full stop address." )); } }; const scheduleDestinationSearch = () => { window.clearTimeout(destinationTimer); if (!destinationPlaceMatchesInput(form.__destinationUpdatePlace, destinationInput.value)) { form.__destinationUpdatePlace = null; } markPassengerDestinationUpdateEditing(form); if (!destinationAutocompleteReady()) { hideInlinePlaceSuggestions(destinationSuggestions); return; } const input = destinationInput.value.trim(); if (input.length < 3) { hideInlinePlaceSuggestions(destinationSuggestions); return; } const requestId = ++destinationRequestId; destinationTimer = window.setTimeout(async () => { try { destinationUpdateStatus(form, "Searching destination addresses..."); const payload = await fetchPlaceAutocomplete({ action: "autocomplete", input, sessionToken: destinationSessionToken(), country, city }); if (requestId !== destinationRequestId || !form.isConnected) return; renderInlinePlaceSuggestions(destinationSuggestions, payload?.suggestions ?? [], chooseDestination); destinationUpdateStatus(form, (payload?.suggestions ?? []).length ? "Choose the matching destination from the suggestions." : "No destination suggestion found; continue with the full address."); } catch (error) { hideInlinePlaceSuggestions(destinationSuggestions); destinationUpdateStatus(form, passengerFacingPlaceErrorMessage( error, "Address suggestions are unavailable right now. Enter the full destination address." )); } }, 350); }; const scheduleStopSearch = () => { window.clearTimeout(stopTimer); markPassengerDestinationUpdateEditing(form); if (!destinationAutocompleteReady() || stopsInput.closest(".destination-stop-field")?.hidden) { hideInlinePlaceSuggestions(stopSuggestions); return; } const input = activeStopLineInfo(stopsInput).text; if (input.length < 3) { hideInlinePlaceSuggestions(stopSuggestions); return; } const requestId = ++stopRequestId; stopTimer = window.setTimeout(async () => { try { destinationUpdateStatus(form, "Searching stop addresses..."); const payload = await fetchPlaceAutocomplete({ action: "autocomplete", input, sessionToken: destinationSessionToken(), country, city }); if (requestId !== stopRequestId || !form.isConnected) return; renderInlinePlaceSuggestions(stopSuggestions, payload?.suggestions ?? [], chooseStop); destinationUpdateStatus(form, (payload?.suggestions ?? []).length ? "Choose the matching stop from the suggestions." : "No stop suggestion found; continue with the full address."); } catch (error) { hideInlinePlaceSuggestions(stopSuggestions); destinationUpdateStatus(form, passengerFacingPlaceErrorMessage( error, "Stop suggestions are unavailable right now. Enter the full stop address." )); } }, 350); }; destinationInput.addEventListener("input", scheduleDestinationSearch); destinationInput.addEventListener("focus", scheduleDestinationSearch); destinationInput.addEventListener("blur", () => window.setTimeout(() => { delete form.dataset.routeChangeEditingAt; hideInlinePlaceSuggestions(destinationSuggestions); }, 150)); stopsInput.addEventListener("input", scheduleStopSearch); stopsInput.addEventListener("focus", scheduleStopSearch); stopsInput.addEventListener("click", scheduleStopSearch); stopsInput.addEventListener("keyup", scheduleStopSearch); stopsInput.addEventListener("blur", () => window.setTimeout(() => { delete form.dataset.routeChangeEditingAt; hideInlinePlaceSuggestions(stopSuggestions); }, 150)); } function handlePickupInput() { clearLowFareReview(); if (clearStaleGpsPickupFallbackText()) return; if (els.pickupUseCurrentLocation?.checked && !pickupUsesCurrentLocationText(els.pickupDescription.value) && !pickupUsesGpsFallbackText(els.pickupDescription.value)) { els.pickupUseCurrentLocation.checked = false; stopAutomaticPassengerPickupGps(); selectedCurrentPickupGps = null; if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = "Exact pickup location is off."; } if (!pickupPlaceMatchesInput(selectedPickupPlace, els.pickupDescription.value)) { clearPickupPlaceSelection(pickupAutocompleteReady() ? "Choose a suggested pickup address if it appears, or check exact current location." : "Pickup will use the address as typed unless exact current location is checked."); } renderRecentAddressShortcuts(); updateFareGuidance(); schedulePickupAutocomplete(); } function handleDestinationInput() { clearLowFareReview(); if (!destinationPlaceMatchesInput(selectedDestinationPlace, els.destination.value)) { clearDestinationPlaceSelection(destinationAutocompleteReady() ? "Choose a suggested address if it appears." : "Destination text will be routed as typed."); } renderRecentAddressShortcuts(); updateFareGuidance(); scheduleDestinationAutocomplete(); } function schedulePickupAutocomplete() { window.clearTimeout(pickupAutocompleteTimer); if (!pickupAutocompleteReady()) { hidePickupSuggestions(); if (els.pickupPlaceStatus && placesAutocompleteEnabled()) { els.pickupPlaceStatus.textContent = "Sign in as a passenger to use pickup suggestions."; } return; } const input = els.pickupDescription.value.trim(); if (input.length < 3) { hidePickupSuggestions(); if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "Type at least 3 characters to search pickup addresses."; return; } const requestId = ++pickupAutocompleteRequestId; pickupAutocompleteTimer = window.setTimeout(async () => { try { if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "Searching pickup addresses..."; const payload = await fetchPlaceAutocomplete({ action: "autocomplete", input, sessionToken: pickupSessionToken(), country: state.passenger?.country ?? selectedPassengerCountry(), city: state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(selectedPassengerCountry()) }); if (requestId !== pickupAutocompleteRequestId) return; renderPickupSuggestions(payload?.suggestions ?? []); if (els.pickupPlaceStatus) { els.pickupPlaceStatus.textContent = (payload?.suggestions ?? []).length ? "Choose the matching pickup from the suggestions." : "No pickup suggestion found; Waka can still use GPS or the full typed address."; } } catch (error) { hidePickupSuggestions(); if (els.pickupPlaceStatus) { els.pickupPlaceStatus.textContent = passengerFacingPlaceErrorMessage( error, "Address suggestions are unavailable right now. Enter the full pickup address." ); } } }, 350); } function scheduleDestinationAutocomplete() { window.clearTimeout(destinationAutocompleteTimer); if (!destinationAutocompleteReady()) { hideDestinationSuggestions(); if (els.destinationPlaceStatus && placesAutocompleteEnabled()) { els.destinationPlaceStatus.textContent = "Sign in as a passenger to use place suggestions."; } return; } const input = els.destination.value.trim(); if (input.length < 3) { hideDestinationSuggestions(); if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = "Type at least 3 characters to search addresses."; return; } const requestId = ++destinationAutocompleteRequestId; destinationAutocompleteTimer = window.setTimeout(async () => { try { if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = "Searching destination addresses..."; const payload = await fetchPlaceAutocomplete({ action: "autocomplete", input, sessionToken: destinationSessionToken(), country: state.passenger?.country ?? selectedPassengerCountry(), city: state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(selectedPassengerCountry()) }); if (requestId !== destinationAutocompleteRequestId) return; renderDestinationSuggestions(payload?.suggestions ?? []); if (els.destinationPlaceStatus) { els.destinationPlaceStatus.textContent = (payload?.suggestions ?? []).length ? "Choose the matching destination from the suggestions." : "No address suggestion found; continue with the full address."; } } catch (error) { hideDestinationSuggestions(); if (els.destinationPlaceStatus) { els.destinationPlaceStatus.textContent = passengerFacingPlaceErrorMessage( error, "Address suggestions are unavailable right now. Enter the full destination address." ); } } }, 350); } async function selectPickupSuggestion(suggestion) { try { clearLowFareReview(); if (els.pickupUseCurrentLocation) els.pickupUseCurrentLocation.checked = false; selectedCurrentPickupGps = null; if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "Confirming pickup place..."; const payload = await fetchPickupPlaceDetails(suggestion); const place = normalizedPlaceSelection({ ...payload?.place, displayName: payload?.place?.displayName || suggestion.mainText || suggestion.text }); if (!place?.placeId) throw new Error("Selected pickup did not return a place id."); selectedPickupPlace = place; rememberRecentAddress("pickup", place); els.pickupDescription.value = place.formattedAddress || place.displayName || suggestion.text; hidePickupSuggestions(); pickupAutocompleteSessionToken = null; if (els.pickupPlaceStatus) { els.pickupPlaceStatus.textContent = `Selected: ${place.displayName || place.formattedAddress}`; } renderRecentAddressShortcuts(); updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(100); } catch (error) { if (els.pickupPlaceStatus) { els.pickupPlaceStatus.textContent = passengerFacingPlaceErrorMessage( error, "Could not confirm that pickup suggestion. Enter the full pickup address." ); } } } async function useCurrentPickupLocation() { clearLowFareReview(); clearStaleGpsPickupFallbackText({ status: false }); const previousPickupAddress = String(els.pickupDescription?.value ?? "").trim(); const canRestorePreviousPickupAddress = Boolean(previousPickupAddress) && previousPickupAddress !== "Capturing current location..." && !pickupUsesCurrentLocationText(previousPickupAddress) && !pickupUsesGpsFallbackText(previousPickupAddress); if (els.pickupUseCurrentLocation) els.pickupUseCurrentLocation.checked = true; if (!window.isSecureContext) { if (els.pickupUseCurrentLocation) els.pickupUseCurrentLocation.checked = false; if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "Current location requires a secure HTTPS page."; return; } if (!navigator.geolocation) { if (els.pickupUseCurrentLocation) els.pickupUseCurrentLocation.checked = false; if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "This browser does not support current location. Type the pickup address instead."; return; } const existingPoint = pendingPickupGps && !pickupGpsQualityIssue(pendingPickupGps) ? pendingPickupGps : null; if (existingPoint) { applyCurrentPickupPoint(existingPoint, "Current location selected. Looking up the nearest pickup address..."); } else { els.pickupDescription.value = "Capturing current location..."; if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "Capturing your current location. Approve the browser location prompt if it appears."; } setButtonBusy(els.useCurrentPickup, true); const point = existingPoint ?? await capturePassengerPickupGps({ automatic: false }); setButtonBusy(els.useCurrentPickup, false); if (!point) { if (els.pickupUseCurrentLocation) els.pickupUseCurrentLocation.checked = false; if (els.pickupDescription.value === "Capturing current location...") els.pickupDescription.value = ""; if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "Current location could not be captured. In Chrome, allow Location for this site or type the pickup address."; return; } applyCurrentPickupPoint(point, "Current location selected. Looking up the nearest pickup address..."); try { const payload = await fetchPickupReverseGeocode(point); const place = normalizedPlaceSelection(payload?.place); if (!place?.formattedAddress) throw new Error("No street address was found for this GPS point."); selectedPickupPlace = { ...place, placeId: null, latitude: point.latitude, longitude: point.longitude, source: "browser-gps" }; rememberRecentAddress("pickup", { ...selectedPickupPlace, formattedAddress: place.formattedAddress }); els.pickupDescription.value = place.formattedAddress; if (els.pickupPlaceStatus) { els.pickupPlaceStatus.textContent = `Using current location: ${place.displayName || place.formattedAddress}. Riders will see the verified pickup point.`; } } catch (error) { selectedPickupPlace = null; if (els.pickupDescription) { els.pickupDescription.value = canRestorePreviousPickupAddress ? previousPickupAddress : ""; } if (els.pickupPlaceStatus) { els.pickupPlaceStatus.textContent = `Exact GPS was captured, but street address lookup did not finish: ${passengerFacingPlaceErrorMessage(error, "address lookup unavailable")}. Type the full pickup address or try current location again.`; } } updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(100); } async function resolveCurrentPickupAddressForPublish(point, fallbackLabel) { const gps = normalizeGpsPoint(point); const label = String(fallbackLabel ?? "").trim(); const needsStreetAddressLookup = !label || pickupUsesCurrentLocationText(label) || pickupUsesGpsFallbackText(label); if (!gps || !needsStreetAddressLookup) return fallbackLabel; try { if (els.pickupPlaceStatus) { els.pickupPlaceStatus.textContent = "Confirming nearest pickup address from current location..."; } const payload = await fetchPickupReverseGeocode(gps); const place = normalizedPlaceSelection(payload?.place); if (!place?.formattedAddress) throw new Error("No street address was found for this GPS point."); selectedCurrentPickupGps = gps; selectedPickupPlace = { ...place, placeId: null, latitude: gps.latitude, longitude: gps.longitude, source: "browser-gps" }; rememberRecentAddress("pickup", { ...selectedPickupPlace, formattedAddress: place.formattedAddress }); els.pickupDescription.value = place.formattedAddress; if (els.pickupPlaceStatus) { els.pickupPlaceStatus.textContent = `Using current location: ${place.displayName || place.formattedAddress}.`; } updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(100); return place.formattedAddress; } catch (error) { logClientWarning("Current pickup reverse geocode before publish failed.", error); selectedPickupPlace = null; const currentValue = String(els.pickupDescription?.value ?? "").trim(); if (!currentValue || pickupUsesCurrentLocationText(currentValue) || pickupUsesGpsFallbackText(currentValue)) { els.pickupDescription.value = ""; } const message = `Street address lookup did not finish: ${passengerFacingPlaceErrorMessage(error, "address lookup unavailable")}. Type the full pickup address or try current location again before publishing.`; if (els.pickupPlaceStatus) { els.pickupPlaceStatus.textContent = message; } updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(100); throw new Error(message); } } async function ensureCurrentPickupAddressForPublish() { clearStaleGpsPickupFallbackText({ status: false }); const capturedPoint = await ensurePassengerPickupGpsForPublish(); const gps = normalizeGpsPoint(selectedCurrentPickupGps ?? capturedPoint ?? pendingPickupGps); const enteredPickupDescription = String(els.pickupDescription?.value ?? "").trim(); const enteredPickupIsExactAddress = Boolean(enteredPickupDescription) && !exactPickupAddressIssue(enteredPickupDescription); if (!passengerWantsCurrentPickup() || enteredPickupIsExactAddress) return enteredPickupDescription; if (!gps || pickupGpsQualityIssue(gps)) return enteredPickupDescription; if (pickupFieldCanUseGpsAutofill()) { applyCurrentPickupPoint(gps, "Current location selected. Looking up the nearest pickup address..."); } return resolveCurrentPickupAddressForPublish(gps, currentPickupLocationLabel(gps)); } function pickupFieldCanUseGpsAutofill() { const value = els.pickupDescription?.value.trim() ?? ""; return !value || pickupUsesCurrentLocationText(value) || pickupUsesGpsFallbackText(value) || value === "Capturing current location..."; } function applyCurrentPickupPoint(point, statusText = "") { const gps = normalizeGpsPoint(point); if (!gps || !els.pickupDescription) return false; selectedPickupPlace = null; selectedCurrentPickupGps = gps; els.pickupDescription.value = currentPickupLocationLabel(gps); hidePickupSuggestions(); if (statusText && els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = statusText; updateFareGuidance(); return true; } function activateUseCurrentPickup(event) { event?.preventDefault?.(); if (useCurrentPickupActivationInFlight) return; useCurrentPickupActivationInFlight = true; Promise.resolve(useCurrentPickupLocation()).finally(() => { window.setTimeout(() => { useCurrentPickupActivationInFlight = false; }, 0); }); } function handlePickupUseCurrentLocationChange(event) { const checked = Boolean(event?.target?.checked ?? els.pickupUseCurrentLocation?.checked); clearLowFareReview(); clearStaleGpsPickupFallbackText({ status: false }); if (checked) { void useCurrentPickupLocation(); ensurePassengerPickupGpsAutoCapture(); return; } stopAutomaticPassengerPickupGps(); selectedCurrentPickupGps = null; if (pickupUsesCurrentLocationText(els.pickupDescription?.value) || pickupUsesGpsFallbackText(els.pickupDescription?.value) || els.pickupDescription?.value === "Capturing current location...") { els.pickupDescription.value = ""; } if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = "Exact pickup location is off."; if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "Enter a pickup address, or check exact current location."; updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(100); } async function selectDestinationSuggestion(suggestion) { try { clearLowFareReview(); if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = "Confirming destination place..."; const payload = await fetchDestinationPlaceDetails(suggestion); const place = normalizedPlaceSelection({ ...payload?.place, displayName: payload?.place?.displayName || suggestion.mainText || suggestion.text }); if (!place?.placeId) throw new Error("Selected destination did not return a place id."); selectedDestinationPlace = place; rememberRecentAddress("destination", place); els.destination.value = place.formattedAddress || place.displayName || suggestion.text; hideDestinationSuggestions(); destinationAutocompleteSessionToken = null; if (els.destinationPlaceStatus) { els.destinationPlaceStatus.textContent = `Selected: ${place.displayName || place.formattedAddress}`; } renderRecentAddressShortcuts(); updateFareGuidance(); } catch (error) { if (els.destinationPlaceStatus) { els.destinationPlaceStatus.textContent = passengerFacingPlaceErrorMessage( error, "Could not confirm that address suggestion. Enter the full destination address." ); } } } function updatePassengerFareModeControls(guidance = null) { const mode = passengerFareMode(); state.passengerFareMode = mode; syncPassengerFareModeInputs(mode); document.querySelectorAll("[data-passenger-fare-mode]").forEach((button) => { const active = normalizePassengerFareMode(button.dataset.passengerFareMode) === mode; button.classList.toggle("active", active); button.setAttribute("aria-pressed", active ? "true" : "false"); }); const nonNegotiable = mode === "non_negotiable"; if (els.fareOffer) { els.fareOffer.readOnly = nonNegotiable; els.fareOffer.required = !nonNegotiable; els.fareOffer.setAttribute("aria-readonly", nonNegotiable ? "true" : "false"); } if (!nonNegotiable) { if (els.passengerFareModeStatus) { els.passengerFareModeStatus.textContent = "Negotiated fare selected. Riders can accept your offer or send counter-offers."; } syncNegotiatedFareInlinePanel(); return; } clearLowFareReview(); els.fareDetailsPanel?.classList.remove("negotiated-fare-inline"); setRideRequestOptionPanel("fare", false); const activeGuidance = guidance ?? (typeof lastRouteFareGuidance !== "undefined" ? lastRouteFareGuidance : null); const automaticFare = passengerMinimumFareFromGuidance(activeGuidance, els.vehiclePreference?.value); if (automaticFare && els.fareOffer) { els.fareOffer.value = String(automaticFare); } if (els.passengerFareModeStatus) { els.passengerFareModeStatus.textContent = automaticFare ? `No negotiation selected. Waka will publish at the minimum fare of ${formatMoney(automaticFare, state.passenger?.country)}; riders can accept or decline.` : "No negotiation selected. Waka will calculate and display the minimum fare after pickup and destination are confirmed."; } } function setPassengerFareMode(value) { const mode = normalizePassengerFareMode(value); state.passengerFareMode = mode; syncPassengerFareModeInputs(mode, { userSelected: true }); saveState(); const guidance = updateFareGuidance(); updatePassengerFareModeControls(guidance); syncNegotiatedFareInlinePanel({ focus: mode === "negotiable" }); } function handlePassengerFareModeSelection(event) { const control = event.currentTarget; if (control instanceof HTMLInputElement && control.id === "passengerFareNegotiable") { setPassengerFareMode(control.checked ? "negotiable" : "non_negotiable"); return; } if (control instanceof HTMLSelectElement && control.id === "passengerFareMode") { setPassengerFareMode(control.value); return; } setPassengerFareMode(control?.dataset?.passengerFareMode); } function handlePassengerFareModeButtonActivation(event) { const now = Date.now(); if (event?.type === "click" && now - passengerFareModeLastPointerAt < 700) { event.preventDefault?.(); return; } if (event?.type && event.type !== "click" && event.type !== "keydown") { if (now - passengerFareModeLastPointerAt < 250) { event.preventDefault?.(); return; } passengerFareModeLastPointerAt = now; } event?.preventDefault?.(); const button = event?.currentTarget; setPassengerFareMode(button?.dataset?.passengerFareMode); } function handlePassengerFareModeButtonKeyActivation(event) { if (!["Enter", " "].includes(event?.key)) return; handlePassengerFareModeButtonActivation(event); } function currentFareReviewKey(guidance, fareOffer) { return JSON.stringify({ pickup: els.pickupDescription?.value.trim() ?? "", destination: els.destination?.value.trim() ?? "", fareOffer: Number(fareOffer) || 0, min: guidance?.min ?? null, max: guidance?.max ?? null, distanceMiles: guidance?.distanceMiles ? Number(guidance.distanceMiles).toFixed(1) : null, minutes: guidance?.minutes ?? null }); } function clearLowFareReview() { pendingLowFareOverrideKey = ""; if (!els.fareReviewPanel) return; els.fareReviewPanel.hidden = true; els.fareReviewPanel.replaceChildren(); } function showLowFareReview(guidance, fareOffer) { if (!els.fareReviewPanel || !guidance) return; setRideRequestOptionPanel("fare", true); const reviewKey = currentFareReviewKey(guidance, fareOffer); els.fareReviewPanel.hidden = false; els.fareReviewPanel.replaceChildren(); const message = document.createElement("div"); message.textContent = `Your $${fareOffer} offer is below the suggested $${guidance.min}-$${guidance.max} range for this route. You can adjust it or publish anyway.`; const actions = document.createElement("div"); actions.className = "fare-review-actions"; const useMinimum = document.createElement("button"); useMinimum.type = "button"; useMinimum.className = "secondary-action"; useMinimum.textContent = `Use $${guidance.min}`; useMinimum.addEventListener("click", () => { els.fareOffer.value = String(guidance.min); clearLowFareReview(); updateFareGuidance(); els.fareOffer.focus(); }); const publishAnyway = document.createElement("button"); publishAnyway.type = "button"; publishAnyway.className = "ghost-action"; publishAnyway.textContent = "Publish anyway"; publishAnyway.addEventListener("click", () => { pendingLowFareOverrideKey = reviewKey; els.rideRequestForm.requestSubmit(); }); actions.append(useMinimum, publishAnyway); els.fareReviewPanel.append(message, actions); els.fareReviewPanel.scrollIntoView({ behavior: "smooth", block: "center" }); } function passengerMinimumAllowedFare(guidance, country) { const baselineMinimum = Number.isFinite(Number(guidance?.min)) ? Number(guidance.min) : minimumFareOffer(country); return Math.max(minimumFareOffer(country), Math.ceil(baselineMinimum - 4)); } function showPassengerMinimumFareBlock(guidance, fareOffer, minimumAllowedFare) { if (!els.fareReviewPanel || !guidance) { translatedAlert("publishRideFailed", { message: `Increase the passenger fare to at least ${formatMoney(minimumAllowedFare)} before publishing.` }); return; } setRideRequestOptionPanel("fare", true); els.fareReviewPanel.hidden = false; els.fareReviewPanel.replaceChildren(); const message = document.createElement("div"); message.textContent = `Your ${formatMoney(fareOffer, state.passenger?.country)} offer is more than ${formatMoney(4, state.passenger?.country)} below the suggested minimum of ${formatMoney(guidance.min, state.passenger?.country)}. Increase it to at least ${formatMoney(minimumAllowedFare, state.passenger?.country)} before publishing.`; const actions = document.createElement("div"); actions.className = "fare-review-actions"; const useMinimumAllowed = document.createElement("button"); useMinimumAllowed.type = "button"; useMinimumAllowed.className = "secondary-action"; useMinimumAllowed.textContent = `Use ${formatMoney(minimumAllowedFare, state.passenger?.country)}`; useMinimumAllowed.addEventListener("click", () => { els.fareOffer.value = String(minimumAllowedFare); clearLowFareReview(); updateFareGuidance(); els.fareOffer.focus(); }); actions.append(useMinimumAllowed); els.fareReviewPanel.append(message, actions); els.fareReviewPanel.scrollIntoView({ behavior: "smooth", block: "center" }); } function xlSpecialFareFloorFromGuidance(guidance) { return guidance?.max != null ? Number(guidance.max) : null; } function requestFareGuidance(request) { const distance = Number(request?.estimatedDistanceMiles); const minutes = Number(request?.estimatedTravelMinutes); if (!Number.isFinite(distance) || distance <= 0) return null; return fareGuidanceFromDistance(distance, Number.isFinite(minutes) ? minutes : null, request?.rideStops, { source: request?.routeEstimateSource, provider: request?.routeEstimateProvider, cached: request?.routeEstimateCached, routeKey: request?.routeEstimateKey, routePolyline: request?.routeEstimatePolyline, country: request?.country, city: request?.city, destinationFingerprint: request?.routeEstimateDestinationFingerprint, estimatedAt: request?.routeEstimateCreatedAt }); } function xlSpecialFareFloorForRequest(request) { if (normalizeCarTypePreference(request?.carTypePreference) !== "suv") return null; return xlSpecialFareFloorFromGuidance(requestFareGuidance(request)); } function showXlSpecialFareBlock(guidance, fareOffer) { if (!els.fareReviewPanel || !guidance) return; setRideRequestOptionPanel("fare", true); els.fareReviewPanel.hidden = false; els.fareReviewPanel.replaceChildren(); const message = document.createElement("div"); message.textContent = `XL/Special rides must start above the normal maximum of $${guidance.max}. Increase the passenger fare before publishing.`; const actions = document.createElement("div"); actions.className = "fare-review-actions"; const useXlMinimum = document.createElement("button"); useXlMinimum.type = "button"; useXlMinimum.className = "secondary-action"; useXlMinimum.textContent = `Use $${guidance.max + 1}`; useXlMinimum.addEventListener("click", () => { els.fareOffer.value = String(guidance.max + 1); clearLowFareReview(); updateFareGuidance(); els.fareOffer.focus(); }); actions.append(useXlMinimum); els.fareReviewPanel.append(message, actions); els.fareReviewPanel.scrollIntoView({ behavior: "smooth", block: "center" }); } function passengerRidePublishedMessage(request) { const fare = formatMoney(request.fareOffer, request.country); if (requestIsNonNegotiableFare(request)) { return `Ride request published for ${fare}. No negotiation: waiting for a rider to accept.`; } return `Ride request published for ${fare}. Waiting for rider offers.`; } function passengerPhoneVerifiedForRidePublishing() { return Boolean(state.passenger?.phoneVerified || smsVerificationRelaxedForTesting()); } function passengerRequestHasRiderCancelReopenNotice(request) { if (!request?.id || !requestBelongsToPassenger(request) || request.status !== "open" || selectedRiderIdForRequest(request)) return false; return (state.notifications ?? []).some((notice) => { if (notice?.recipientRole !== "passenger" || notice.requestId !== request.id) return false; const text = `${notice.eventType || ""} ${notice.id || ""} ${notice.title || ""} ${notice.body || ""}`.toLowerCase(); return /ride_reopened|rider_cancelled_before_pickup|rider_canceled_before_pickup|cancelled_before_pickup|canceled_before_pickup|rider cancelled|rider canceled|open again|reopened/.test(text); }); } function addPassengerRideNotice(title, body, requestId, { deliver = false, eventKey = "" } = {}) { if (deliver && typeof addRideAccountNotice === "function") { return addRideAccountNotice("passenger", title, body, requestId, eventKey); } if (!state.passenger?.id) return; const notice = { id: eventKey ? `notice-passenger-${eventKey}` : makeId("notice"), recipientId: state.passenger.id, recipientRole: "passenger", title, body, requestId, actionUrl: typeof workspaceNotificationUrl === "function" ? workspaceNotificationUrl("passenger", "trips", requestId) : "", eventType: eventKey ? eventKey.split("-")[0] : "passenger_action_status", createdAt: new Date().toISOString(), readAt: null }; state.notifications = upsertById(state.notifications, notice); saveState(); return notice; } async function createBusinessAccount(event) { event.preventDefault(); if (!state.passenger || !hasSignedIn("passenger")) { els.businessAccountStatus.textContent = "Sign in as a passenger before creating a business account."; return; } const businessName = els.businessName.value.trim(); const billingEmail = els.businessBillingEmail.value.trim().toLowerCase(); const businessCategory = els.businessCategory?.value || "other"; const businessAddress = els.businessAddress?.value.trim() || ""; const contactName = els.businessContactName?.value.trim() || state.passenger.name; const contactPhone = els.businessContactPhone?.value.trim() || state.passenger.phone; const planCode = normalizeBusinessPlanCode(els.businessPlan?.value); const referralCode = els.businessReferralCode?.value.trim() || ""; if (businessName.length < 2 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(billingEmail) || businessAddress.length < 4 || contactName.length < 2 || contactPhone.length < 7) { els.businessAccountStatus.textContent = "Enter business name, billing email, address, and authorized contact details."; return; } const localAccount = { id: makeId("business"), ownerId: state.passenger.id, ownerName: state.passenger.name, businessName, billingEmail, businessCategory, businessAddress, contactName, contactPhone, planCode, referralCode, verificationStatus: "pending_review", status: "pending_review", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; try { els.businessAccountStatus.textContent = "Creating business account..."; const savedAccount = await saveBusinessAccountToSupabase(localAccount); state.businessAccounts = upsertById(state.businessAccounts, savedAccount); els.businessName.value = ""; els.businessBillingEmail.value = ""; if (els.businessAddress) els.businessAddress.value = ""; if (els.businessContactName) els.businessContactName.value = ""; if (els.businessContactPhone) els.businessContactPhone.value = ""; if (els.businessReferralCode) els.businessReferralCode.value = ""; if (referralCode) await claimReferralCodeValue("business", referralCode, els.businessAccountStatus); saveState(); renderAll(); els.businessAccountStatus.textContent = businessAccountSummary(savedAccount); } catch (error) { els.businessAccountStatus.textContent = `Business account was not created: ${error.message}`; } } async function updatePassengerActiveLocation(event) { event.preventDefault(); if (!state.passenger || !hasSignedIn("passenger")) return; const country = els.passengerActiveCountry.value; const city = els.passengerActiveCity.value; try { const subdivision = locationSubdivisionLabel(country); els.passengerLocationStatus.textContent = `Updating passenger ${subdivision}...`; await updatePassengerCurrentCityInSupabase(state.passenger.supabaseUserId ?? state.passenger.id, country, city); state.passenger = { ...state.passenger, country, city }; state.passengers = upsertById(state.passengers, state.passenger); clearSelectedRequestOutsideLocation(country, city); clearPassengerPickupGps(); saveState(); populateLocationFields(); hydrateForms(); renderAll(); void refreshMarketplace({ silent: true }); els.passengerLocationStatus.textContent = `Ride requests now publish in ${city} ${subdivision}, ${country}.`; } catch (error) { els.passengerLocationStatus.textContent = error.message; } } function initialBusinessAccountValues() { const wantsBusiness = els.passengerAccountUse?.value === "business"; return { wantsBusiness, businessName: els.passengerInitialBusinessName?.value.trim() ?? "", billingEmail: els.passengerInitialBusinessBillingEmail?.value.trim().toLowerCase() ?? "", businessCategory: els.passengerInitialBusinessCategory?.value || "other", businessAddress: els.passengerInitialBusinessAddress?.value.trim() ?? "", planCode: normalizeBusinessPlanCode(els.passengerInitialBusinessPlan?.value), referralCode: els.passengerInitialBusinessReferralCode?.value.trim() || els.passengerReferralCode?.value.trim() || "" }; } function validBusinessAccountFields(values) { if (!values.wantsBusiness) return true; return values.businessName.length >= 2 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.billingEmail) && values.businessAddress.length >= 4; } async function createInitialBusinessAccountForPassenger(values) { if (!values.wantsBusiness || !state.passenger) return null; const localAccount = { id: makeId("business"), ownerId: state.passenger.id, ownerName: state.passenger.name, businessName: values.businessName, billingEmail: values.billingEmail, businessCategory: values.businessCategory, businessAddress: values.businessAddress, contactName: state.passenger.name, contactPhone: state.passenger.phone, planCode: values.planCode, referralCode: values.referralCode, verificationStatus: "pending_review", status: "pending_review", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; const savedAccount = await saveBusinessAccountToSupabase(localAccount); if (values.referralCode) await claimReferralCodeValue("business", values.referralCode, els.passengerStatus); state.businessAccounts = upsertById(state.businessAccounts, savedAccount); return savedAccount; } async function createPassenger(event) { event.preventDefault(); updatePassengerInitialBusinessFields(); setTranslatedStatus(els.passengerStatus, "checkingPassengerAccount"); const phone = els.passengerPhone.value.trim(); const profilePhotoName = els.passengerPhoto.files[0]?.name ?? state.passenger?.profilePhotoName ?? ""; const businessValues = initialBusinessAccountValues(); const dateOfBirth = businessValues.wantsBusiness ? null : normalizeDateOfBirthInput(els.passengerDob); const email = els.passengerEmail.value.trim().toLowerCase(); let addingPassengerRoleToExistingLogin = false; if (!validateAccountForm(els.passengerAccountForm, els.passengerStatus)) return; if (!validBusinessAccountFields(businessValues)) { els.passengerStatus.textContent = "Enter business name, billing email, and business address, or choose Personal rides."; return; } if (!businessValues.wantsBusiness && !validDateOfBirth(dateOfBirth)) { setTranslatedStatus(els.passengerStatus, "validDateOfBirthRequired"); return; } if (hasSupabaseRuntime()) { try { const excludeUserId = await profileAvailabilityExcludeUserId(email, state.passenger?.id ?? null); const availability = await profileContactAvailability(email, phone, excludeUserId, "passenger"); if (!availability.emailAvailable || !availability.phoneAvailable) { const roleSetup = await existingProfileRoleSetup( email, els.passengerPassword.value, phone, "passenger", availability, (message) => { els.passengerStatus.textContent = message; } ); if (roleSetup.action === "existing_role" && roleSetup.user && roleSetup.profile) { const passengerProfile = profileForWorkspaceRole(roleSetup.profile, { role: "passenger" }); applySignedInProfile("passenger", passengerProfile, roleSetup.user); rememberRoleSetupPhoneVerification("passenger", phone, roleSetup.profile); state.accountMode.passenger = "signin"; state.activeTab = "passenger"; state.showRoleEntry = false; routePassengerToRequestAfterSignIn(); clearPendingProfileRecovery("passenger"); saveState(); renderAll(); els.passengerStatus.textContent = "Passenger account already exists for this Waka login. Signed in and opened Ride request."; return; } if (roleSetup.action !== "can_add_role") { els.passengerStatus.textContent = roleSetupBlockedMessage("passenger", roleSetup.reason); return; } rememberRoleSetupPhoneVerification("passenger", phone, roleSetup.profile); addingPassengerRoleToExistingLogin = true; els.passengerStatus.textContent = "Existing Waka login confirmed. Adding passenger access to this account..."; } } catch (error) { const availabilityMessage = profileAvailabilityErrorMessage(error); if (availabilityMessage) { els.passengerStatus.textContent = availabilityMessage; return; } logClientWarning("Profile contact availability check was skipped.", error); } } if (!(await ensureVerifiedPhoneForAccount("passenger", phone, els.passengerStatus))) return; const passenger = { id: state.passenger?.id ?? makeId("passenger"), name: els.passengerName.value.trim(), email, password: els.passengerPassword.value, phone, phoneVerified: true, phoneVerifiedAt: state.verification.passenger?.verifiedAt ?? state.passenger?.phoneVerifiedAt ?? new Date().toISOString(), phoneVerificationProvider: state.verification.passenger?.provider ?? "manual-pilot", accountUse: businessValues.wantsBusiness ? "business" : "personal", nationalId: businessValues.wantsBusiness ? "" : els.passengerNationalId.value.trim(), dateOfBirth, preferredLanguage: state.language, country: els.passengerCountry.value, city: els.passengerCity.value, profilePhotoName, profilePhotoPath: state.passenger?.profilePhotoPath ?? null, createdAt: state.passenger?.createdAt ?? new Date().toISOString() }; try { setButtonBusy(els.passengerSaveButton, true); const setPassengerStage = (message) => { els.passengerStatus.textContent = message; }; setTranslatedStatus(els.passengerStatus, isSupabaseMode() ? "startingPassengerSupabase" : "savingPassenger"); let user = null; let loadedProfile = null; if (hasSupabaseRuntime() && !addingPassengerRoleToExistingLogin) { const result = await submitNewPassengerOnboardingToSupabase(passenger, setPassengerStage); els.passengerPassword.value = ""; els.passengerPhoto.value = ""; if (result?.emailConfirmationRequired) { state.accountMode.passenger = "signin"; state.activeTab = "passenger"; state.showRoleEntry = false; clearPendingProfileRecovery("passenger"); saveState(); renderAll(); if (els.passengerSignInEmail) els.passengerSignInEmail.value = passenger.email; if (els.passengerSignInPassword) els.passengerSignInPassword.value = ""; const pendingMessage = `Passenger account created for ${passenger.email}. Confirm the email from the Waka link, then sign in here to request rides.`; if (els.passengerSignInStatus) els.passengerSignInStatus.textContent = pendingMessage; if (els.passengerStatus) els.passengerStatus.textContent = pendingMessage; return; } const signedIn = await signInAndLoadProfileForRole(passenger.email, passenger.password, "passenger", setPassengerStage); user = signedIn.user; loadedProfile = signedIn.profile; } else { user = await saveProfileToSupabase({ ...passenger, role: "passenger" }, setPassengerStage, { waitForProfile: true, preventExistingAccount: false }); } state.passenger = loadedProfile ? profileToPassenger(loadedProfile) : { ...passenger, password: undefined, id: user?.id ?? passenger.id, profilePhotoPath: user?.profilePhotoPath ?? passenger.profilePhotoPath, supabaseUserId: user?.id ?? null }; state.sessions.passenger = { phone: state.passenger.phone, email: state.passenger.email, userId: state.passenger.supabaseUserId, signedInAt: new Date().toISOString() }; await claimReferralCodeForRole("passenger", els.passengerStatus); els.passengerPassword.value = ""; els.passengerPhoto.value = ""; state.passengers = upsertById(state.passengers, state.passenger); state.accountMode.passenger = "signin"; clearPendingProfileRecovery("passenger"); let businessCreated = null; try { businessCreated = await createInitialBusinessAccountForPassenger(businessValues); } catch (error) { els.passengerStatus.textContent = `Passenger account was created, but the business account was not created: ${error.message}`; } if (typeof routePassengerToRequestAfterSignIn === "function") { routePassengerToRequestAfterSignIn(); } else { state.activeTab = "passenger"; state.showRoleEntry = false; state.passengerPage = "request"; } saveState(); renderAll(); const passengerCreatedKey = user?.emailSetupPending ? "passengerCreatedEmailPending" : "passengerCreated"; setTranslatedStatus(els.passengerStatus, passengerCreatedKey, { name: state.passenger.name }); if (businessCreated) els.businessAccountStatus.textContent = businessAccountSummary(businessCreated); } catch (error) { setTranslatedStatus(els.passengerStatus, "passengerAccountFailed", { message: passengerAccountErrorMessage(error) }); } finally { setButtonBusy(els.passengerSaveButton, false); } } async function createRideRequest(event) { event.preventDefault(); if (els.rideRequestForm?.dataset.submitting === "true") return; if (els.rideRequestForm) els.rideRequestForm.dataset.submitting = "true"; try { clearStaleGpsPickupFallbackText({ status: false }); if (!state.passenger) { translatedAlert("passengerAccountRequired"); return; } if (!hasSignedIn("passenger")) { translatedAlert("passengerSignInRequired"); return; } if (!passengerPhoneVerifiedForRidePublishing()) { translatedAlert("passengerPhoneRequired"); return; } await assertPlatformFeatureEnabled("ride_publishing_enabled", "Ride publishing"); if (paymentSetupRelaxedForTesting()) { await ensureStagingPaymentAccountForTesting("passenger", state.passenger).catch((error) => { logClientWarning("Staging passenger payment setup could not be prepared before ride publishing.", error); }); } if (hasSupabaseRuntime()) { await loadMarketplaceFromSupabase().catch((error) => { logClientWarning("Marketplace refresh before active ride check was skipped.", error); }); } const pendingRide = passengerPendingRide(); if (pendingRide) { state.selectedRequestId = pendingRide.id; state.passengerPage = "trips"; saveState(); renderAll(); translatedAlert("publishRideFailed", { message: passengerPendingRideMessage(pendingRide) }); return; } if (!paymentAccountReady("passenger", state.passenger) && hasSupabaseRuntime()) { await refreshPaymentAccountsFromSupabase("passenger"); await loadMarketplaceFromSupabase().catch((error) => { logClientWarning("Marketplace refresh before ride publish was skipped.", error); }); } if (!paymentAccountReady("passenger", state.passenger)) { state.passengerPage = "payment"; saveState(); renderAll(); translatedAlert("passengerPaymentRequired"); return; } const fareMode = passengerFareMode(); const vehicleDesignation = normalizeCarTypePreference(els.vehiclePreference.value); let fareOffer = Number(String(els.fareOffer.value).replace(/[^\d]/g, "")); if (fareMode === "negotiable" && (!fareOffer || fareOffer < minimumFareOffer(state.passenger.country))) { translatedAlert("realisticFareRequired"); return; } const destinationAddress = els.destination.value.trim(); if (!destinationAddress) { if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = "Enter a destination address before publishing."; translatedAlert("publishRideFailed", { message: "Enter a destination address before publishing." }); return; } const rideTiming = els.rideTiming.value; const scheduledDate = rideTiming === "scheduled" ? new Date(els.scheduledAt.value) : null; if (rideTiming === "scheduled" && (!els.scheduledAt.value || Number.isNaN(scheduledDate.getTime()))) { translatedAlert("scheduledTimeRequired"); return; } if (scheduledDate && scheduledDate.getTime() <= Date.now() + 30 * 60000) { translatedAlert("scheduleThirtyMinutes"); return; } await waitForRecentAddressConfirmations(); const useExactPickupLocation = passengerWantsCurrentPickup(); if (useExactPickupLocation) { await ensureCurrentPickupAddressForPublish(); } else { selectedCurrentPickupGps = null; } let enteredPickupDescription = els.pickupDescription.value.trim(); let gpsBeforeAddressLookup = useExactPickupLocation ? normalizeGpsPoint(selectedCurrentPickupGps ?? pendingPickupGps) : null; if (useExactPickupLocation) { const exactPickupIssue = gpsBeforeAddressLookup ? pickupGpsQualityIssue(gpsBeforeAddressLookup) : "Exact current location is required before publishing. Allow Location for this site, then capture pickup again."; if (exactPickupIssue) { if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = exactPickupIssue; if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = exactPickupIssue; await waitForPassengerUiPaintBeforeAlert(); translatedAlert("publishRideFailed", { message: exactPickupIssue }); return; } } let pickupDescription = useExactPickupLocation ? enteredPickupDescription || currentPickupLocationLabel(selectedCurrentPickupGps ?? pendingPickupGps) : enteredPickupDescription; if (useExactPickupLocation) { pickupDescription = await resolveCurrentPickupAddressForPublish(gpsBeforeAddressLookup, pickupDescription); } let currentPickupGps = useExactPickupLocation ? normalizeGpsPoint(selectedCurrentPickupGps) ?? gpsBeforeAddressLookup : null; enteredPickupDescription = els.pickupDescription.value.trim(); const pickupAddressIssue = exactPickupAddressIssue(pickupDescription); if (pickupAddressIssue) { if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = pickupAddressIssue; if (els.pickupDescription instanceof HTMLElement) { window.setTimeout(() => els.pickupDescription.focus(), 0); } await waitForPassengerUiPaintBeforeAlert(); translatedAlert("publishRideFailed", { message: pickupAddressIssue }); return; } let pickupGpsForRoute = useExactPickupLocation ? currentPickupGps : null; let pickupOriginDescription = useExactPickupLocation && currentPickupGps ? currentPickupLocationLabel(currentPickupGps) : pickupDescription; let pickupOrigin = routeOriginForEstimate( state.passenger.country, state.passenger.city, els.pickupArea.value, pickupOriginDescription, pickupGpsForRoute ); if (!routeOriginIsSpecific(pickupOrigin)) { const message = useExactPickupLocation ? "Exact current location could not be confirmed. Allow Location for this site or enter the pickup address." : "Enter a complete pickup address before publishing."; if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = message; translatedAlert("publishRideFailed", { message }); return; } if (!enteredPickupDescription && pickupOrigin.source === "browser-gps" && els.pickupDescription) { els.pickupDescription.value = pickupDescription; if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "Using the confirmed pickup address from current location."; } let requestPickupGps = requestPickupGpsFromRouteOrigin( pickupOrigin, useExactPickupLocation ? currentPickupGps ?? pendingPickupGps : null ); if (!requestPickupGps) { const message = pickupOrigin?.source === "typed-address" ? "Choose the pickup address from suggestions, or check Current, so nearby riders can see the request." : "Confirm the pickup address before publishing so nearby riders can see the request."; if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = message; if (els.pickupDescription instanceof HTMLElement) { window.setTimeout(() => els.pickupDescription.focus(), 0); } await waitForPassengerUiPaintBeforeAlert(); translatedAlert("publishRideFailed", { message }); return; } if (pickupOrigin.source === "browser-gps" || currentPickupGps) { const pickupGpsIssue = pickupGpsQualityIssue(requestPickupGps); if (pickupGpsIssue) { const message = "Exact pickup GPS needs a clearer signal before publishing. Move near a window, allow precise Location for this site, then capture pickup again."; if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = pickupGpsIssue; if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = message; await waitForPassengerUiPaintBeforeAlert(); translatedAlert("publishRideFailed", { message }); return; } } let guidance = updateFareGuidance(); const previewGuidance = guidance; if (routeEstimatesEnabled()) { const publishGuidanceKey = routeGuidanceInputKey( state.passenger.country, state.passenger.city, els.pickupArea.value, els.destinationArea.value, pickupOriginDescription, els.destination.value.trim(), rideStopsFormValue(), pickupGpsForRoute, selectedDestinationPlace ); let confirmedGuidance = cachedConfirmedFareGuidanceForKey(publishGuidanceKey); if (!confirmedGuidance && fareGuidanceInFlightKey === publishGuidanceKey) { if (els.fareGuidance) els.fareGuidance.textContent = "Finishing accurate driving distance before publishing..."; confirmedGuidance = await waitForConfirmedFareGuidance(publishGuidanceKey); } if (confirmedGuidance) { guidance = confirmedGuidance; rememberStablePassengerFareGuidance(publishGuidanceKey, guidance); if (els.fareGuidance) els.fareGuidance.textContent = fareGuidanceMessage(guidance); } else { if (els.fareGuidance) els.fareGuidance.textContent = "Checking accurate driving distance before publishing..."; try { const accurateGuidance = await accurateFareGuidanceForRide( state.passenger.country, state.passenger.city, els.pickupArea.value, els.destinationArea.value, els.destination.value.trim(), pickupGpsForRoute, rideStopsFormValue(), pickupOriginDescription, selectedDestinationPlace ); guidance = accurateGuidance ?? previewGuidance; if (routeGuidanceConfirmedForPublish(guidance)) { lastRouteFareGuidance = guidance; lastRouteFareGuidanceKey = publishGuidanceKey; rememberStablePassengerFareGuidance(publishGuidanceKey, guidance); } if (els.fareGuidance) { els.fareGuidance.textContent = guidance ? fareGuidanceMessage(guidance) : routeEstimateErrorMessage(lastRouteEstimateError); } } catch (error) { if (els.fareGuidance) els.fareGuidance.textContent = routeEstimateErrorMessage(error); translatedAlert("publishRideFailed", { message: error.message }); return; } } if (!routeGuidanceConfirmedForPublish(guidance)) { const message = routeEstimateErrorMessage(lastRouteEstimateError); if (els.fareGuidance) els.fareGuidance.textContent = message; translatedAlert("publishRideFailed", { message }); return; } } if (fareMode === "non_negotiable") { const automaticFare = passengerMinimumFareFromGuidance(guidance, vehicleDesignation); if (!automaticFare) { updatePassengerFareModeControls(guidance); translatedAlert("publishRideFailed", { message: "Wait for Waka to calculate the minimum fare before publishing a no-negotiation ride." }); return; } fareOffer = automaticFare; if (els.fareOffer) els.fareOffer.value = String(fareOffer); updatePassengerFareModeControls(guidance); } if (guidance && fareMode === "negotiable") { const minimumAllowedFare = passengerMinimumAllowedFare(guidance, state.passenger.country); if (fareOffer < minimumAllowedFare) { showPassengerMinimumFareBlock(guidance, fareOffer, minimumAllowedFare); return; } } if (guidance && fareMode === "negotiable" && vehicleDesignation === "suv" && fareOffer <= guidance.max) { showXlSpecialFareBlock(guidance, fareOffer); return; } clearLowFareReview(); const paymentPreference = validPaymentPreferenceForCountry(els.paymentPreference.value || "online_card", state.passenger.country); els.paymentPreference.value = paymentPreference; const rideStops = normalizeRideStops(rideStopsFormValue()); const rideStopPoints = rideStopPointsForRoute(rideStops); if (!rideStopPointsComplete(rideStops, rideStopPoints)) { translatedAlert("publishRideFailed", { message: "Choose each stop from the address suggestions so Waka can verify rider arrival at every stop." }); return; } const destinationPlace = destinationPlaceForRoute(els.destination.value.trim()); const publishedPickupArea = pickupAreaForPublish( state.passenger.country, state.passenger.city, els.pickupArea.value, pickupDescription, requestPickupGps ); const publishedDestinationArea = destinationAreaForPublish( state.passenger.country, state.passenger.city, els.destinationArea.value, els.destination.value.trim(), destinationPlace ); const businessAccountId = automaticRideBillingAccountId(); if (els.rideBillingAccount) els.rideBillingAccount.value = businessAccountId ?? ""; const businessAccount = businessAccountId ? passengerBusinessAccounts().find((account) => account.id === businessAccountId) : null; if (businessAccountId && !businessAccountCanRequest(businessAccount)) { translatedAlert("publishRideFailed", { message: "Business account must be Waka-verified with an active free month, Starter billing, or active Partner billing before posting a business ride." }); return; } const request = { id: makeId("request"), passengerId: state.passenger.id, passengerName: state.passenger.name, passengerPhone: state.passenger.phone, businessAccountId, country: state.passenger.country, city: state.passenger.city, pickupArea: publishedPickupArea, pickupDescription, destinationArea: publishedDestinationArea, destination: els.destination.value.trim(), destinationPlaceId: destinationPlace?.placeId ?? null, destinationFormattedAddress: destinationPlace?.formattedAddress ?? null, destinationLatitude: destinationPlace?.latitude ?? null, destinationLongitude: destinationPlace?.longitude ?? null, vehicle: "car", carTypePreference: vehicleDesignation, rideStops, rideStopPoints, fareOffer, fareMode, estimatedDistanceMiles: guidance?.distanceMiles ?? null, estimatedTravelMinutes: guidance?.minutes ?? null, routeEstimateSource: normalizedRouteEstimateSourceForDatabase(guidance?.source), routeEstimateProvider: normalizedRouteEstimateProviderForDatabase(guidance?.source, guidance?.provider), routeEstimateCached: Boolean(guidance?.cached), routeEstimateKey: guidance?.routeKey ?? null, routeEstimatePolyline: guidance?.routePolyline ?? null, routeEstimateDestinationFingerprint: guidance?.destinationFingerprint ?? null, routeEstimateCreatedAt: guidance?.estimatedAt ?? null, paymentPreference, pickupGps: requestPickupGps, pickupLatitude: requestPickupGps?.latitude ?? null, pickupLongitude: requestPickupGps?.longitude ?? null, pickupGpsAccuracyMeters: requestPickupGps?.accuracyMeters ?? null, pickupGpsCapturedAt: requestPickupGps?.capturedAt ?? null, rideTiming, scheduledAt: scheduledDate?.toISOString() ?? null, riderConfirmationStatus: null, riderConfirmationRequestedAt: null, riderConfirmedAt: null, releasedAt: null, status: "open", selectedOfferId: null, createdAt: new Date().toISOString() }; try { const savedRequest = await saveRideRequestToSupabase(request); rememberRideRouteAddresses(savedRequest); state.requests.unshift(savedRequest); state.selectedRequestId = savedRequest.id; state.passengerPage = "trips"; passengerWorkspacePageSelectedInSession = true; if (typeof updatePassengerWorkspaceRoute === "function") { updatePassengerWorkspaceRoute("trips", { replace: true, requestId: savedRequest.id, preferPathRoute: true }); } pendingPickupGps = null; selectedPickupPlace = null; selectedCurrentPickupGps = null; selectedDestinationPlace = null; hidePickupSuggestions(); hideDestinationSuggestions(); if (els.pickupUseCurrentLocation) els.pickupUseCurrentLocation.checked = false; if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = "Exact pickup location is off."; if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "Enter a pickup address, or check exact current location."; if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = "Start typing a destination address."; els.rideRequestForm.reset(); updatePassengerFareModeControls(); updateScheduledRideControls(); setPassengerRiderAvailabilityMessage("Enter a pickup address or check exact current location to see nearby rider availability."); setRideStopsInputEnabled(false); populateLocationFields(); saveState(); renderAll(); void refreshMarketplace({ silent: true }); const publishedMessage = passengerRidePublishedMessage(savedRequest); els.selectedSummary.textContent = publishedMessage; } catch (error) { translatedAlert("publishRideFailed", { message: error.message }); } } catch (error) { const message = error?.message || "The ride request could not be published. Please try again."; if (els.fareGuidance) els.fareGuidance.textContent = message; translatedAlert("publishRideFailed", { message }); } finally { if (els.rideRequestForm) delete els.rideRequestForm.dataset.submitting; } } async function signOutRole(type) { if (typeof rememberWorkspaceUiState === "function") rememberWorkspaceUiState(type); if (type === "passenger" || type === "rider") { if (typeof clearPasswordResetMode === "function") clearPasswordResetMode(type); if (typeof clearPasswordResetLocationFlag === "function") clearPasswordResetLocationFlag(); } if (type === "passenger") { stopAutomaticPassengerPickupGps(); stopPassengerApproachAutoRefresh(); clearRecentAddressHistory(); pendingPickupGps = null; selectedPickupPlace = null; } if (type === "rider") { stopAutomaticRiderGps(); const rider = currentRiderRecord(); if (riderCurrentGps(rider)) { try { await clearRiderLiveGpsInSupabase(clearRiderLiveGpsFields(rider)); } catch (error) { logClientWarning("Could not clear rider live GPS before sign-out.", error); } } } if (isSupabaseMode()) { await clearStaleSupabaseSession(); } supabaseRestSession = null; updateConnectionStatus(); state.sessions[type] = null; state.accountMode[type] = "signin"; if (type === "passenger") { state.passenger = null; state.selectedRequestId = null; pendingPickupGps = null; selectedPickupPlace = null; selectedDestinationPlace = null; hidePickupSuggestions(); hideDestinationSuggestions(); els.passengerSignInPassword.value = ""; setTranslatedStatus(els.passengerSignInStatus, "signedOut"); } if (type === "rider") { state.riderAvailabilityActivated = false; riderAutoGpsPaused = true; lastRiderAutoGpsSyncAt = 0; lastRiderAutoGpsSyncPoint = null; state.rider = null; els.riderSignInPassword.value = ""; setTranslatedStatus(els.riderSignInStatus, "signedOut"); } saveState(); populateLocationFields(); hydrateForms(); renderAll(); } // Rider-facing onboarding, vehicle, eligibility, tax, subscription, availability, and marketplace UI. const subscriptionReminderCheckStorageKey = "waka-subscription-reminder-check-v1"; const subscriptionReminderChecksInFlight = new Set(); const riderProfilePhotoUrlCache = new Map(); function riderSelfBackgroundCheckRecords(riderId = state.rider?.id) { if (!riderId) return []; return (state.backgroundChecks ?? []) .filter((record) => record.riderId === riderId) .sort((a, b) => new Date(b.completedAt ?? b.createdAt ?? 0) - new Date(a.completedAt ?? a.createdAt ?? 0)); } function latestRiderSelfBackgroundCheck(rider = currentRiderRecord()) { return riderSelfBackgroundCheckRecords(rider?.id)[0] ?? null; } function normalizedRiderBackgroundStatus(rider = currentRiderRecord()) { const latest = latestRiderSelfBackgroundCheck(rider); return String(latest?.status ?? rider?.backgroundCheckStatus ?? "not requested").replace(/_/g, " ").toLowerCase(); } function normalizedRiderBackgroundDecision(rider = currentRiderRecord()) { const latest = latestRiderSelfBackgroundCheck(rider); return String(latest?.decision ?? rider?.backgroundCheckDecision ?? "pending").replace(/_/g, " ").toLowerCase(); } function riderTestingRelaxationEnabled(flagName) { return (appConfig[flagName] === true || String(appConfig[flagName] ?? "").toLowerCase() === "true") && /\b(staging|pilot|test|preview)\b/i.test(String(appConfig.projectName || "")); } function riderBackgroundCheckRelaxedForTesting() { return riderTestingRelaxationEnabled("relaxBackgroundCheckForTesting"); } function riderManualBackgroundReviewMode() { return /\b(manual|admin)/i.test(String(appConfig.backgroundCheckProvider || "")); } function riderCanStartBackgroundCheckFromStatus(rider = currentRiderRecord()) { if (riderManualBackgroundReviewMode()) return false; return rider?.status === "background_pending" || (rider?.status === "pending" && riderBackgroundCheckRelaxedForTesting()); } function riderBackgroundCheckStep(rider = currentRiderRecord()) { if (riderManualBackgroundReviewMode()) { if (rider?.status === "approved") return ["complete", "Admin safety review", "Waka admin review is complete."]; if (rider?.status === "background_pending") return ["current", "Admin safety review", "Waka admin is reviewing local documents, vehicle details, permit, and safety information."]; return ["locked", "Admin safety review", "Waka admin reviews local documents before approval."]; } const status = normalizedRiderBackgroundStatus(rider); const decision = normalizedRiderBackgroundDecision(rider); const riderStatus = rider?.status ?? ""; if (riderStatus === "pending" && riderBackgroundCheckRelaxedForTesting()) { return ["current", "Background check", "Testing mode allows the rider to start the Checkr step from Eligibility while admin review is pending."]; } if (["pending", "needs_correction", "profile only"].includes(riderStatus)) { return ["locked", "Background check", "Admin will unlock Checkr only after the application details pass initial review."]; } if (!["background_pending", "approved"].includes(riderStatus)) { return ["locked", "Background check", "Provider screening opens only after admin invites the rider to Checkr."]; } if (decision === "clear") return ["complete", "Background check", "Provider screening is clear and available to admin."]; if (decision === "consider") return ["current", "Background check", "Provider screening needs admin review before approval."]; if (decision === "adverse") return ["locked", "Background check", "Provider returned an adverse status; contact Waka support."]; if (["requested", "running", "review"].includes(status)) return ["current", "Background check", "Provider screening is in progress."]; if (rider?.status === "background_pending") return ["current", "Background check", "Complete Checkr screening from Eligibility checks."]; return ["locked", "Background check", "Admin will unlock Checkr only after the application details pass initial review."]; } function riderBackgroundCheckReadyForAdminReview(rider = currentRiderRecord()) { return ["clear", "consider"].includes(normalizedRiderBackgroundDecision(rider)); } function riderWorkspaceStatusMessage(rider = currentRiderRecord()) { if (!rider) return "Sign in or submit an application to access the rider platform."; if (rider.needsApplication || rider.status === "profile only") { return "Create or sign in with a separate Waka Cameroon rider account to submit a rider application."; } if (rider.status === "pending") { if (directRidePaymentMode()) { return "Your rider application is waiting for Waka Cameroon admin review. Ride requests, offers, and chat unlock after the required document and safety review steps."; } return riderBackgroundCheckRelaxedForTesting() ? "Your rider application is waiting for admin review. Stay on Eligibility to monitor progress and start the relaxed Checkr testing step." : "Your rider application is waiting for admin document review. Checkr, Stripe, ride requests, offers, and chat unlock only after the required review steps."; } if (rider.status === "background_pending") { return directRidePaymentMode() ? "Your rider application passed initial admin review. Complete any requested local document or permit review so admin can make the final decision." : "Your rider application passed initial admin review. Complete the Checkr background check from Eligibility checks so admin can make the final decision."; } if (rider.status === "needs_correction") { return `Admin requested rider application corrections. Update the rider form and resubmit before review continues.${rider.reviewNote ? ` Note: ${rider.reviewNote}` : ""}`; } if (rider.status === "declined") { return "Your rider application was declined by admin. Contact Waka support before submitting new documents."; } if (rider.status !== "approved") { return "Admin approval is required before the rider platform unlocks."; } if (!riderComplianceReady(rider)) { return riderComplianceStatusText(rider); } const end = riderAccessEnd(rider); const remaining = daysUntil(end); const label = riderAccessLabel(rider); if (isSubscriptionActive(rider)) { const setupGaps = [ paymentAccountReady("rider", rider) ? null : "payment account", riderCurrentFreshGps(rider) ? null : "availability activation" ].filter(Boolean); if (remaining > subscriptionRenewalNoticeDays) { const accessName = label === "free trial" ? "Free trial" : "Paid rider access"; return setupGaps.length ? `Approved. ${accessName} is active until ${formatDate(end)}. Complete ${setupGaps.join(", ")} before requests appear.` : directRidePaymentMode() ? `Approved. ${accessName} is active until ${formatDate(end)}. Wallet review continues through Waka operations.` : `Approved. ${accessName} is active until ${formatDate(end)}. Payment choices open 3 days before expiry.`; } const reminder = remaining <= subscriptionRenewalNoticeDays ? (directRidePaymentMode() ? " Waka operations will review wallet/commission status before the free period ends." : " Renewal is due soon; choose manual payment or automatic renewal so paid access starts after this period ends.") : ""; const setup = setupGaps.length ? ` Complete ${setupGaps.join(", ")} before requests appear.` : ""; return `Approved. Your ${label} has ${pluralDays(remaining)} left, until ${formatDate(end)}.${reminder}${setup}`; } return directRidePaymentMode() ? "Approved, but rider wallet/access review is inactive. Contact Waka operations to restore request access." : "Approved, but paid rider access is inactive. Choose weekly or monthly Waka Rider Access before receiving and responding to ride requests."; } function riderFlowModel(rider = currentRiderRecord()) { const vehicleName = "Car"; const location = rider?.city && rider?.country ? `${rider.city}, ${rider.country}` : "Market not set"; const baseMeta = rider ? [`${vehicleName} platform`, location, `Status: ${rider.status ?? "not submitted"}`] : []; if (!rider) { const manualReview = riderManualBackgroundReviewMode(); return { title: "Application not submitted", summary: "Create a rider account and submit required documents for admin review.", meta: baseMeta, steps: [ ["current", "Account", "Create rider profile and upload required documents."], ["locked", "Admin document review", manualReview ? "Admin checks local documents, vehicle details, permit, and emergency contact." : "Admin checks the application before Waka pays for Checkr screening."], ["locked", manualReview ? "Manual safety review" : "Background check", manualReview ? "Provider billing is disabled for the Cameroon MVP." : "Provider screening opens only after admin invites you."], ["locked", "Admin review", manualReview ? "Admin approval depends on documents, local safety review, and eligibility." : "Admin approval depends on documents, background check, and eligibility."], ["locked", directRidePaymentMode() ? "Free period and wallet review" : "Free trial and paid rider access", directRidePaymentMode() ? "The free period starts after the first completed ride; wallet/commission review follows." : "The free trial starts after approval. Weekly or monthly Waka Rider Access applies after the trial."], ["locked", "Ride requests", "Vehicle-matching requests unlock after approval and active access."] ] }; } if (rider.status === "pending") { const manualReview = riderManualBackgroundReviewMode(); const relaxedBackground = riderBackgroundCheckRelaxedForTesting(); return { title: "Application pending", summary: manualReview ? "Waka admin is reviewing your Cameroon rider application, documents, vehicle details, permit, and emergency contact." : relaxedBackground ? "Admin is reviewing your application. Eligibility remains the only rider page during review, and staging lets you start the Checkr testing step without production provider cost." : "Admin is reviewing your application details and documents first. Checkr stays locked until admin invites you, so Waka does not pay for a background check before corrections are handled.", meta: baseMeta, steps: [ ["complete", "Application submitted", "Profile and rider documents are saved for review."], ["current", "Admin document review", manualReview ? "Admin checks your application before approving or requesting corrections." : "Admin checks your application before releasing the paid background-check step."], manualReview ? ["current", "Manual safety review", "No Checkr/provider step is required in the Cameroon MVP."] : relaxedBackground ? ["current", "Checkr testing step", "Open Eligibility checks and start the relaxed Checkr background-check flow for staging."] : ["locked", "Checkr background check", "This unlocks only after admin confirms the application is ready."], ["locked", "Final admin decision", manualReview ? "Admin approves, declines, or requests corrections after local review." : "Admin approves or declines after Checkr results return."], ["locked", directRidePaymentMode() ? "Free period" : "Free trial", directRidePaymentMode() ? "The rider free period starts after the first completed ride." : "The rider free trial starts after admin approval."], ["locked", "Ride requests", "Marketplace access is blocked while pending."] ] }; } if (rider.status === "background_pending") { const manualReview = riderManualBackgroundReviewMode(); return { title: manualReview ? "Manual safety review" : "Background check invited", summary: manualReview ? "Admin has moved your application into final local safety review. Watch for corrections or approval." : "Admin has approved your application details for Checkr screening. Open Eligibility checks, start the background check, and follow the provider email, SMS, or hosted instructions so admin can see the result.", meta: [...baseMeta, manualReview ? "Manual review" : "Checkr unlocked"], steps: [ ["complete", "Application submitted", "Profile and rider documents are saved."], ["complete", "Admin document review", manualReview ? "Admin moved the application into final local review." : "Admin released the background-check step."], riderBackgroundCheckStep(rider), [manualReview || riderBackgroundCheckReadyForAdminReview(rider) ? "current" : "locked", "Final admin decision", manualReview ? "Admin approves, declines, or requests corrections after local review." : "Admin approves or declines after Checkr results return."], ["locked", directRidePaymentMode() ? "Free period" : "Free trial", directRidePaymentMode() ? "The rider free period starts after the first completed ride." : "The rider free trial starts after final approval."], ["locked", "Ride requests", "Marketplace access is blocked until approval."] ] }; } if (rider.status === "needs_correction") { return { title: "Application corrections needed", summary: rider.reviewNote || "Admin reopened the rider application for updates. Make corrections and resubmit from Profile.", meta: [...baseMeta, "Corrections requested"], steps: [ ["complete", "Application submitted", "Admin reviewed the first submission."], ["current", "Rider corrections", "Update the rider form and resubmit for admin review."], ["locked", "Admin document review", "Review continues after corrected resubmission."], ["locked", riderManualBackgroundReviewMode() ? "Manual safety review" : "Checkr background check", riderManualBackgroundReviewMode() ? "Admin resumes local review after corrections pass initial review." : "Admin unlocks Checkr only after corrections pass initial review."], ["locked", "Final admin decision", riderManualBackgroundReviewMode() ? "Admin approves, declines, or asks for more correction after local review." : "Admin approves or declines after Checkr results return."], ["locked", "Ride requests", "Marketplace access remains blocked until approval."] ] }; } if (rider.status === "declined") { return { title: "Application declined", summary: "Rider access is blocked until Waka support or admin resolves the application.", meta: baseMeta, steps: [ ["complete", "Application submitted", "The rider application was reviewed."], riderBackgroundCheckStep(rider), ["locked", "Admin decision", "The current decision is declined."], ["locked", directRidePaymentMode() ? "Rider wallet review" : "Paid rider access", "No rider access is active."], ["locked", "Ride requests", "Marketplace access remains blocked."] ] }; } if (rider.status === "approved" && isSubscriptionActive(rider)) { if (!riderComplianceReady(rider)) { return { title: "Compliance renewal required", summary: riderComplianceStatusText(rider), meta: [...baseMeta, "License or insurance renewal needed"], steps: [ ["complete", "Application approved", "Admin approval is recorded."], ["locked", "License and insurance compliance", riderComplianceStatusText(rider)], ["locked", "Rider availability", "Service is blocked until compliance dates are current."], ["locked", "Ride requests", "Marketplace access remains blocked while compliance is expired or missing."] ] }; } const end = riderAccessEnd(rider); const remaining = daysUntil(end); const label = riderAccessLabel(rider); const accessHealthy = remaining > subscriptionRenewalNoticeDays; const paymentReady = paymentAccountReady("rider", rider); const directPayment = directRidePaymentMode(); const paymentLabel = directPayment ? "Wallet mode active" : paymentSetupRelaxedForTesting() ? "Staging payout relaxed" : "Payment linked"; const regionsReady = true; const gpsReady = Boolean(riderCurrentFreshGps(rider)); const canActivate = riderCanActivateAvailability(rider); const readyForRequests = paymentReady && regionsReady && gpsReady; return { title: readyForRequests ? `${vehicleName} rider platform active` : "Rider setup required", summary: readyForRequests ? (accessHealthy ? `${label === "free trial" ? "Free trial" : "Paid rider access"} active until ${formatDate(end)}.` : `${label === "free trial" ? "Free trial" : "Paid rider access"} has ${pluralDays(remaining)} left.`) : (directPayment ? "Activate rider availability before requests appear. Waka operations handles wallet review for Cameroon direct payment." : paymentSetupRelaxedForTesting() ? "Activate rider availability before requests appear. Stripe payout setup is relaxed for staging." : "Payment account and rider activation are required before requests appear."), meta: [ ...baseMeta, `Access until ${formatDate(end)}`, remaining <= subscriptionRenewalNoticeDays ? "Renewal reminder active" : "Renewal reminder off", paymentReady ? paymentLabel : "Payment needed", "Nearby pickup scope active", riderDestinationScopeLabel(), gpsReady ? "Available online" : "Availability offline" ], steps: [ ["complete", "Eligibility approved", "Documents and background-check decision passed admin review."], ["complete", "Application approved", "Admin approval is complete."], ["current", label === "free trial" ? "Free period" : directPayment ? "Wallet review" : "Paid rider access", accessHealthy ? (directPayment ? "Wallet review continues in Waka operations." : "Payment choices open 3 days before expiry.") : `${pluralDays(remaining)} left before payment is required.`], [paymentReady ? "complete" : "current", directPayment ? "Wallet mode" : "Payout account", paymentReady ? (directPayment ? "Direct passenger-to-rider payment is active." : paymentSetupRelaxedForTesting() ? "Staging allows testing without Stripe Connect." : "Payout account is saved.") : "Save a payout account before receiving requests."], ["complete", "Marketplace scope", "Showing all nearby pickups within the pickup radius."], [gpsReady ? "complete" : canActivate ? "current" : "locked", "Rider availability", gpsReady ? "You are online for nearby requests." : "Activate availability from the Initialize rider availability menu before requests appear."], [readyForRequests ? "complete" : "current", "Ride requests", readyForRequests ? `${vehicleName} rider sees matching passenger requests and can accept or counter-offer.` : (paymentSetupRelaxedForTesting() ? "Incoming requests appear after availability is activated." : "Incoming requests appear after payout and activation are ready.")] ] }; } return { title: "Rider access payment required", summary: directRidePaymentMode() ? "The free period has ended. Rider access is paused until Waka operations confirms wallet and commission readiness." : "The free trial has ended. Rider access is paused until the provider confirms weekly or monthly Waka Rider Access payment.", meta: [...baseMeta, riderPlanSummary()], steps: [ ["complete", "Eligibility approved", "Documents and background-check decision passed admin review."], ["complete", "Application approved", "Admin approval is complete."], ["locked", directRidePaymentMode() ? "Free period inactive" : "Trial or paid access inactive", directRidePaymentMode() ? "Wallet/commission review is required after the free period." : "The free trial has ended and weekly or monthly access is not active."], ["current", directRidePaymentMode() ? "Wallet review" : "Rider access payment", directRidePaymentMode() ? "Contact Waka operations to restore rider tools." : "Open Waka Rider Access checkout to restore rider tools."], ["locked", "Ride requests", directRidePaymentMode() ? "Marketplace access is blocked until wallet review is active." : "Marketplace access is blocked until subscription is active."] ] }; } function riderOverviewCard({ label, value, detail, target, tone = "neutral" }) { const targetAttribute = target ? ` data-rider-overview-target="${escapeHtml(target)}"` : ""; return ` `; } function riderOverviewStatusCards(rider = currentRiderRecord()) { const availablePages = typeof availableRiderWorkspacePages === "function" ? availableRiderWorkspacePages(rider) : riderWorkspacePages; const targetOrNull = (page) => availablePages.includes(page) ? page : null; const status = rider?.status ?? "not submitted"; const eligibility = { pending: ["Admin review", riderManualBackgroundReviewMode() ? "Monitor local document and safety review." : "Monitor review and Checkr testing progress.", "warning", "checks"], background_pending: [riderManualBackgroundReviewMode() ? "Final review" : "Checkr ready", riderManualBackgroundReviewMode() ? "Waka admin is completing final local review." : "Open Eligibility checks to complete provider screening.", "current", "checks"], needs_correction: ["Corrections needed", "Update the Profile form and resubmit.", "warning", "profile"], approved: ["Approved", "Eligibility is complete. Keep setup active.", "ready", "checks"], declined: ["Declined", "Contact Waka support before continuing.", "danger", "support"], suspended: ["Suspended", "Rider access is paused by admin.", "danger", "support"], "profile only": ["Application needed", "Complete the rider application from Profile.", "warning", "profile"] }[status] ?? ["Not submitted", "Create or finish the rider application.", "warning", "profile"]; const requestsReady = riderCanSeeRequests(rider); const regionsReady = true; const payoutReady = paymentAccountReady("rider", rider); const activeAccess = rider?.status === "approved" && isSubscriptionActive(rider); const availabilityReady = Boolean(riderCurrentFreshGps(rider)); const blockingRide = riderBlockingImmediateRide(rider); let requestValue = "Locked"; let requestDetail = riderWorkspaceStatusMessage(rider); let requestTarget = rider?.status === "approved" ? "initialize" : "checks"; if (blockingRide) { requestValue = "On a ride"; requestDetail = blockingRide.status === "in_progress" ? "New immediate requests resume when this ride is about 7 minutes from drop-off." : "New immediate requests pause while the matched ride is active."; requestTarget = "requests"; } else if (requestsReady) { requestValue = "Receiving"; requestDetail = "Incoming passenger requests appear here for accept or counter-offer."; requestTarget = "requests"; } else if (rider?.status !== "approved") { requestValue = "Admin review"; requestDetail = "Ride requests unlock after final admin approval."; requestTarget = "checks"; } else if (!activeAccess) { requestValue = directRidePaymentMode() ? "Wallet review" : "Subscription needed"; requestDetail = directRidePaymentMode() ? "Resolve wallet/access review before ride requests appear." : "Renew or restore rider access before ride requests appear."; requestTarget = "checks"; } else if (!riderComplianceReady(rider)) { requestValue = "Renewal needed"; requestDetail = riderComplianceStatusText(rider); requestTarget = "profile"; } else if (!payoutReady) { requestValue = directRidePaymentMode() ? "Wallet needed" : "Payout needed"; requestDetail = directRidePaymentMode() ? "Resolve Waka wallet review first. Then use Initialize rider availability before opening Ride requests." : "Set up Stripe payout first. Then use Initialize rider availability before opening Ride requests."; requestTarget = "payment"; } else if (!regionsReady) { requestValue = "Choose regions"; requestDetail = "Open Initialize rider availability to choose preferred destinations or show all nearby pickups."; requestTarget = "initialize"; } else if (!availabilityReady) { requestValue = "Activate"; requestDetail = "Open Initialize rider availability and tap Activate when you are ready to receive nearby requests."; requestTarget = "initialize"; } const records = riderCompletedRideEarningRecords(rider); const now = new Date(); const monthTotal = periodEarningsTotal(records, new Date(now.getFullYear(), now.getMonth(), 1)); const earningsValue = records.length ? formatMoney(monthTotal, rider?.country) : formatMoney(0, rider?.country); const earningsDetail = records.length ? `${records.length} completed ride${records.length === 1 ? "" : "s"} loaded.` : "Completed ride earnings will appear after trips finish."; const payoutValue = payoutReady ? (directRidePaymentMode() ? "Wallet ready" : "Stripe ready") : rider?.status === "approved" ? "Action needed" : "Locked"; const payoutDetail = payoutReady ? paymentAccountSummary("rider", rider) : rider?.status === "approved" ? (directRidePaymentMode() ? "Contact Waka operations if wallet review is not current." : "Set up Stripe payout before earnings can be sent.") : (directRidePaymentMode() ? "Wallet review opens after approval." : "Payout setup opens after approval."); return [ { label: "Eligibility", value: eligibility[0], detail: eligibility[1], tone: eligibility[2], target: targetOrNull(eligibility[3]) }, { label: "Ride access", value: requestValue, detail: requestDetail, tone: requestsReady ? "ready" : "warning", target: targetOrNull(requestTarget) }, { label: "Earnings this month", value: earningsValue, detail: earningsDetail, tone: records.length ? "ready" : "neutral", target: targetOrNull("earnings") }, { label: "Payout account", value: payoutValue, detail: payoutDetail, tone: payoutReady ? "ready" : rider?.status === "approved" ? "warning" : "neutral", target: targetOrNull("payment") } ]; } function renderRiderOverviewGrid(riderSignedIn = Boolean(state.sessions.rider && state.rider), rider = currentRiderRecord()) { if (!els.riderOverviewGrid) return; const onOverview = typeof riderWorkspacePage !== "function" || riderWorkspacePage() === "overview"; els.riderOverviewGrid.hidden = !riderSignedIn || !onOverview; if (!riderSignedIn || !onOverview) { els.riderOverviewGrid.innerHTML = ""; return; } els.riderOverviewGrid.innerHTML = riderOverviewStatusCards(rider).map(riderOverviewCard).join(""); els.riderOverviewGrid.querySelectorAll("[data-rider-overview-target]").forEach((card) => { card.addEventListener("click", () => setRiderWorkspacePage(card.dataset.riderOverviewTarget)); }); } function openRiderCorrectionForm() { if (typeof setRiderWorkspacePage === "function") { setRiderWorkspacePage("profile"); } else { state.riderPage = "profile"; saveState(); renderAll(); } hydrateForms(); const note = currentRiderRecord()?.reviewNote; if (els.riderStatus) { els.riderStatus.textContent = `Admin requested corrections. Update the rider application form and resubmit.${note ? ` Note: ${note}` : ""}`; } setTimeout(() => { els.riderAccountForm?.scrollIntoView({ block: "start", behavior: "smooth" }); const firstEditable = els.riderAccountForm?.querySelector("input:not([type='hidden']), select, textarea"); firstEditable?.focus({ preventScroll: true }); }, 0); } function renderRiderFlow() { const riderSignedIn = Boolean(state.sessions.rider && state.rider); const page = typeof riderWorkspacePage === "function" ? riderWorkspacePage() : "overview"; const rider = currentRiderRecord(); const showProgressOnChecks = page === "checks" && rider?.status !== "approved"; els.riderFlowCard.hidden = !riderSignedIn || !(page === "overview" || showProgressOnChecks); if (!riderSignedIn) return; const model = riderFlowModel(rider); els.riderFlowTitle.textContent = model.title; els.riderFlowSummary.textContent = model.summary; els.riderFlowSteps.innerHTML = model.steps.map(([status, label, detail]) => `
${escapeHtml(label)} ${escapeHtml(detail)}
${escapeHtml(status)}
`).join(""); els.riderFlowMeta.innerHTML = model.meta.map(chip).join(""); if (els.riderFlowActions) { const needsCorrection = rider?.status === "needs_correction"; const backgroundPending = rider?.status === "background_pending"; els.riderFlowActions.hidden = !needsCorrection && !backgroundPending; els.riderFlowActions.innerHTML = needsCorrection ? `` : backgroundPending ? `` : ""; els.riderFlowActions.querySelector("#openRiderCorrectionForm")?.addEventListener("click", openRiderCorrectionForm); els.riderFlowActions.querySelector("#openRiderEligibilityChecks")?.addEventListener("click", () => setRiderWorkspacePage("checks")); } } function populateRiderDailyRegionOptions(country = els.riderActiveCountry?.value, city = els.riderActiveCity?.value) { const rider = currentRiderRecord(); populateMultiSelect(els.riderDailyRegions, areas(country, city).map((area) => area.name), riderDailyDestinationRegions(rider)); const preference = riderDayPreferenceFor(rider); if (preference?.showAllNearbyPickups) state.riderDestinationScope = "all"; if (els.riderDestinationScope) els.riderDestinationScope.value = state.riderDestinationScope; } function vehicleYearOptions() { const currentYear = new Date().getFullYear() + 1; const years = []; for (let year = currentYear; year >= minimumVehicleYear; year -= 1) years.push(String(year)); return years; } function populateVehicleCatalogFields(rider = state.rider) { if (!els.riderCarMake || !els.riderCarModel || !els.riderCarBodyType || !els.riderCarYear || !els.riderCarColor) return; const makes = Object.keys(carMakeCatalog); const selectedMake = makes.includes(rider?.carMake) ? rider.carMake : makes[0]; populateSelect(els.riderCarMake, makes, selectedMake); populateSelect(els.riderCarModel, carMakeCatalog[selectedMake] ?? carMakeCatalog.Other, rider?.carModel); populateSelectOptions(els.riderCarBodyType, carBodyTypeOptions, normalizeCarBodyType(rider?.carBodyType)); populateRiderVehicleDesignationOptions(rider); populateSelect(els.riderCarYear, vehicleYearOptions(), String(rider?.carYear ?? new Date().getFullYear())); populateSelect(els.riderCarColor, carColors, rider?.carColor ?? carColors[0]); } function riderVehicleDesignationSelection(rider, bodyType) { const current = els.riderVehicleDesignation?.value; const stored = rider?.vehicleDesignation || riderDocumentMetadata(rider).vehicleDesignation; const candidate = current && current !== "normal" ? current : stored || current; const selected = normalizeRiderVehicleDesignation(candidate, bodyType); if (carBodyTypeAllowsXlSpecial(bodyType)) return selected === "normal" ? "both" : selected; return "normal"; } function populateRiderVehicleDesignationOptions(rider = state.rider) { if (!els.riderVehicleDesignation) return; const bodyType = normalizeCarBodyType(els.riderCarBodyType?.value ?? rider?.carBodyType); const allowsXlSpecial = carBodyTypeAllowsXlSpecial(bodyType); const selected = riderVehicleDesignationSelection(rider, bodyType); populateSelectOptions(els.riderVehicleDesignation, riderVehicleDesignationOptions, selected); [...els.riderVehicleDesignation.options].forEach((option) => { option.disabled = !allowsXlSpecial && option.value !== "normal"; }); els.riderVehicleDesignation.disabled = false; els.riderVehicleDesignation.value = selected; } function updateRiderVehicleDesignationForBodyType() { const bodyType = normalizeCarBodyType(els.riderCarBodyType?.value); populateRiderVehicleDesignationOptions({ ...state.rider, carBodyType: bodyType, vehicleDesignation: riderVehicleDesignationSelection(state.rider, bodyType) }); if (state.rider) { state.rider.carBodyType = bodyType; state.rider.vehicleDesignation = normalizeRiderVehicleDesignation(els.riderVehicleDesignation?.value, bodyType); } } function riderProfileDetailRow(label, value) { return `
${escapeHtml(label)}${escapeHtml(value || "not captured")}
`; } function riderProfileDetailSection(title, rows) { return `

${escapeHtml(title)}

${rows.map(([label, value]) => riderProfileDetailRow(label, value)).join("")}
`; } function riderProfileRatingSection(rider) { const categories = typeof riderRatingCategorySummaries === "function" ? riderRatingCategorySummaries(rider?.id) : []; const count = categories.find((category) => category.key === "overall")?.count ?? 0; const rows = categories.map((category) => [ category.label, Number(category.percent) ? `${Math.round(category.percent)}%` : "not enough ratings yet" ]); rows.push(["Ratings counted", count ? `${count}` : "0"]); return riderProfileDetailSection("Anonymous ratings", rows); } function renderRiderRatingsPanel(rider = currentRiderRecord()) { if (!els.riderRatingsPanel) return; const categories = typeof riderRatingCategorySummaries === "function" ? riderRatingCategorySummaries(rider?.id) : []; const count = categories.find((category) => category.key === "overall")?.count ?? 0; const categoryRows = categories.map((category) => riderProfileDetailRow( category.label, Number(category.percent) ? `${Math.round(category.percent)}%` : "not enough ratings yet" )).join(""); els.riderRatingsPanel.innerHTML = ` Anonymous passenger feedback

Rider ratings

Ratings are shown as category percentages. Waka does not show which passenger submitted a rating.

${categoryRows} ${riderProfileDetailRow("Ratings counted", count ? `${count}` : "0")}
`; } function riderInitials(rider = state.rider) { const name = String(rider?.name ?? rider?.email ?? rider?.phone ?? "Rider").trim(); const parts = name.split(/\s+/).filter(Boolean); return (parts.length > 1 ? `${parts[0][0]}${parts[1][0]}` : parts[0]?.slice(0, 2) || "R").toUpperCase(); } function setRiderProfileAvatarFallback(rider = state.rider) { if (!els.riderProfileAvatar) return; els.riderProfileAvatar.textContent = riderInitials(rider); } async function ensureRiderProfilePhotoUrl(rider = state.rider) { if (!els.riderProfileAvatar || !rider?.profilePhotoPath || !isSupabaseMode() || !supabaseClient) return; const cacheKey = rider.profilePhotoPath; const cached = riderProfilePhotoUrlCache.get(cacheKey); if (cached) { els.riderProfileAvatar.innerHTML = ``; return; } try { const { data, error } = await supabaseClient.storage .from(appConfig.buckets.profilePhotos) .createSignedUrl(rider.profilePhotoPath, 600); if (error || !data?.signedUrl) throw error || new Error("Signed profile photo URL was not returned."); riderProfilePhotoUrlCache.set(cacheKey, data.signedUrl); if (currentRiderRecord()?.profilePhotoPath === rider.profilePhotoPath) { els.riderProfileAvatar.innerHTML = ``; } } catch (error) { logClientWarning("Rider profile picture could not be displayed.", error); setRiderProfileAvatarFallback(rider); } } function renderRiderProfileSummary(rider = currentRiderRecord()) { if (!rider) return; setRiderProfileAvatarFallback(rider); if (rider.profilePhotoPath) void ensureRiderProfilePhotoUrl(rider); if (els.riderProfilePhotoStatus) { els.riderProfilePhotoStatus.textContent = rider.profilePhotoPath || rider.profilePhotoName ? `Profile picture: ${rider.profilePhotoName || "uploaded"}` : "Profile picture not uploaded."; } syncRiderNavigationPreferenceInput(riderNavigationPreference(rider)); if (!els.riderProfileDetailList) return; if (rider.status !== "approved" && rider.status !== "needs_correction" && !rider.needsApplication && rider.status !== "profile only") { els.riderProfileDetailList.innerHTML = riderProfileDetailSection("Application", [ ["Application status", rider.status || "pending"], ["Next step", riderWorkspaceStatusMessage(rider)] ]); renderRiderComplianceRenewalForm(rider); return; } const metadata = riderDocumentMetadata(rider); const vehicle = `${rider.carYear || ""} ${rider.carMake || ""} ${rider.carModel || ""}`.trim() || "Vehicle not complete"; els.riderProfileDetailList.innerHTML = [ riderProfileDetailSection("Identity", [ ["Driver's license", rider.nationalId || "not captured"], ["License expires", rider.driverLicenseExpiresOn ? formatDate(rider.driverLicenseExpiresOn) : "not captured"], ["Birth month", storedDateToYearMonth(rider.dateOfBirth) || "not captured"] ]), riderProfileDetailSection("Vehicle", [ ["Vehicle", vehicle], ["Body type", carBodyTypeLabel(rider.carBodyType)], ["Vehicle designation", riderVehicleDesignationLabel(metadata.vehicleDesignation)], ["Plate number", rider.registration || "not captured"], ["Insurance expires", rider.insuranceExpiresOn ? formatDate(rider.insuranceExpiresOn) : "not captured"] ]), riderProfileDetailSection("Service preferences", [ ["Service state", `${rider.city || "not set"}, ${rider.country || "not set"}`], ["Compliance", riderComplianceStatusText(rider)], ["Navigation", metadata.navigationPreference === "waze" ? "Waze" : "Google Maps"] ]), riderProfileRatingSection(rider) ].join(""); renderRiderRatingsPanel(rider); renderRiderComplianceRenewalForm(rider); } function renderRiderComplianceRenewalForm(rider = currentRiderRecord()) { if (!els.riderComplianceRenewalForm) return; const canRenew = Boolean(rider && !rider.needsApplication && rider.status !== "profile only" && rider.status !== "declined"); els.riderComplianceRenewalForm.hidden = !canRenew; if (!canRenew || !els.riderComplianceRenewalStatus) return; if (rider.status === "suspended") { els.riderComplianceRenewalStatus.textContent = "Upload current documents. Admin approval restores rider service after review."; return; } if (!riderComplianceReady(rider)) { els.riderComplianceRenewalStatus.textContent = "Upload renewed documents. Rider service remains blocked until the expired item is renewed and reviewed."; return; } const upcoming = riderUpcomingComplianceItems(rider); els.riderComplianceRenewalStatus.textContent = upcoming.length ? "Renew before expiration to avoid automatic suspension." : "Renew documents any time before expiration. Uploading a new document is required when changing an expiration date."; } function complianceRenewalPair(dateValue, file, label) { const hasDate = Boolean(String(dateValue ?? "").trim()); const hasFile = Boolean(file); if (!hasDate && !hasFile) return null; if (!hasDate) throw new Error(`Enter the new ${label} expiration date.`); if (!hasFile) throw new Error(`Upload the new ${label} document.`); const days = daysUntilDate(dateValue); if (days === null || days < 0) throw new Error(`Enter a current ${label} expiration date.`); return { dateValue, file }; } async function submitRiderComplianceRenewal(event) { event.preventDefault(); const rider = currentRiderRecord(); if (!rider) { if (els.riderComplianceRenewalStatus) els.riderComplianceRenewalStatus.textContent = "Sign in as a rider before uploading renewed documents."; return; } try { const license = complianceRenewalPair( els.riderRenewLicenseExpiresOn?.value, els.riderRenewLicenseDocument?.files?.[0] ?? null, "driver's license" ); const insurance = complianceRenewalPair( els.riderRenewInsuranceExpiresOn?.value, els.riderRenewInsuranceDocument?.files?.[0] ?? null, "insurance" ); if (!license && !insurance) { els.riderComplianceRenewalStatus.textContent = "Choose at least one renewed document and expiration date."; return; } setButtonBusy(els.riderSubmitComplianceRenewal, true); els.riderComplianceRenewalStatus.textContent = "Uploading renewed documents..."; const result = await submitRiderComplianceRenewalToSupabase(rider, { driverLicenseExpiresOn: license?.dateValue ?? null, driverLicenseFile: license?.file ?? null, insuranceExpiresOn: insurance?.dateValue ?? null, insuranceFile: insurance?.file ?? null }); const application = result?.application ?? {}; const documents = { ...riderDocuments(rider), ...(result?.documents ?? {}) }; const nextStatus = application.status ?? (rider.status === "suspended" ? "pending" : rider.status); const updatedRider = { ...rider, status: nextStatus, reviewNote: application.review_note ?? (nextStatus === "pending" ? "Renewed compliance documents submitted for admin review." : rider.reviewNote ?? ""), driverLicenseExpiresOn: application.driver_license_expires_on ?? license?.dateValue ?? rider.driverLicenseExpiresOn, insuranceExpiresOn: application.insurance_expires_on ?? insurance?.dateValue ?? rider.insuranceExpiresOn, complianceSuspendedAt: application.compliance_suspended_at ?? (nextStatus === "approved" ? null : rider.complianceSuspendedAt ?? null), complianceSuspensionReason: application.compliance_suspension_reason ?? (nextStatus === "approved" ? null : rider.complianceSuspensionReason ?? null), documentName: application.document_path ?? riderDocumentPayload(documents), documents: { ...documents, vehicleDesignation: normalizeRiderVehicleDesignation(rider.vehicleDesignation, rider.carBodyType), navigationPreference: riderNavigationPreference(rider) }, driverLicenseDocumentPath: documents.driverLicense, insuranceDocumentPath: documents.insurance }; saveCurrentRiderRecord(updatedRider); if (els.riderRenewLicenseExpiresOn) els.riderRenewLicenseExpiresOn.value = ""; if (els.riderRenewLicenseDocument) els.riderRenewLicenseDocument.value = ""; if (els.riderRenewInsuranceExpiresOn) els.riderRenewInsuranceExpiresOn.value = ""; if (els.riderRenewInsuranceDocument) els.riderRenewInsuranceDocument.value = ""; saveState(); renderAll(); els.riderComplianceRenewalStatus.textContent = nextStatus === "approved" ? "Renewed documents saved. Rider service remains active while documents are current." : "Renewed documents submitted. Admin review is required before rider service resumes."; } catch (error) { if (els.riderComplianceRenewalStatus) els.riderComplianceRenewalStatus.textContent = error.message; } finally { setButtonBusy(els.riderSubmitComplianceRenewal, false); } } function renderRiderBackgroundCheckPanel() { if (!els.riderBackgroundCheckPanel) return; const signedIn = Boolean(state.sessions.rider && state.rider); const onChecksPage = typeof riderWorkspacePage !== "function" || riderWorkspacePage() === "checks"; els.riderBackgroundCheckPanel.hidden = !signedIn || !onChecksPage; if (!signedIn) return; const rider = currentRiderRecord() ?? state.rider; const relaxedBackground = riderBackgroundCheckRelaxedForTesting(); const backgroundUnlocked = riderCanStartBackgroundCheckFromStatus(rider) || rider?.status === "approved"; const manualReview = riderManualBackgroundReviewMode(); if (manualReview && !["background_pending", "approved"].includes(rider?.status)) { els.riderBackgroundCheckPanel.hidden = true; return; } if (!backgroundUnlocked) { if (!manualReview) { els.riderBackgroundCheckPanel.hidden = true; return; } } const latest = latestRiderSelfBackgroundCheck(rider); const provider = latest?.provider || rider?.backgroundCheckProvider || appConfig.backgroundCheckProvider || "background-check provider"; const status = normalizedRiderBackgroundStatus(rider); const decision = normalizedRiderBackgroundDecision(rider); const hasConsent = Boolean(rider?.backgroundCheckConsentAt); const alreadyStarted = !["", "not requested", "not-requested", "not_requested"].includes(status); const finalDecision = ["clear", "consider", "adverse"].includes(decision) || ["clear", "review", "adverse", "failed"].includes(status); const canStart = hasSupabaseRuntime() && hasConsent && !alreadyStarted && riderCanStartBackgroundCheckFromStatus(rider); if (els.riderBackgroundCheckBadge) { els.riderBackgroundCheckBadge.textContent = `${status || "not requested"} / ${decision || "pending"}`; } if (els.riderBackgroundCheckSummary) { if (manualReview) { els.riderBackgroundCheckSummary.textContent = rider?.status === "approved" ? "Manual Waka admin review is complete for this Cameroon rider account." : "Waka admin is reviewing local documents, vehicle details, permit, emergency contact, and safety information. No Checkr/provider step is required in this Cameroon MVP."; } else if (!hasConsent) { els.riderBackgroundCheckSummary.textContent = "Submit the rider application with background-check consent before provider screening can start."; } else if (decision === "clear") { els.riderBackgroundCheckSummary.textContent = "Provider screening is clear. Admin can use this result with your documents when deciding eligibility."; } else if (decision === "consider") { els.riderBackgroundCheckSummary.textContent = "Provider screening needs admin review. Admin will consider the report with your documents before deciding eligibility."; } else if (decision === "adverse" || status === "adverse") { els.riderBackgroundCheckSummary.textContent = "Provider screening returned an adverse status. Contact Waka support for next steps before eligibility can be approved."; } else if (alreadyStarted) { els.riderBackgroundCheckSummary.textContent = "Provider screening has started. Watch for provider email, SMS, or hosted instructions and complete any requested steps."; } else if (rider?.status === "pending" && relaxedBackground) { els.riderBackgroundCheckSummary.textContent = `Testing mode is on for this staging account. Select Start ${provider} background check to record the relaxed Checkr step while admin review is pending. Production still requires the real provider result.`; } else { els.riderBackgroundCheckSummary.textContent = `Admin has unlocked ${provider} screening. Select Start background check, then follow the provider email, SMS, or hosted instructions. Waka pays for this required screening.`; } } if (els.startRiderBackgroundCheck) { els.startRiderBackgroundCheck.disabled = manualReview || !canStart; els.startRiderBackgroundCheck.hidden = manualReview || finalDecision; els.startRiderBackgroundCheck.textContent = manualReview ? "Manual review" : alreadyStarted ? "Background check started" : `Start ${provider} background check`; } if (els.riderBackgroundCheckStatus && !els.riderBackgroundCheckStatus.dataset.busy) { els.riderBackgroundCheckStatus.textContent = manualReview ? "Waka Cameroon uses manual admin review for the MVP. Admin can approve, decline, or request corrections." : !hasSupabaseRuntime() ? "Background checks require the Supabase production runtime." : !hasConsent ? "Consent is captured during rider application submission." : alreadyStarted ? `Latest ${provider} status: ${status}; decision: ${decision}.` : rider?.status === "pending" && relaxedBackground ? `Ready to start the relaxed ${provider} testing step from Eligibility.` : rider?.status === "background_pending" ? `Ready to start ${provider} screening. Instructions are also sent by email when delivery is configured.` : "Admin has not unlocked provider screening yet."; } if (!els.riderBackgroundCheckList) return; els.riderBackgroundCheckList.innerHTML = ""; const records = riderSelfBackgroundCheckRecords(rider?.id); if (!records.length) { const item = document.createElement("article"); item.className = "notice-item"; item.innerHTML = ` ${escapeHtml(manualReview ? "Manual review" : `${provider} next steps`)}

${manualReview ? "No provider action is needed from the rider. Waka admin reviews the submitted Cameroon documents and will send corrections or approval." : rider?.status === "pending" && relaxedBackground ? `Select Start ${escapeHtml(provider)} background check. Staging records a relaxed Checkr testing step for admin review without using production provider billing.` : `Select Start ${escapeHtml(provider)} background check. If a provider page opens, complete it there. If the provider sends email or SMS instructions, complete those steps and return to Waka. Admin will see the returned status here and in Rider approvals.`}

${manualReview ? "Manual review is tracked by admin." : "No provider result has been returned yet."} `; els.riderBackgroundCheckList.append(item); return; } records.slice(0, 3).forEach((record) => { const item = document.createElement("article"); item.className = "notice-item"; item.innerHTML = ` ${escapeHtml(record.provider || provider)} - ${escapeHtml(record.status || "requested")}

${escapeHtml(record.summary || `Decision: ${record.decision || "pending"}.`)}

${record.completedAt ? `Completed ${formatDateTime(record.completedAt)}` : `Started ${formatDateTime(record.createdAt)}`} `; els.riderBackgroundCheckList.append(item); }); } function riderTaxDocumentAccessAction(taxDocument) { const providerUrl = normalizeHttpsUrl(taxDocument.providerDocumentUrl); if (providerUrl) { return `Open provider form`; } return storageReviewButton(`${taxDocument.documentType} ${taxDocument.taxYear}`, appConfig.buckets.riderDocuments, taxDocument.storagePath); } function riderTaxDocumentDeliveryText(taxDocument) { const delivery = taxDocument.deliveryMethod ? String(taxDocument.deliveryMethod).replace(/_/g, " ") : "provider portal"; const filing = taxDocument.filingStatus ? ` Filing: ${String(taxDocument.filingStatus).replace(/_/g, " ")}.` : ""; const reference = taxDocument.providerDocumentId ? ` Reference: ${taxDocument.providerDocumentId}.` : ""; return `Provider: ${taxDocument.provider || "Waka"}. Delivery: ${delivery}.${filing}${reference}`; } function renderRiderTaxDocuments() { if (!els.riderTaxPanel || !els.riderTaxList) return; const signedIn = Boolean(state.sessions.rider && state.rider); const onChecksPage = typeof riderWorkspacePage !== "function" || riderWorkspacePage() === "checks"; const rider = currentRiderRecord() ?? state.rider; const riderApproved = rider?.status === "approved"; els.riderTaxPanel.hidden = !signedIn || !onChecksPage || !riderApproved; els.riderTaxList.innerHTML = ""; if (!signedIn || !onChecksPage || !riderApproved) return; const taxIdentity = taxIdentityForRider(rider?.id); const provider = appConfig.taxOnboardingProvider || "tax provider"; if (els.riderTaxOnboardingSummary) { els.riderTaxOnboardingSummary.textContent = taxIdentity ? `${taxIdentityStatusText(taxIdentity)} Provider reference: ${taxIdentity.providerSubjectId || "provider-held"}.` : `Use ${provider} hosted onboarding for tax setup. Waka does not collect or store raw SSN, EIN, ITIN, W-9, or full TIN values.`; } if (els.startRiderTaxOnboarding) { els.startRiderTaxOnboarding.disabled = !riderApproved || !hasSupabaseRuntime(); els.startRiderTaxOnboarding.textContent = taxIdentity?.status === "verified" ? "Update hosted tax setup" : "Open hosted tax setup"; } if (els.riderTaxOnboardingStatus && !els.riderTaxOnboardingStatus.dataset.busy) { els.riderTaxOnboardingStatus.textContent = riderApproved ? "Complete tax setup only inside the provider-hosted flow." : "Tax onboarding opens after admin approval, before payouts or annual tax documents."; } const documents = taxDocumentsForRider(state.rider.id); if (!documents.length) { els.riderTaxList.append(emptyState("No tax documents are available yet. Annual tax documents will appear here when issued by Waka or its tax provider.")); return; } documents.forEach((taxDocument) => { const item = document.createElement("article"); item.className = "notice-item"; const openButton = riderTaxDocumentAccessAction(taxDocument); item.innerHTML = ` ${escapeHtml(taxDocument.documentType)} - ${escapeHtml(String(taxDocument.taxYear))}

Status: ${escapeHtml(taxDocument.status)}. ${escapeHtml(riderTaxDocumentDeliveryText(taxDocument))}

${taxDocument.availableAt ? `Available ${formatDate(taxDocument.availableAt)}` : taxDocument.issuedAt ? `Issued ${formatDate(taxDocument.issuedAt)}` : "Not available yet"} ${openButton ? `
${openButton}
` : ""} `; els.riderTaxList.append(item); }); wireStorageReviewButtons(els.riderTaxList); } function riderEarningsRouteLabel(request) { if (!request) return "Completed ride"; const pickup = requestPickupDisplayText(request, "Pickup"); const destination = request.destinationFormattedAddress || request.destination || request.destinationArea || "Destination"; return `${pickup} -> ${destination}`; } function riderEarningRecordTimestamp(record) { return record.processedAt || record.completedAt || record.createdAt || record.updatedAt; } function riderCompletedTelemetryMileageMiles(requestId, riderId) { if (!requestId || !riderId) return null; const miles = (state.riderCompletedMileageSegments ?? []) .filter((segment) => segment.requestId === requestId && segment.riderId === riderId && segment.status === "closed") .reduce((total, segment) => total + Number(segment.distanceMiles || 0), 0); return Number.isFinite(miles) && miles > 0 ? miles : null; } function riderCompletedWakaMileageMiles(request, settlement = null, riderId = null) { if (!request || request.status !== "completed") return null; const resolvedRiderId = riderId || settlement?.riderId || selectedRiderIdForRequest(request); const telemetryMiles = riderCompletedTelemetryMileageMiles(request.id, resolvedRiderId); if (telemetryMiles != null) return telemetryMiles; const miles = Number(request.estimatedDistanceMiles ?? settlement?.distanceMiles); return Number.isFinite(miles) && miles > 0 ? miles : null; } function formatCompletedWakaMileage(value, fallback = "0.0 mi") { const miles = Number(value); if (!Number.isFinite(miles) || miles <= 0) return fallback; return `${miles.toFixed(miles < 10 ? 1 : 0)} mi`; } function riderCompletedRideEarningRecords(rider = currentRiderRecord()) { const riderId = rider?.id; if (!riderId) return []; const requests = state.requests.filter((request) => selectedRiderIdForRequest(request) === riderId && request.status === "completed"); const requestMap = new Map(state.requests.map((request) => [request.id, request])); const tipsByRequest = new Map(); rideTipRecords() .filter((tip) => tip.riderId === riderId && !["failed", "refunded"].includes(tip.status)) .forEach((tip) => { const current = tipsByRequest.get(tip.requestId) ?? { amount: 0, payout: 0, fee: 0, count: 0 }; current.amount += Number(tip.amount || 0); current.payout += Number(tip.riderPayoutAmount || 0); current.fee += Number(tip.stripeFeeAmount || 0); current.count += 1; tipsByRequest.set(tip.requestId, current); }); const settledRequestIds = new Set(); const settlementRecords = rideSettlementRecords() .filter((settlement) => settlement.riderId === riderId) .map((settlement) => { settledRequestIds.add(settlement.requestId); const request = requestMap.get(settlement.requestId); const tip = tipsByRequest.get(settlement.requestId) ?? { amount: 0, payout: 0, fee: 0, count: 0 }; const completedWakaMiles = riderCompletedWakaMileageMiles(request, settlement, riderId); return { id: `settlement-${settlement.id}`, requestId: settlement.requestId, route: riderEarningsRouteLabel(request), country: request?.country || rider.country, completedWakaMiles, fareAmount: Number(settlement.fareAmount || 0), stripeFeeAmount: Number(settlement.stripeFeeAmount || 0) + tip.fee, riderPayoutAmount: Number(settlement.riderPayoutAmount || 0), tipPayoutAmount: tip.payout, totalEarned: Number(settlement.riderPayoutAmount || 0) + tip.payout, tipAmount: tip.amount, tipCount: tip.count, status: settlement.status || "pending_provider_payout", providerReference: settlement.providerTransferReference || settlement.providerReference || "", failureReason: settlement.failureReason || "", completedAt: request?.completedAt, processedAt: settlement.processedAt, createdAt: settlement.createdAt || request?.completedAt, updatedAt: settlement.updatedAt }; }); const fallbackRecords = requests .filter((request) => !settledRequestIds.has(request.id)) .map((request) => { const breakdown = rideFinancialBreakdown(request, totalTipAmountForRequest(request.id)); return { id: `completed-${request.id}`, requestId: request.id, route: riderEarningsRouteLabel(request), country: request.country || rider.country, completedWakaMiles: riderCompletedWakaMileageMiles(request, null, riderId), fareAmount: centsToDollars(breakdown.fareCents), stripeFeeAmount: centsToDollars(breakdown.stripeFeeCents), riderPayoutAmount: centsToDollars(breakdown.riderPayoutCents), tipPayoutAmount: 0, totalEarned: centsToDollars(breakdown.riderPayoutCents), tipAmount: centsToDollars(breakdown.tipCents), tipCount: breakdown.tipCents > 0 ? 1 : 0, status: "pending_provider_payout", providerReference: "", failureReason: "", completedAt: request.completedAt, processedAt: null, createdAt: request.completedAt || request.createdAt, updatedAt: request.updatedAt }; }); return [...settlementRecords, ...fallbackRecords] .sort((a, b) => new Date(riderEarningRecordTimestamp(b) || 0) - new Date(riderEarningRecordTimestamp(a) || 0)); } function startOfLocalDay(value = new Date()) { const date = new Date(value); date.setHours(0, 0, 0, 0); return date; } function startOfLocalWeek(value = new Date()) { const date = startOfLocalDay(value); date.setDate(date.getDate() - date.getDay()); return date; } function periodEarningsTotal(records, startDate) { const startMs = startDate.getTime(); return records .filter((record) => new Date(riderEarningRecordTimestamp(record) || 0).getTime() >= startMs) .reduce((total, record) => total + Number(record.totalEarned || 0), 0); } function renderRiderEarnings() { if (!els.riderEarningsPanel || !els.riderEarningsSummary || !els.riderEarningsList) return; const rider = currentRiderRecord(); const signedIn = Boolean(state.sessions.rider && rider); const onEarningsPage = typeof riderWorkspacePage !== "function" || riderWorkspacePage() === "earnings"; els.riderEarningsPanel.hidden = !signedIn || !onEarningsPage; els.riderEarningsSummary.innerHTML = ""; els.riderEarningsList.innerHTML = ""; if (!signedIn || !onEarningsPage) { if (els.riderEarningsCount) els.riderEarningsCount.textContent = "0 rides"; return; } const records = riderCompletedRideEarningRecords(rider); const now = new Date(); const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); const startOfYear = new Date(now.getFullYear(), 0, 1); const completedWakaMiles = records.reduce((total, record) => total + (Number(record.completedWakaMiles) || 0), 0); const summary = [ ["Today", periodEarningsTotal(records, startOfLocalDay(now)), "Completed rides paid or pending today"], ["This week", periodEarningsTotal(records, startOfLocalWeek(now)), "Sunday through today"], ["This month", periodEarningsTotal(records, startOfMonth), "Month-to-date rider earnings"], ["This year", periodEarningsTotal(records, startOfYear), "Year-to-date rider earnings"] ]; if (els.riderEarningsCount) els.riderEarningsCount.textContent = `${records.length} completed ride${records.length === 1 ? "" : "s"}`; els.riderEarningsSummary.innerHTML = `
Completed Waka miles ${escapeHtml(formatCompletedWakaMileage(completedWakaMiles))} Only completed Waka rides are counted.
` + summary.map(([label, amount, detail]) => `
${escapeHtml(label)} ${escapeHtml(formatMoney(amount, rider.country))} ${escapeHtml(detail)}
`).join(""); if (!records.length) { els.riderEarningsList.append(emptyState("Completed rides and rider payouts will appear here after trips are finished.")); return; } records.slice(0, 20).forEach((record) => { const item = document.createElement("article"); item.className = `market-card platform-issue-${record.status === "failed" ? "critical" : record.status === "paid_out" ? "info" : "warning"}`; const tipText = record.tipPayoutAmount > 0 ? ` Tip payout ${formatMoney(record.tipPayoutAmount, record.country)}.` : ""; item.innerHTML = `
Ride earnings - ${escapeHtml(record.status.replace(/_/g, " "))} ${escapeHtml(formatMoney(record.totalEarned, record.country))} earned ${escapeHtml(formatDateTime(riderEarningRecordTimestamp(record)))}

${escapeHtml(record.route)}

Actual Waka mileage: ${escapeHtml(formatCompletedWakaMileage(record.completedWakaMiles, "Mileage pending"))}

${directRidePaymentMode() ? `Fare ${escapeHtml(formatMoney(record.fareAmount, record.country))}; paid directly by passenger; wallet commission review handled by Waka operations.${tipText}` : `Fare ${escapeHtml(formatMoney(record.fareAmount, record.country))}; Stripe fee ${escapeHtml(formatMoney(record.stripeFeeAmount, record.country))}; ride payout ${escapeHtml(formatMoney(record.riderPayoutAmount, record.country))}.${tipText}`}

${[ `Ride ID: ${record.requestId}`, record.completedWakaMiles != null ? `Waka miles: ${formatCompletedWakaMileage(record.completedWakaMiles)}` : "Waka miles pending", directRidePaymentMode() ? (record.providerReference ? `Wallet ref: ${record.providerReference}` : "Direct payment recorded") : (record.providerReference ? `Stripe ref: ${record.providerReference}` : "Stripe payout pending"), record.failureReason ? `Issue: ${record.failureReason}` : "" ].filter(Boolean).map(chip).join("")}
`; els.riderEarningsList.append(item); }); } function riderPickupRadiusForEtaMinutes(minutes, rider = currentRiderRecord()) { const speedKmh = riderPickupEtaSpeedKmh[rider?.vehicle] ?? riderPickupEtaSpeedKmh.car; return (Number(minutes) * speedKmh) / (riderPickupEtaRoadFactor * 60); } function riderServiceRadius(rider = currentRiderRecord(), request = null) { const baseRadius = riderProximityLimit[rider?.vehicle] ?? riderProximityLimit.car; if (!isScheduledRequest(request)) return baseRadius; return Math.max(baseRadius, riderPickupRadiusForEtaMinutes(scheduledRiderPickupMaxEtaMinutes, rider)); } function riderCanActivateAvailability(rider = currentRiderRecord()) { return Boolean(rider && hasSignedIn("rider") && rider.status === "approved" && isSubscriptionActive(rider) && riderComplianceReady(rider)); } function riderRequestSetupGaps(rider = currentRiderRecord()) { if (!rider) return []; return [ paymentAccountReady("rider", rider) ? null : "payout account" ].filter(Boolean); } function riderAvailabilityStatusText(rider = currentRiderRecord()) { if (rider && !riderComplianceReady(rider)) return riderComplianceStatusText(rider); if (!riderCanActivateAvailability(rider)) return riderWorkspaceStatusMessage(rider); const currentGps = riderCurrentFreshGps(rider); const activated = state.riderAvailabilityActivated === true; const blockingRide = riderBlockingImmediateRide(rider); const setupGaps = riderRequestSetupGaps(rider); const setupNote = setupGaps.length ? ` Complete ${setupGaps.join(" and ")} before ride requests appear.` : ""; if (blockingRide) { return blockingRide.status === "in_progress" ? `Online for this active ride. New requests resume when you are about 7 minutes from drop-off.${setupNote}` : `Online for this active ride. New immediate requests are paused until the ride starts and you are about 7 minutes from drop-off.${setupNote}`; } if (currentGps) { return `Online and available. ${gpsStatusLabel(currentGps, "Location active")} is used privately to show only nearby requests.${setupNote}`; } if (activated) { return `Activated. Waka is waiting for a fresh location update before nearby ride requests appear.${setupNote}`; } return `Offline. Activate when you are ready to receive nearby ride requests.${setupNote}`; } function riderServiceAreaSummary(rider = currentRiderRecord()) { if (!rider) return "Nearby requests use your availability and pickup radius."; const regions = riderDailyDestinationRegions(rider); const destinations = regions.length ? ` Optional preferred destinations saved: ${regions.join(", ")}.` : ""; return `Car requests use your live location while you are online: immediate pickups within about ${riderPickupMaxEtaMinutes} minutes, and scheduled rides within about ${scheduledRiderPickupMaxEtaMinutes} minutes in the active launch market.${destinations} ${riderAvailabilityStatusText(rider)}`; } function renderRiderDailyRegionStatus(rider = currentRiderRecord()) { if (!els.riderDailyRegionStatus) return; if (!rider) { els.riderDailyRegionStatus.textContent = "Destination preferences are on the Destination page. Nearby requests use your active location."; return; } if (rider.status !== "approved") { els.riderDailyRegionStatus.textContent = riderWorkspaceStatusMessage(rider); return; } const regions = riderDailyDestinationRegions(rider); const remaining = riderDailyRegionUpdatesRemaining(rider); els.riderDailyRegionStatus.textContent = regions.length ? `Optional preferred destinations saved: ${regions.join(", ")}. Showing all nearby pickups within the pickup radius. ${remaining} update${remaining === 1 ? "" : "s"} remaining today.` : "Showing all nearby pickups within the pickup radius."; } function updateRiderAreas() { const country = els.riderCountry.value || state.rider?.country || selectedPassengerCountry(); const city = els.riderCity.value || cityNames(country)[0]; const selectedCity = cityNames(country).includes(city) ? city : cityNames(country)[0]; populateSelect(els.riderCity, cityNames(country), selectedCity); populateSelect(els.riderArea, areas(country, selectedCity).map((area) => area.name), state.rider?.area); } function updateRiderActiveAreas() { const country = els.riderActiveCountry.value || state.rider?.country || selectedPassengerCountry(); const city = els.riderActiveCity.value || cityNames(country)[0]; const selectedCity = cityNames(country).includes(city) ? city : cityNames(country)[0]; populateSelect(els.riderActiveCity, cityNames(country), selectedCity); populateSelect(els.riderActiveArea, areas(country, selectedCity).map((area) => area.name), state.rider?.area ?? areas(country, selectedCity)[0]?.name); populateRiderDailyRegionOptions(country, selectedCity); } function updateRiderCityOptions() { const country = els.riderCountry.value; populateSelect(els.riderCity, cityNames(country), cityNames(country)[0]); populateSelect(els.riderArea, areas(country, els.riderCity.value).map((area) => area.name), areas(country, els.riderCity.value)[0]?.name); } function updateRiderActiveCityOptions() { const country = els.riderActiveCountry.value; populateSelect(els.riderActiveCity, cityNames(country), cityNames(country)[0]); populateSelect(els.riderActiveArea, areas(country, els.riderActiveCity.value).map((area) => area.name), areas(country, els.riderActiveCity.value)[0]?.name); populateRiderDailyRegionOptions(country, els.riderActiveCity.value); } function renderRiderStatus() { ensureRiderScreenWakeLockEvents(); renderRiderBackgroundCheckPanel(); renderRiderProfileSummary(); if (!state.rider) { els.riderStatus.textContent = "No rider application saved yet."; els.subscriptionText.textContent = directRidePaymentMode() ? `Approved Cameroon riders get a ${trialDays}-day commission-free period after the first completed ride, then wallet/commission review applies.` : `Approved riders get a ${trialDays}-day free trial, then choose $160 monthly for 30 days or $50 weekly for 7 days.`; els.subscriptionPaymentStatus.textContent = directRidePaymentMode() ? "Create and approve a rider account before Waka operations reviews wallet readiness." : "Create and approve a rider account before opening Waka Rider Access checkout."; els.paySubscription.disabled = true; renderRiderEarnings(); renderRiderAvailabilityControls(null); return; } const rider = state.riders.find((item) => item.id === state.rider.id) ?? state.rider; const statusText = { pending: "waiting for admin review", background_pending: riderManualBackgroundReviewMode() ? "in final manual admin review" : "invited to complete the Checkr background check", needs_correction: "needs corrections before resubmission", approved: "approved", declined: "declined by admin", suspended: "suspended" }[rider.status]; els.riderStatus.textContent = `${rider.name} is ${statusText}. Vehicle: ${rider.carYear || ""} ${rider.carMake || ""} ${rider.carModel || "car"}. Plate: ${rider.registration}.`; if (rider.status !== "approved") { els.subscriptionText.textContent = riderWorkspaceStatusMessage(rider); els.subscriptionPaymentStatus.textContent = directRidePaymentMode() ? "Rider wallet review opens only after admin approval." : "Rider plan checkout opens only after admin approval."; els.paySubscription.disabled = true; renderRiderEarnings(); renderRiderAvailabilityControls(rider); return; } const end = riderAccessEnd(rider); const remaining = daysUntil(end); const label = riderAccessLabel(rider); if (isSubscriptionActive(rider)) { const accessHealthy = remaining > subscriptionRenewalNoticeDays; const accessName = label === "free trial" ? (directRidePaymentMode() ? "Free period" : "Free trial") : directRidePaymentMode() ? "Rider wallet review" : "Rider access"; if (accessHealthy) { els.subscriptionText.textContent = `${accessName} active until ${formatDate(end)}. ${riderPlanSummary()} ${directRidePaymentMode() ? "Waka operations monitors wallet status." : "Payment choices open 3 days before expiry."}`; } else { const reminder = remaining <= subscriptionRenewalNoticeDays ? (directRidePaymentMode() ? " Waka operations should review wallet/commission status before access changes." : ` Renewal is due soon; choose manual payment or automatic renewal so paid access starts after this period ends.`) : (directRidePaymentMode() ? ` Wallet review activates when ${subscriptionRenewalNoticeDays} days or fewer remain.` : ` Checkout opens when ${subscriptionRenewalNoticeDays} days or fewer remain.`); els.subscriptionText.textContent = `Rider access renewal: ${pluralDays(remaining)} left, until ${formatDate(end)}. ${riderPlanSummary()}${reminder}`; } els.subscriptionPaymentStatus.textContent = accessHealthy ? (directRidePaymentMode() ? "No rider wallet action is needed until this access period is close to ending." : "No payment action needed until this access period is close to ending.") : (directRidePaymentMode() ? "Wallet reminder: Waka operations should confirm commission readiness." : "Renewal reminder: pay manually or keep your provider payment method active for automatic renewal."); els.paySubscription.disabled = accessHealthy; } else { els.subscriptionText.textContent = directRidePaymentMode() ? `Rider access is inactive${end ? ` since ${formatDate(end)}` : ""}. ${riderPlanSummary()} Contact Waka operations to continue receiving and responding to ride requests.` : `Rider access is inactive${end ? ` since ${formatDate(end)}` : ""}. ${riderPlanSummary()} Open checkout to continue receiving and responding to ride requests.`; els.subscriptionPaymentStatus.textContent = directRidePaymentMode() ? "Waka operations must confirm wallet and commission readiness." : "Open Waka Rider Access checkout. Paid access starts after the free trial period or the current access period ends."; els.paySubscription.disabled = false; } renderRiderEarnings(); renderRiderAvailabilityControls(rider); scheduleRiderSubscriptionReminderCheck(rider); } function renderRiderAvailabilityControls(rider = currentRiderRecord()) { if (!els.riderGpsStatus || !els.captureRiderGps || !els.clearRiderGps) return; const canActivate = riderCanActivateAvailability(rider); const activated = canActivate && state.riderAvailabilityActivated === true; if (!els.riderGpsStatus.dataset.busy) { els.riderGpsStatus.textContent = canActivate ? riderAvailabilityStatusText(rider) : riderWorkspaceStatusMessage(rider); } els.captureRiderGps.textContent = "Activate"; els.clearRiderGps.textContent = "Deactivate"; els.captureRiderGps.disabled = !canActivate; els.clearRiderGps.disabled = !canActivate; els.captureRiderGps.setAttribute("aria-pressed", String(activated)); } function subscriptionReminderFunctionName() { return String(appConfig.subscriptionReminderFunctionName || "subscription-reminders").trim() || "subscription-reminders"; } function subscriptionReminderDueForRider(rider) { if (!rider || rider.status !== "approved") return false; const end = riderAccessEnd(rider); if (!end) return false; return daysUntil(end) <= subscriptionRenewalNoticeDays; } function subscriptionReminderLocalKey(rider) { return `${subscriptionReminderCheckStorageKey}:${rider.id}:${riderAccessEnd(rider) || "none"}:${localDateKey()}`; } async function sendRiderSubscriptionReminderCheck() { const token = await currentSupabaseAccessToken(); if (!token) throw new Error("Sign in before checking subscription reminders."); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/${subscriptionReminderFunctionName()}`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify({ mode: "self" }) }), "Checking subscription reminders", optionalSupabaseRequestTimeoutMs ); const payload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(payload?.error || "Subscription reminder check failed."); return payload; } function scheduleRiderSubscriptionReminderCheck(rider = currentRiderRecord()) { if (!hasSupabaseRuntime() || !hasSignedIn("rider") || !subscriptionReminderDueForRider(rider)) return; const key = subscriptionReminderLocalKey(rider); try { if (localStorage.getItem(key)) return; } catch { // Storage can be unavailable; the reminder can still be checked once per render cycle. } if (subscriptionReminderChecksInFlight.has(key)) return; subscriptionReminderChecksInFlight.add(key); window.setTimeout(async () => { try { const result = await sendRiderSubscriptionReminderCheck(); try { localStorage.setItem(key, JSON.stringify({ checkedAt: new Date().toISOString(), created: result?.created ?? 0 })); } catch { // Non-critical; reminders are idempotent server-side. } await refreshAccountNotificationsFromSupabase("rider", { force: true }); renderAccountNotices("rider"); } catch (error) { logClientWarning("Rider subscription reminder check failed.", error); } finally { subscriptionReminderChecksInFlight.delete(key); } }, 0); } async function startRiderBackgroundCheck() { const status = els.riderBackgroundCheckStatus; const rider = currentRiderRecord() ?? state.rider; if (!rider || !state.sessions.rider) { if (status) status.textContent = "Sign in as a rider before starting the background check."; return; } if (!rider.backgroundCheckConsentAt) { if (status) status.textContent = "Submit the rider application with background-check consent before starting provider screening."; return; } if (riderManualBackgroundReviewMode()) { if (status) status.textContent = "Waka Cameroon uses manual admin review for this MVP. No provider background-check flow is started from the rider app."; return; } if (!riderCanStartBackgroundCheckFromStatus(rider)) { if (status) status.textContent = riderBackgroundCheckRelaxedForTesting() ? "Eligibility must be pending admin review or Checkr-invited before starting the testing background-check step." : "Admin must review the application and invite you to Checkr before provider screening starts."; return; } if (!hasSupabaseRuntime()) { if (status) status.textContent = "Background checks require the Supabase production runtime."; return; } try { setButtonBusy(els.startRiderBackgroundCheck, true); if (status) { status.dataset.busy = "true"; status.textContent = `Starting ${appConfig.backgroundCheckProvider || "provider"} background check...`; } const payload = { riderId: rider.id }; let responsePayload = null; if (supabaseClient?.functions?.invoke) { const { data, error } = await withSupabaseTimeout( supabaseClient.functions.invoke("background-check-start", { body: payload }), "Starting the provider background check", optionalSupabaseRequestTimeoutMs ); if (error) throw error; responsePayload = data; } else { const token = await currentSupabaseAccessToken(); if (!token) throw new Error("Sign in before starting the background check."); const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/background-check-start`, { method: "POST", headers: { apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify(payload) }), "Starting the provider background check", optionalSupabaseRequestTimeoutMs ); responsePayload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(responsePayload?.error || "Background-check Edge Function failed."); } const savedCheck = responsePayload?.check ? mapRiderBackgroundCheckFromDatabase(responsePayload.check) : null; if (savedCheck?.id) { state.backgroundChecks = upsertById(state.backgroundChecks.filter((item) => item.id !== savedCheck.id), savedCheck); const riderPatch = { backgroundCheckStatus: savedCheck.status, backgroundCheckDecision: savedCheck.decision, backgroundCheckProvider: savedCheck.provider, backgroundCheckSummary: savedCheck.summary }; state.rider = { ...state.rider, ...riderPatch }; state.riders = state.riders.map((item) => item.id === rider.id ? { ...item, ...riderPatch } : item); saveState(); } if (responsePayload?.url) { const opened = window.open(responsePayload.url, "_blank", "noopener,noreferrer"); if (status) { status.textContent = opened ? "Background check opened in a separate tab. Complete it there, then return to Waka." : "Popup was blocked. Copy the provider link from your email or allow popups for Waka, then start again."; } return; } if (status) status.textContent = responsePayload?.relaxed ? "Relaxed Checkr testing step recorded. Stay on Eligibility to monitor admin review." : "Background check started. Complete any provider email, SMS, or hosted instructions. Admin will see the result when the provider returns it to Waka."; renderAll(); } catch (error) { const providerHint = /edge function|failed to send|provider secrets|not configured|function/i.test(error.message) ? " Waka still has your admin invitation; contact Waka support if no provider email or hosted page arrives." : ""; if (status) status.textContent = `Could not start background check: ${error.message}.${providerHint}`; } finally { if (status) delete status.dataset.busy; setButtonBusy(els.startRiderBackgroundCheck, false); } } async function startRiderStripeConnectOnboarding({ status = els.riderTaxOnboardingStatus, button = els.startRiderTaxOnboarding, label = "Stripe Connect setup" } = {}) { const rider = currentRiderRecord() ?? state.rider; if (!rider || !state.sessions.rider) { if (status) status.textContent = "Sign in as a rider before starting Stripe setup."; return; } if (rider.status !== "approved") { if (status) status.textContent = "Stripe setup opens after admin approval."; return; } if (!hasSupabaseRuntime()) { if (status) status.textContent = "Stripe setup requires the Supabase production runtime."; return; } try { if (button) setButtonBusy(button, true); if (status) { status.dataset.busy = "true"; status.textContent = `Opening ${label}...`; } const payload = {}; let responsePayload = null; if (supabaseClient?.functions?.invoke) { const { data, error } = await withSupabaseTimeout( supabaseClient.functions.invoke("tax-onboarding-start", { body: payload }), "Starting hosted tax onboarding", supabaseProfileSaveTimeoutMs ); if (error) throw error; responsePayload = data; } else { const response = await withSupabaseTimeout( fetch(`${appConfig.supabaseUrl}/functions/v1/tax-onboarding-start`, { method: "POST", headers: { "content-type": "application/json", apikey: appConfig.supabaseAnonKey, authorization: `Bearer ${supabaseRestSession?.access_token || appConfig.supabaseAnonKey}` }, body: JSON.stringify(payload) }), "Starting hosted tax onboarding", supabaseProfileSaveTimeoutMs ); responsePayload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(responsePayload?.error || "Hosted tax onboarding Edge Function failed."); } if (responsePayload?.reference?.id) { const reference = mapTaxIdentityReferenceFromDatabase({ id: responsePayload.reference.id, rider_id: responsePayload.reference.rider_id ?? rider.id, provider: responsePayload.reference.provider, provider_subject_id: responsePayload.reference.provider_subject_id, tax_profile_status: responsePayload.reference.tax_profile_status, tin_last4: responsePayload.reference.tin_last4, legal_name: responsePayload.reference.legal_name, business_name: responsePayload.reference.business_name, tax_classification: responsePayload.reference.tax_classification, last_verified_at: responsePayload.reference.last_verified_at, created_at: responsePayload.reference.created_at, updated_at: responsePayload.reference.updated_at }); state.taxIdentityReferences = upsertById( state.taxIdentityReferences.filter((item) => item.riderId !== rider.id), reference ); } if (responsePayload?.paymentAccount?.id) { const paymentAccount = mapPaymentAccountFromDatabase(responsePayload.paymentAccount, new Map([[rider.id, { full_name: rider.name }]])); state.paymentAccounts = upsertById( state.paymentAccounts.filter((item) => !(item.role === "rider" && item.userId === rider.id)), paymentAccount ); } saveState(); renderAll(); if (!responsePayload?.url) throw new Error("Stripe did not return a hosted onboarding URL."); const opened = window.open(responsePayload.url, "_blank", "noopener,noreferrer"); if (status) { status.textContent = opened ? "Stripe opened in a separate tab. Finish onboarding there, then return to Waka." : "Popup was blocked. Allow popups for Waka, then try Stripe setup again."; } } catch (error) { if (button === els.startRiderStripePayoutSetup && paymentSetupRelaxedForTesting()) { try { if (status) status.textContent = `Stripe payout setup could not open: ${error.message}. Staging is linking a test payout account...`; const stagingAccount = { id: paymentAccountFor("rider", rider.id)?.id ?? makeId("payacct"), userId: rider.id, userName: rider.name, role: "rider", provider: "stripe-connect-test", accountType: "test_payout_account", accountHolder: rider.name || "Waka rider", accountLast4: "0000", institutionName: "Stripe Connect staging payout", reference: `staging-rider-payout-${rider.id}`, status: "linked", createdAt: paymentAccountFor("rider", rider.id)?.createdAt ?? new Date().toISOString(), updatedAt: new Date().toISOString() }; let savedAccount = stagingAccount; let localOnly = false; try { savedAccount = await savePaymentAccountToSupabase(stagingAccount); } catch (saveError) { localOnly = true; logClientWarning("Staging rider payout account could not be saved to Supabase; keeping it local for pilot testing.", saveError); } state.paymentAccounts = upsertById( state.paymentAccounts.filter((item) => !(item.role === "rider" && item.userId === rider.id)), savedAccount ); saveState(); renderAll(); void refreshMarketplace({ silent: true }); if (status) { status.textContent = localOnly ? `Staging test payout account is linked on this device because Stripe setup returned: ${error.message}` : `Staging test payout account is linked because Stripe setup returned: ${error.message}`; } return; } catch (fallbackError) { if (status) status.textContent = `Stripe setup failed, and the staging test payout account could not be linked: ${fallbackError.message}`; return; } } const setupHint = /edge function|failed to send|function|not configured|provider secrets/i.test(error.message) ? " Stripe Connect setup needs the Supabase Edge Function and Stripe secrets configured; Waka still keeps you in this rider workspace." : ""; if (status) status.textContent = `Could not start Stripe setup: ${error.message}.${setupHint}`; } finally { if (status) delete status.dataset.busy; if (button) setButtonBusy(button, false); } } async function startRiderTaxOnboarding() { if (String(appConfig.taxOnboardingMode || "").toLowerCase() === "disabled") { if (els.riderTaxOnboardingStatus) { els.riderTaxOnboardingStatus.textContent = "No hosted tax-provider flow is required for this Cameroon MVP package."; } return; } return startRiderStripeConnectOnboarding({ status: els.riderTaxOnboardingStatus, button: els.startRiderTaxOnboarding, label: `${appConfig.taxOnboardingProvider || "Stripe Connect"} hosted tax and payout setup` }); } async function startRiderStripePayoutSetup() { const rider = currentRiderRecord(); if (directRidePaymentMode()) { if (els.riderPaymentStatus) { els.riderPaymentStatus.textContent = paymentAccountSummary("rider", rider); } return; } return startRiderStripeConnectOnboarding({ status: els.riderPaymentStatus, button: els.startRiderStripePayoutSetup, label: "Stripe Connect payout setup" }); } function automaticRiderGpsReady() { return autoRiderGpsEnabled() && state.riderAvailabilityActivated === true && activeRole() === "rider" && riderCanActivateAvailability(currentRiderRecord()); } function riderAutoGpsSyncPolicy() { const activeRide = riderActiveImmediateRide(currentRiderRecord()); if (activeRide) { return { mode: "active", intervalMs: riderAutoGpsActiveRideSyncIntervalMs, minElapsedMs: riderAutoGpsActiveRideMinElapsedMs, movementMeters: riderAutoGpsActiveRideMinMovementMeters }; } return { mode: "matching", intervalMs: riderAutoGpsMovingSyncIntervalMs, movementMeters: riderAutoGpsMovingMinMovementMeters, idleIntervalMs: riderAutoGpsIdleSyncIntervalMs, idleMovementMeters: riderAutoGpsIdleHeartbeatMeters }; } function shouldSyncRiderGpsPoint(point, options = {}) { if (options.force) return true; if (!lastRiderAutoGpsSyncPoint || !lastRiderAutoGpsSyncAt) return true; const policy = riderAutoGpsSyncPolicy(); const elapsedMs = Date.now() - lastRiderAutoGpsSyncAt; const movedMeters = gpsDistanceMetersBetween(point, lastRiderAutoGpsSyncPoint); if (policy.mode === "active") { return elapsedMs >= policy.intervalMs || (movedMeters != null && movedMeters >= policy.movementMeters && elapsedMs >= policy.minElapsedMs); } if (movedMeters != null && movedMeters >= policy.movementMeters && elapsedMs >= policy.intervalMs) return true; if (elapsedMs >= policy.idleIntervalMs) return true; return movedMeters != null && movedMeters >= policy.idleMovementMeters && elapsedMs >= policy.intervalMs; } async function saveRiderLiveGpsPoint(currentGps, options = {}) { const rider = currentRiderRecord(); if (!rider || !hasSignedIn("rider")) return null; const qualityIssue = riderLiveGpsQualityIssue(currentGps); if (qualityIssue) { if (els.riderGpsStatus) els.riderGpsStatus.textContent = qualityIssue; return null; } if (!shouldSyncRiderGpsPoint(currentGps, options)) return rider; if (riderAutoGpsSyncPromise) return riderAutoGpsSyncPromise; const nextRider = { ...rider, currentGps, currentLatitude: currentGps.latitude, currentLongitude: currentGps.longitude, currentGpsAccuracyMeters: currentGps.accuracyMeters, currentGpsCapturedAt: currentGps.capturedAt }; riderAutoGpsSyncPromise = (async () => { await updateRiderLocationPresenceInSupabase(nextRider); const savedRider = { ...nextRider, supabaseUserId: state.rider?.supabaseUserId ?? nextRider.supabaseUserId }; saveCurrentRiderRecord(savedRider); lastRiderAutoGpsSyncAt = Date.now(); lastRiderAutoGpsSyncPoint = currentGps; void refreshMarketplace({ silent: true }); if (els.riderGpsStatus) { els.riderGpsStatus.textContent = hasSupabaseRuntime() ? `Online and available. ${gpsStatusLabel(currentGps, "Location active")} is used to match nearby requests.` : `Online in this local workspace. ${gpsStatusLabel(currentGps, "Location active")}.`; } renderRiderAvailabilityControls(savedRider); return savedRider; })(); try { return await riderAutoGpsSyncPromise; } finally { riderAutoGpsSyncPromise = null; } } function stopAutomaticRiderGps() { if (riderGpsWatchId != null && navigator.geolocation?.clearWatch) { navigator.geolocation.clearWatch(riderGpsWatchId); } riderGpsWatchId = null; } let riderScreenWakeLockEventsWired = false; function ensureRiderScreenWakeLockEvents() { if (riderScreenWakeLockEventsWired) return; riderScreenWakeLockEventsWired = true; window.addEventListener("focus", () => void ensureRiderScreenWakeLock()); document.addEventListener("visibilitychange", () => { if (!document.hidden) void ensureRiderScreenWakeLock(); }); ["pointerdown", "touchstart", "keydown"].forEach((eventName) => { document.addEventListener(eventName, () => void ensureRiderScreenWakeLock(), { passive: true }); }); } function riderScreenWakeLockShouldBeActive() { return Boolean( activeRole() === "rider" && !document.hidden && hasSignedIn("rider") && currentRiderRecord() ); } async function releaseRiderScreenWakeLock() { if (!riderScreenWakeLock) return; const lock = riderScreenWakeLock; riderScreenWakeLock = null; try { await lock.release?.(); } catch (error) { logClientWarning("Rider screen wake lock could not be released.", error); } } async function ensureRiderScreenWakeLock() { ensureRiderScreenWakeLockEvents(); if (!riderScreenWakeLockShouldBeActive()) { await releaseRiderScreenWakeLock(); return; } if (!navigator.wakeLock?.request) return; if (riderScreenWakeLock) return; if (Date.now() < riderScreenWakeLockRetryAfter) return; try { riderScreenWakeLock = await navigator.wakeLock.request("screen"); riderScreenWakeLockRetryAfter = 0; riderScreenWakeLock.addEventListener?.("release", () => { riderScreenWakeLock = null; if (riderScreenWakeLockShouldBeActive()) { window.setTimeout(() => void ensureRiderScreenWakeLock(), 1000); } }, { once: true }); } catch (error) { riderScreenWakeLockRetryAfter = Date.now() + riderScreenWakeLockRetryDelayMs; logClientWarning("Rider screen wake lock is unavailable in this browser session.", error); } } function ensureAutomaticRiderGps() { if (!autoRiderGpsEnabled()) return; if (!navigator.geolocation) { if (activeRole() === "rider" && els.riderGpsStatus) els.riderGpsStatus.textContent = "Availability cannot be activated because location is not available in this browser."; return; } if (!automaticRiderGpsReady()) { stopAutomaticRiderGps(); return; } if (riderGpsWatchId != null) return; if (els.riderGpsStatus) els.riderGpsStatus.textContent = "Activating availability..."; riderGpsWatchId = navigator.geolocation.watchPosition( (position) => { const currentGps = gpsPointFromPosition(position); if (!currentGps) return; void ensureRiderScreenWakeLock(); void saveRiderLiveGpsPoint(currentGps, { automatic: true }); }, () => { if (els.riderGpsStatus) els.riderGpsStatus.textContent = "Availability could not be activated because location permission was denied or unavailable."; }, { enableHighAccuracy: true, timeout: 15000, maximumAge: 5000 } ); } async function captureRiderLiveGps() { const rider = currentRiderRecord(); if (!rider || !hasSignedIn("rider")) { els.riderGpsStatus.textContent = "Sign in as a rider before activating availability."; return; } if (!riderCanActivateAvailability(rider)) { els.riderGpsStatus.textContent = riderWorkspaceStatusMessage(rider); return; } try { await assertPlatformFeatureEnabled("rider_activation_enabled", "Rider availability"); state.riderAvailabilityActivated = true; riderAutoGpsPaused = false; saveState(); els.riderGpsStatus.dataset.busy = "true"; els.riderGpsStatus.textContent = "Activating availability..."; if (paymentSetupRelaxedForTesting()) { await ensureStagingPaymentAccountForTesting("rider", rider, { localFallback: false }); } const currentGps = await getCurrentGpsPoint(); await saveRiderLiveGpsPoint(currentGps, { automatic: false, force: true }); void ensureRiderScreenWakeLock(); ensureAutomaticRiderGps(); state.riderPage = "requests"; state.selectedRequestId = null; if (typeof updateRiderWorkspaceRoute === "function") { updateRiderWorkspaceRoute("requests", { replace: true }); } saveState(); els.riderGpsStatus.textContent = "Activated. Opening marketplace."; renderAll(); void refreshMarketplace({ silent: true, reason: "rider_activated" }); } catch (error) { state.riderAvailabilityActivated = false; riderAutoGpsPaused = true; saveState(); els.riderGpsStatus.textContent = error.message; } finally { delete els.riderGpsStatus.dataset.busy; } } async function clearRiderLiveGps() { state.riderAvailabilityActivated = false; riderAutoGpsPaused = true; saveState(); stopAutomaticRiderGps(); const rider = currentRiderRecord(); if (!rider || !hasSignedIn("rider")) { els.riderGpsStatus.textContent = "Sign in as a rider before changing availability."; return; } try { els.riderGpsStatus.dataset.busy = "true"; els.riderGpsStatus.textContent = "Deactivating availability..."; const clearedRider = clearRiderLiveGpsFields(rider); await clearRiderLiveGpsInSupabase(clearedRider); saveCurrentRiderRecord(clearedRider); renderAll(); void refreshMarketplace({ silent: true }); els.riderGpsStatus.textContent = "Offline. Activate when you are ready to receive nearby ride requests."; renderRiderAvailabilityControls(clearedRider); } catch (error) { els.riderGpsStatus.textContent = error.message; } finally { delete els.riderGpsStatus.dataset.busy; } } async function updateRiderActiveLocation(event) { event.preventDefault(); if (!state.rider || !hasSignedIn("rider")) return; const country = els.riderActiveCountry.value; const city = els.riderActiveCity.value; const area = els.riderActiveArea.value; state.riderDestinationScope = "all"; if (els.riderDestinationScope) els.riderDestinationScope.value = "all"; const showAllNearbyPickups = true; const regionsToSave = []; const shouldSavePreference = true; const existingPreference = riderDayPreferenceFor(state.rider); const updatesUsed = existingPreference?.updatesUsed ?? 0; if (shouldSavePreference && updatesUsed >= 2) { els.riderDailyRegionStatus.textContent = "Today's destination regions were already set and updated once. Try again tomorrow."; return; } try { els.riderLocationStatus.textContent = !showAllNearbyPickups ? "Saving today's preferred rider regions..." : "Saving all-nearby pickup visibility..."; const riderId = state.rider.supabaseUserId ?? state.rider.id; await updateRiderCurrentAreaInSupabase(riderId, country, city, area); let savedPreference = existingPreference ?? state.rider.dailyRegions ?? null; if (shouldSavePreference) { const preference = { id: existingPreference?.id ?? makeId("day"), riderId: state.rider.id, riderName: state.rider.name, serviceDate: localDateKey(), country, city, originArea: area, regions: regionsToSave, showAllNearbyPickups, updatesUsed: updatesUsed + 1, createdAt: existingPreference?.createdAt ?? new Date().toISOString(), updatedAt: new Date().toISOString() }; savedPreference = await saveRiderDayPreferenceToSupabase(preference); } state.rider = { ...state.rider, country, city, area, dailyRegions: savedPreference ?? null }; state.riders = upsertById(state.riders, state.rider); if (savedPreference) { state.riderDayPreferences = upsertById( state.riderDayPreferences.filter((item) => !(item.riderId === state.rider.id && item.serviceDate === savedPreference.serviceDate)), savedPreference ); } clearSelectedRequestOutsideLocation(country, city); saveState(); if (lastLocationUpdateSource !== "location update RPC") { await updateRiderLocationPresenceInSupabase(state.rider); } populateLocationFields(); hydrateForms(); renderAll(); void refreshMarketplace({ silent: true }); renderRiderAvailabilityControls(state.rider); els.riderLocationStatus.textContent = riderServiceAreaSummary(state.rider); renderRiderDailyRegionStatus(state.rider); } catch (error) { els.riderLocationStatus.textContent = error.message; } } async function createRider(event) { event.preventDefault(); setTranslatedStatus(els.riderStatus, "checkingRiderApplication"); const resubmittingCorrections = state.rider?.status === "needs_correction"; const country = els.riderCountry.value; const documentFiles = selectedRiderDocumentFiles(); const selectedDocumentNames = Object.fromEntries(Object.entries(documentFiles) .filter(([, file]) => Boolean(file)) .map(([key, file]) => [key, file.name])); const documentNames = { ...(resubmittingCorrections ? riderDocuments(state.rider) : emptyRiderDocuments()), ...selectedDocumentNames }; const profilePhotoName = els.riderPhoto.files[0]?.name ?? state.rider?.profilePhotoName ?? ""; const phone = els.riderPhone.value.trim(); const birthMonth = normalizeYearMonthOfBirthInput(els.riderDob); const dateOfBirth = yearMonthToStoredDate(birthMonth); const driverLicenseExpiresOn = els.riderLicenseExpiresOn?.value ?? ""; const insuranceExpiresOn = els.riderInsuranceExpiresOn?.value ?? ""; const missingDocuments = missingRiderDocumentLabels(documentNames); if (!validateAccountForm(els.riderAccountForm, els.riderStatus)) return; if (!dateOfBirth) { els.riderStatus.textContent = "Enter a valid year and month of birth as YYYY-MM."; return; } if (daysUntilDate(driverLicenseExpiresOn) === null || daysUntilDate(driverLicenseExpiresOn) < 0) { els.riderStatus.textContent = "Enter a current driver's license expiration date."; return; } if (daysUntilDate(insuranceExpiresOn) === null || daysUntilDate(insuranceExpiresOn) < 0) { els.riderStatus.textContent = "Enter a current insurance expiration date."; return; } if (missingDocuments.length) { setTranslatedStatus(els.riderStatus, "missingRiderDocuments", { documents: missingDocuments.join(", ") }); return; } if (!(await ensureVerifiedPhoneForAccount("rider", phone, els.riderStatus))) return; const rider = { id: state.rider?.id ?? makeId("rider"), name: els.riderName.value.trim(), email: els.riderEmail.value.trim().toLowerCase(), password: els.riderPassword.value, phone, phoneVerified: true, phoneVerifiedAt: state.verification.rider?.verifiedAt ?? state.rider?.phoneVerifiedAt ?? new Date().toISOString(), phoneVerificationProvider: state.verification.rider?.provider ?? "manual-pilot", nationalId: els.riderNationalId.value.trim(), dateOfBirth, preferredLanguage: state.language, country, city: els.riderCity.value, area: els.riderArea.value, vehicle: "car", credential: els.riderNationalId.value.trim(), driverLicenseExpiresOn, registration: els.riderRegistration.value.trim(), carMake: els.riderCarMake.value, carModel: els.riderCarModel.value, carBodyType: normalizeCarBodyType(els.riderCarBodyType.value), vehicleDesignation: normalizeRiderVehicleDesignation(els.riderVehicleDesignation?.value, els.riderCarBodyType.value), navigationPreference: normalizeRiderNavigationPreference(els.riderNavigationPreference?.value), carYear: els.riderCarYear.value, carColor: els.riderCarColor.value.trim(), vehicleVin: els.riderVehicleVin.value.trim().toUpperCase(), insuranceProvider: els.riderInsuranceProvider.value.trim(), insuranceNumber: els.riderInsuranceNumber.value.trim(), insuranceExpiresOn, backgroundCheckConsentAt: els.riderBackgroundConsent?.checked ? new Date().toISOString() : null, backgroundCheckProvider: appConfig.backgroundCheckProvider || "checkr", backgroundCheckConsentVersion: "maryland-2026-05", profilePhotoName, profilePhotoPath: state.rider?.profilePhotoPath ?? null, documentName: riderApplicationDocumentPayload(documentNames, { carBodyType: normalizeCarBodyType(els.riderCarBodyType.value), vehicleDesignation: normalizeRiderVehicleDesignation(els.riderVehicleDesignation?.value, els.riderCarBodyType.value), navigationPreference: normalizeRiderNavigationPreference(els.riderNavigationPreference?.value) }), documents: { ...documentNames, vehicleDesignation: normalizeRiderVehicleDesignation(els.riderVehicleDesignation?.value, els.riderCarBodyType.value), navigationPreference: normalizeRiderNavigationPreference(els.riderNavigationPreference?.value) }, driverLicenseDocumentName: documentNames.driverLicense, vehicleRegistrationDocumentName: documentNames.vehicleRegistration, insuranceDocumentName: documentNames.insurance, vehicleInspectionDocumentName: documentNames.vehicleInspection, backgroundCheckStatus: resubmittingCorrections ? state.rider?.backgroundCheckStatus ?? "not requested" : "not requested", backgroundCheckDecision: resubmittingCorrections ? state.rider?.backgroundCheckDecision ?? "pending" : "pending", status: "pending", reviewNote: "", approvedAt: null, trialEndsAt: null, subscriptionPaidUntil: null, rating: "new", createdAt: new Date().toISOString() }; let syncedRiderUser = null; try { setButtonBusy(els.riderSubmitButton, true); const setRiderStage = (message) => { els.riderStatus.textContent = message; }; setTranslatedStatus(els.riderStatus, isSupabaseMode() ? "startingRiderSupabase" : "savingRiderApplication"); const signedInUser = hasSupabaseRuntime() ? await getSupabaseUser().catch((error) => { logClientWarning("Current Supabase user could not be checked before rider onboarding.", error); return null; }) : null; const signedInUserOwnsEmail = authUserEmail(signedInUser) === rider.email; const signedInUserOwnsVerifiedPhone = authUserMatchesVerifiedPhone(signedInUser, rider); if (hasSupabaseRuntime()) { try { const excludeUserId = await profileAvailabilityExcludeUserId(rider.email, state.rider?.id ?? null); const availability = await profileContactAvailability(rider.email, rider.phone, excludeUserId, "rider"); if (!availability.emailAvailable || !availability.phoneAvailable) { const secureOnboardingCanVerifyOwner = !resubmittingCorrections && !signedInUserOwnsEmail && !signedInUserOwnsVerifiedPhone; if (!secureOnboardingCanVerifyOwner) { els.riderStatus.textContent = !availability.emailAvailable ? "A rider account already exists with this email address. Sign in with that rider account instead of creating a duplicate." : "A rider account already exists with this phone number. Sign in with that rider account instead of creating a duplicate."; return; } logClientWarning("Rider contact availability is reserved for secure onboarding owner verification.", availability); } } catch (error) { const availabilityMessage = profileAvailabilityErrorMessage(error); if (availabilityMessage) { els.riderStatus.textContent = availabilityMessage; return; } logClientWarning("Profile contact availability check was skipped.", error); } } if (hasSupabaseRuntime() && !resubmittingCorrections && !signedInUserOwnsEmail && !signedInUserOwnsVerifiedPhone) { const result = await submitNewRiderOnboardingToSupabase(rider, setRiderStage); const savedDocuments = { ...rider.documents, ...(result?.documents || {}) }; els.riderPassword.value = ""; els.riderPhoto.value = ""; els.riderLicenseDocument.value = ""; els.riderRegistrationDocument.value = ""; els.riderInsuranceDocument.value = ""; els.riderInspectionDocument.value = ""; state.accountMode.rider = "signin"; state.activeTab = "rider"; state.showRoleEntry = false; state.riderPage = "checks"; clearPendingProfileRecovery("rider"); saveState(); renderAll(); if (els.riderSignInEmail) els.riderSignInEmail.value = rider.email; if (els.riderSignInPassword) els.riderSignInPassword.value = ""; const successMessage = `Rider application submitted for admin review. Confirm the email for ${rider.email}, then sign in here to monitor Eligibility checks.`; if (els.riderSignInStatus) els.riderSignInStatus.textContent = successMessage; if (els.riderStatus) els.riderStatus.textContent = successMessage; if (els.riderBackgroundCheckStatus) { els.riderBackgroundCheckStatus.textContent = riderManualBackgroundReviewMode() ? "Application submitted. Eligibility checks will show Waka admin review after sign-in." : "Application submitted. Eligibility checks will show admin review and Checkr testing progress after sign-in."; } return savedDocuments; } const user = await saveProfileToSupabase({ ...rider, role: "rider" }, setRiderStage, { waitForProfile: true, preventExistingAccount: false }); syncedRiderUser = user; setTranslatedStatus(els.riderStatus, "submittingRiderApplication"); const savedDocuments = await saveRiderApplicationToSupabase(rider, user?.id) ?? rider.documents; state.rider = { ...rider, password: undefined, id: user?.id ?? rider.id, profilePhotoPath: user?.profilePhotoPath ?? rider.profilePhotoPath, documentName: riderApplicationDocumentPayload(savedDocuments, rider), documents: { ...savedDocuments, vehicleDesignation: rider.vehicleDesignation, navigationPreference: rider.navigationPreference }, driverLicenseDocumentPath: savedDocuments.driverLicense, vehicleRegistrationDocumentPath: savedDocuments.vehicleRegistration, insuranceDocumentPath: savedDocuments.insurance, vehicleInspectionDocumentPath: savedDocuments.vehicleInspection, supabaseUserId: user?.id ?? null }; state.sessions.rider = { phone: state.rider.phone, email: state.rider.email, userId: state.rider.supabaseUserId, signedInAt: new Date().toISOString() }; await claimReferralCodeForRole("rider", els.riderStatus); els.riderPassword.value = ""; els.riderPhoto.value = ""; els.riderLicenseDocument.value = ""; els.riderRegistrationDocument.value = ""; els.riderInsuranceDocument.value = ""; els.riderInspectionDocument.value = ""; state.riders = state.riders.filter((item) => item.id !== rider.id && item.id !== user?.id); state.riders.unshift(state.rider); state.accountMode.rider = "signin"; clearPendingProfileRecovery("rider"); state.riderPage = "checks"; saveState(); renderAll(); setTranslatedStatus(els.riderStatus, "riderCreatedPending", { name: state.rider.name }); if (els.riderBackgroundCheckStatus) { els.riderBackgroundCheckStatus.textContent = riderManualBackgroundReviewMode() ? "Application submitted. Eligibility checks show Waka admin review." : "Application submitted. Eligibility checks show admin review and Checkr testing progress."; } setTranslatedStatus(els.riderSessionSummary, "riderCreatedPending", { name: state.rider.name }); } catch (error) { if (syncedRiderUser?.id) { state.rider = { ...rider, password: undefined, id: syncedRiderUser.id, supabaseUserId: syncedRiderUser.id, profilePhotoPath: syncedRiderUser.profilePhotoPath ?? rider.profilePhotoPath, needsApplication: true, status: "profile only" }; state.sessions.rider = { phone: state.rider.phone, email: state.rider.email, userId: state.rider.supabaseUserId, signedInAt: new Date().toISOString() }; state.riders = upsertById(state.riders, state.rider); state.accountMode.rider = "signin"; clearPendingProfileRecovery("rider"); state.riderPage = "profile"; saveState(); renderAll(); els.riderStatus.textContent = `Rider profile was synced, but the application did not reach admin review: ${riderApplicationErrorMessage(error)} Review the form and submit again.`; } else { if (riderAccountCreationRequiresSignIn(error)) { routeRiderAccountCreationToSignIn(rider, error); return; } setTranslatedStatus(els.riderStatus, "riderAccountFailed", { message: riderApplicationErrorMessage(error) }); } } finally { setButtonBusy(els.riderSubmitButton, false); } } function riderAccountCreationRequiresSignIn(error) { const message = String(error?.message || error || ""); return /Supabase created the .*login|email confirmation|already has a Supabase login|existing password|Supabase did not accept the sign-in/i.test(message); } function routeRiderAccountCreationToSignIn(rider, error) { const message = String(error?.message || error || ""); state.accountMode.rider = "signin"; state.activeTab = "rider"; state.showRoleEntry = false; saveState(); renderAll(); if (els.riderSignInEmail) els.riderSignInEmail.value = rider.email; if (els.riderSignInPassword) els.riderSignInPassword.value = ""; const status = els.riderSignInStatus ?? els.riderStatus; if (status) { status.textContent = /email confirmation|Supabase created the .*login|Supabase did not accept the sign-in/i.test(message) ? message : "This email already has a login. Sign in here with the existing password; Waka will open the rider profile or application form."; } } // Runtime render loop, event wiring, install flow, service worker registration, and startup. let serviceWorkerRefreshPending = false; let deploymentUpdateApplying = false; let pendingServiceWorkerRegistration = null; const appCacheName = "waka-negotiated-static-v504"; const deploymentUpdateActiveStatuses = new Set(["open", "matched", "arrived", "in_progress"]); async function clearOldAppCaches() { if (!("caches" in window)) return; try { const keys = await caches.keys(); await Promise.all(keys .filter((key) => key.startsWith("waka-negotiated-static-") && key !== appCacheName) .map((key) => caches.delete(key))); } catch (error) { logClientWarning("Could not clear old app caches.", error); } } function deploymentUpdateCurrentRole() { try { return typeof activeRole === "function" ? activeRole() : state.activeTab; } catch { return state.activeTab; } } function deploymentUpdateRequestMap() { if (typeof stateLookupIndexes === "function") return stateLookupIndexes().requestMap; return new Map((state.requests || []).map((request) => [request.id, request])); } function deploymentUpdateHasOpenRiderOffer() { if (!state.rider?.id || !Array.isArray(state.offers)) return false; const requestMap = deploymentUpdateRequestMap(); return state.offers.some((offer) => { if (offer.riderId !== state.rider.id) return false; if (["withdrawn", "declined", "expired", "accepted"].includes(offer.status)) return false; const request = requestMap.get(offer.requestId); return request?.status === "open"; }); } function deploymentUpdateUserBusy() { const role = deploymentUpdateCurrentRole(); if (!["passenger", "rider"].includes(role)) return false; try { const selected = typeof selectedRequest === "function" ? selectedRequest() : null; const activeRide = typeof activeRideForRole === "function" ? activeRideForRole(selected) : selected; if (activeRide && deploymentUpdateActiveStatuses.has(activeRide.status)) return true; if (role === "passenger") { const ownsOpenRequest = selected?.status === "open" && (typeof requestBelongsToPassenger !== "function" || requestBelongsToPassenger(selected)); if (ownsOpenRequest) return true; } if (role === "rider") { const canSeeOpenRequest = selected?.status === "open" && (typeof roleCanSeeRequest !== "function" || roleCanSeeRequest(selected)); const canActOnSelectedRequest = typeof riderCanShowOfferControls === "function" && riderCanShowOfferControls(undefined, selected || undefined); if (canSeeOpenRequest || canActOnSelectedRequest || deploymentUpdateHasOpenRiderOffer()) return true; } } catch (error) { logClientWarning("Could not determine whether a deployment update can apply immediately.", error); return true; } return false; } function deploymentUpdateCopy(busy = deploymentUpdateUserBusy()) { if (busy) { return { title: "Waka update ready after this step", message: "A verified Waka security and reliability update is ready. Finish this fare decision or ride step, then update." }; } return { title: "Waka update ready", message: "A verified Waka security and reliability update is ready. Updating keeps fares, privacy, and ride screens current." }; } function showDeploymentUpdateNotice(registration = pendingServiceWorkerRegistration) { pendingServiceWorkerRegistration = registration || pendingServiceWorkerRegistration; if (!els.deploymentUpdateNotice) return; const busy = deploymentUpdateUserBusy(); const copy = deploymentUpdateCopy(busy); els.deploymentUpdateNotice.dataset.mode = busy ? "active" : "ready"; if (els.deploymentUpdateTitle) els.deploymentUpdateTitle.textContent = copy.title; if (els.deploymentUpdateMessage) els.deploymentUpdateMessage.textContent = copy.message; if (els.deploymentUpdateNow) { els.deploymentUpdateNow.disabled = false; els.deploymentUpdateNow.textContent = "Update now"; } els.deploymentUpdateNotice.hidden = false; } function hideDeploymentUpdateNotice() { if (els.deploymentUpdateNotice) els.deploymentUpdateNotice.hidden = true; } function applyDeploymentUpdate(registration = pendingServiceWorkerRegistration) { if (deploymentUpdateApplying) return; deploymentUpdateApplying = true; if (els.deploymentUpdateNow) { els.deploymentUpdateNow.disabled = true; els.deploymentUpdateNow.textContent = "Updating..."; } const waitingWorker = registration?.waiting || pendingServiceWorkerRegistration?.waiting; if (waitingWorker) { waitingWorker.postMessage({ type: "SKIP_WAITING" }); window.setTimeout(() => { if (!serviceWorkerRefreshPending) window.location.reload(); }, 4000); return; } window.location.reload(); } function handleDeploymentUpdateAvailable(registration) { pendingServiceWorkerRegistration = registration; if (!registration?.waiting) return; if (deploymentUpdateUserBusy()) { showDeploymentUpdateNotice(registration); return; } applyDeploymentUpdate(registration); } function tryApplyPendingDeploymentUpdate() { if (!pendingServiceWorkerRegistration?.waiting || deploymentUpdateApplying) return; if (deploymentUpdateUserBusy()) { showDeploymentUpdateNotice(pendingServiceWorkerRegistration); return; } applyDeploymentUpdate(pendingServiceWorkerRegistration); } async function refreshServiceWorkerUpdate() { if (!("serviceWorker" in navigator)) return; try { const registration = await navigator.serviceWorker.getRegistration(); if (!registration) return; await registration.update().catch(() => {}); if (registration.waiting) handleDeploymentUpdateAvailable(registration); } catch (error) { logClientWarning("Could not check for a Waka deployment update.", error); } } function renderAll() { applyLanguage(); renderEntryExperience(); renderAccountWorkspaces(); renderRiderFlow(); renderRiderStatus(); renderRoleWorkspace(); if (typeof renderWorkspaceBackgroundMap === "function") renderWorkspaceBackgroundMap(); renderMap(); renderRequests(); renderOffers(); renderSelectedSummary(); renderChat(); updateConnectionStatus(); ensureAutomaticLocationServices(); } function renderWorkspacePanelSafely(label, renderFn) { try { renderFn(); } catch (error) { logClientWarning(`Admin ${label} panel failed to render.`, error); if (els.adminStatus) { els.adminStatus.textContent = `Admin ${label} could not render: ${compactAdminErrorMessage(error)}`; } } } function ensureAutomaticLocationServices() { if (typeof ensurePassengerPickupGpsAutoCapture === "function") ensurePassengerPickupGpsAutoCapture(); if (typeof ensurePassengerNearbyRiderCountsAutoRefresh === "function") ensurePassengerNearbyRiderCountsAutoRefresh(); if (typeof ensurePassengerApproachAutoRefresh === "function") ensurePassengerApproachAutoRefresh(); if (typeof ensureRiderMarketplaceAutoRefresh === "function") ensureRiderMarketplaceAutoRefresh(); if (typeof ensureAccountNoticeAutoRefreshes === "function") ensureAccountNoticeAutoRefreshes(); if (typeof ensureMarketplaceRealtimeSubscription === "function") ensureMarketplaceRealtimeSubscription(); if (typeof ensureAutomaticRiderGps === "function") ensureAutomaticRiderGps(); if (typeof ensureRiderScreenWakeLock === "function") void ensureRiderScreenWakeLock(); if (typeof ensureRiderActiveRideNavigation === "function") ensureRiderActiveRideNavigation(); } function emptyState(text) { const div = document.createElement("div"); div.className = "empty-state"; div.textContent = text; return div; } function escapeHtml(value) { return String(value ?? "").replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" })[character]); } function normalizeHttpsUrl(value) { const trimmed = String(value ?? "").trim(); if (!trimmed) return ""; try { const url = new URL(trimmed); return url.protocol === "https:" ? url.href : ""; } catch (_error) { return ""; } } function chip(text) { const value = String(text ?? ""); const fareMovement = /^(?:Rider fare\s+)?[\u2191\u2193]\s/.test(value); const classNames = []; if (fareMovement) { classNames.push("fare-movement-chip"); if (/\u2191\s/.test(value)) classNames.push("fare-movement-up"); if (/\u2193\s/.test(value)) classNames.push("fare-movement-down"); } if (/^Pickup ETA: .+ \/ Destination drive:/i.test(value)) classNames.push("rider-route-distance-chip"); const className = classNames.length ? ` class="${classNames.join(" ")}"` : ""; return `${escapeHtml(value)}`; } function wireOnce(element, key, eventName, handler) { if (!element || element.dataset?.[key] === "true") return; element.addEventListener(eventName, handler); if (element.dataset) element.dataset[key] = "true"; } function wireAccountAuthEvents() { document.querySelectorAll("[data-account-type][data-account-mode]").forEach((button) => { wireOnce(button, "wakaAccountModeWired", "click", () => setAccountMode(button.dataset.accountType, button.dataset.accountMode)); }); document.querySelectorAll("[data-account-create-link]").forEach((link) => { wireOnce(link, "wakaAccountCreateRouteWired", "click", (event) => { event.preventDefault(); setAccountMode(link.dataset.accountCreateLink, "create"); }); }); document.querySelectorAll("[data-account-signin-link]").forEach((link) => { wireOnce(link, "wakaAccountSigninRouteWired", "click", (event) => { event.preventDefault(); setAccountMode(link.dataset.accountSigninLink, "signin"); }); }); wireOnce(els.passengerSignInForm, "wakaSignInSubmitWired", "submit", (event) => { event.preventDefault(); verifySignIn("passenger"); }); wireOnce(els.sendPassengerSignInCode, "wakaSignInCodeWired", "click", () => sendSignInCode("passenger")); wireOnce(els.verifyPassengerSignIn, "wakaSignInVerifyWired", "click", () => verifySignIn("passenger")); wireOnce(els.forgotPassengerPassword, "wakaPasswordResetRequestWired", "click", () => requestPasswordReset("passenger")); wireOnce(els.sendPassengerPasswordResetPhoneOtp, "wakaPasswordResetPhoneOtpSendWired", "click", () => sendPasswordResetPhoneOtp("passenger")); wireOnce(els.verifyPassengerPasswordResetPhoneOtp, "wakaPasswordResetPhoneOtpVerifyWired", "click", () => verifyPasswordResetPhoneOtp("passenger")); wireOnce(els.savePassengerResetPassword, "wakaPasswordResetSaveWired", "click", () => completePasswordReset("passenger")); wireOnce(els.passengerSignOut, "wakaSignOutWired", "click", () => signOutRole("passenger")); wireOnce(els.passengerMenuSignOut, "wakaSignOutWired", "click", () => signOutRole("passenger")); wireOnce(els.riderSignInForm, "wakaSignInSubmitWired", "submit", (event) => { event.preventDefault(); verifySignIn("rider"); }); wireOnce(els.sendRiderSignInCode, "wakaSignInCodeWired", "click", () => sendSignInCode("rider")); wireOnce(els.verifyRiderSignIn, "wakaSignInVerifyWired", "click", () => verifySignIn("rider")); wireOnce(els.forgotRiderPassword, "wakaPasswordResetRequestWired", "click", () => requestPasswordReset("rider")); wireOnce(els.sendRiderPasswordResetPhoneOtp, "wakaPasswordResetPhoneOtpSendWired", "click", () => sendPasswordResetPhoneOtp("rider")); wireOnce(els.verifyRiderPasswordResetPhoneOtp, "wakaPasswordResetPhoneOtpVerifyWired", "click", () => verifyPasswordResetPhoneOtp("rider")); wireOnce(els.saveRiderResetPassword, "wakaPasswordResetSaveWired", "click", () => completePasswordReset("rider")); wireOnce(els.riderSignOut, "wakaSignOutWired", "click", () => signOutRole("rider")); wireOnce(els.riderMenuSignOutTop, "wakaSignOutWired", "click", () => signOutRole("rider")); wireOnce(els.riderMenuSignOut, "wakaSignOutWired", "click", () => signOutRole("rider")); } function wireDeploymentUpdateEvents() { wireOnce(els.deploymentUpdateNow, "wakaDeploymentUpdateWired", "click", () => applyDeploymentUpdate()); window.addEventListener("online", () => { updateConnectionStatus(); void refreshServiceWorkerUpdate(); }); window.addEventListener("focus", tryApplyPendingDeploymentUpdate); document.addEventListener("visibilitychange", () => { if (!document.hidden) tryApplyPendingDeploymentUpdate(); }); } let riderNavigationPreferenceSyncChain = Promise.resolve(); function updateRiderNavigationPreferenceFromProfile() { const preference = normalizeRiderNavigationPreference(els.riderNavigationPreference?.value); syncRiderNavigationPreferenceInput(preference, { userSelected: true }); const riderId = state.rider?.id ?? state.rider?.supabaseUserId ?? state.sessions?.rider?.userId; rememberRiderNavigationPreferenceOverride(preference, riderId); if (state.rider) { const riderIds = state.riderNavigationPreferenceOverride?.riderIds?.length ? state.riderNavigationPreferenceOverride.riderIds : [riderId, ...currentRiderIdentityAliases()].filter(Boolean); const withNavigationPreference = (rider) => { const documents = { ...riderDocuments(rider), vehicleDesignation: normalizeRiderVehicleDesignation(rider.vehicleDesignation, rider.carBodyType), navigationPreference: preference }; return { ...rider, navigationPreference: preference, documents, documentName: riderDocumentPayload(documents) }; }; state.rider = withNavigationPreference(state.rider); const documents = state.rider.documents; state.riders = upsertById( (state.riders ?? []).map((rider) => ( riderRecordMatchesIdentityAliases(rider, riderIds) ? withNavigationPreference(rider) : rider )), state.rider ); if (riderId) { const syncRiderIds = [...riderIds]; const syncDocuments = { ...documents }; riderNavigationPreferenceSyncChain = riderNavigationPreferenceSyncChain .catch(() => {}) .then(() => { const latestPreference = riderNavigationPreferenceOverrideForRider(syncRiderIds) ?? riderNavigationPreference(); if (latestPreference !== preference) return null; return updateRiderApplicationDocumentsInSupabase(riderId, syncDocuments); }) .catch((error) => logClientWarning("Rider navigation preference could not be synced.", error)); } } saveState(); renderAll(); syncRiderNavigationPreferenceInput(preference, { userSelected: true }); } function wireEvents() { wireAccountAuthEvents(); document.querySelectorAll(".tab-button").forEach((button) => { button.addEventListener("click", () => switchTab(button.dataset.tab)); }); document.querySelectorAll("[data-entry-role]").forEach((button) => { button.addEventListener("click", () => switchTab(button.dataset.entryRole, { resetAccountMode: true })); }); document.querySelectorAll("[data-passenger-page]").forEach((button) => { button.addEventListener("click", () => setPassengerWorkspacePage(button.dataset.passengerPage)); }); document.querySelectorAll(".filter-button").forEach((button) => { button.addEventListener("click", () => { state.filter = button.dataset.filter; document.querySelectorAll(".filter-button").forEach((item) => item.classList.toggle("active", item === button)); saveState(); renderAll(); }); }); wireDateOfBirthInput(els.passengerDob); wireYearMonthInput(els.riderDob); els.passengerCountry.addEventListener("change", updatePassengerCityOptions); els.passengerCity.addEventListener("change", updatePickupOptions); els.pickupArea.addEventListener("change", () => { updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(); }); els.destinationArea.addEventListener("change", updateFareGuidance); els.rideStops.addEventListener("input", updateFareGuidance); els.vehiclePreference.addEventListener("change", () => { clearLowFareReview(); updateFareGuidance(); schedulePassengerNearbyRiderCountsRefresh(); renderAll(); }); els.passengerActiveCountry.addEventListener("change", updatePassengerActiveCityOptions); const handleLanguageChange = (event) => { state.language = event.currentTarget.value; saveState(); els.languageSelects?.forEach((select) => { select.value = state.language; }); if (els.languageSelect) els.languageSelect.value = state.language; renderAll(); }; els.languageSelect?.addEventListener("change", handleLanguageChange); els.languageSelects?.forEach((select) => { select.addEventListener("change", handleLanguageChange); }); els.sendPassengerCode.addEventListener("click", () => sendVerificationCode("passenger")); els.verifyPassengerPhone.addEventListener("click", () => verifyPhone("passenger")); els.passengerEnablePush?.addEventListener("click", () => enableAccountPushNotifications("passenger")); els.destination.addEventListener("input", handleDestinationInput); els.destination.addEventListener("keyup", scheduleDestinationAutocomplete); els.destination.addEventListener("focus", scheduleDestinationAutocomplete); els.destination.addEventListener("blur", () => window.setTimeout(hideDestinationSuggestions, 150)); els.pickupDescription.addEventListener("input", handlePickupInput); els.pickupDescription.addEventListener("keyup", schedulePickupAutocomplete); els.pickupDescription.addEventListener("focus", schedulePickupAutocomplete); els.pickupDescription.addEventListener("blur", () => window.setTimeout(hidePickupSuggestions, 150)); els.rideRequestForm.addEventListener("focusin", ensurePassengerPickupGpsAutoCapture); els.pickupUseCurrentLocation?.addEventListener("change", handlePickupUseCurrentLocationChange); els.useCurrentPickup?.addEventListener("click", activateUseCurrentPickup); els.useCurrentPickup?.addEventListener("pointerup", activateUseCurrentPickup); document.addEventListener("pointerdown", closeWorkspaceMenusFromOutside); document.addEventListener("click", (event) => { if (event.target?.closest?.("#useCurrentPickup")) activateUseCurrentPickup(event); closeWorkspaceMenusFromOutside(event); }); document.addEventListener("keydown", (event) => { if (!["Enter", " "].includes(event.key)) return; if (event.target?.closest?.("#useCurrentPickup")) activateUseCurrentPickup(event); }); els.useCurrentPickup?.addEventListener("click", activateUseCurrentPickup); els.useCurrentPickup?.addEventListener("pointerup", activateUseCurrentPickup); document.addEventListener("click", (event) => { if (event.target?.closest?.("#useCurrentPickup")) activateUseCurrentPickup(event); }); document.addEventListener("keydown", (event) => { if (!["Enter", " "].includes(event.key)) return; if (event.target?.closest?.("#useCurrentPickup")) activateUseCurrentPickup(event); }); els.rideStops.addEventListener("input", handleRideStopsInput); els.rideStops.addEventListener("focus", scheduleRideStopAutocomplete); els.rideStops.addEventListener("click", scheduleRideStopAutocomplete); els.rideStops.addEventListener("keyup", scheduleRideStopAutocomplete); els.rideStops.addEventListener("blur", () => window.setTimeout(hideRideStopSuggestions, 150)); ["pointerup", "touchend", "click"].forEach((eventName) => { els.addRideStop?.addEventListener(eventName, activateRideStopTool); }); els.addRideStop?.addEventListener("keydown", activateRideStopToolFromKey); els.clearRideStops?.addEventListener("click", clearRideStopsInput); [ [els.toggleRideTiming, "timing"], [els.toggleVehiclePreference, "vehicle"], [els.toggleFareDetails, "fare"] ].forEach(([button, optionName]) => { ["pointerup", "touchend", "click"].forEach((eventName) => { button?.addEventListener(eventName, (event) => handleRideRequestOptionToggle(optionName, event)); }); button?.addEventListener("keydown", (event) => handleRideRequestOptionKeyToggle(optionName, event)); }); document.querySelectorAll("[data-passenger-fare-mode]").forEach((button) => { ["pointerup", "touchend", "click"].forEach((eventName) => { button.addEventListener(eventName, handlePassengerFareModeButtonActivation); }); button.addEventListener("keydown", handlePassengerFareModeButtonKeyActivation); }); els.passengerFareMode?.addEventListener("input", handlePassengerFareModeSelection); els.passengerFareMode?.addEventListener("change", handlePassengerFareModeSelection); updatePassengerFareModeControls(); initializeRideRequestOptionPanels(); els.fareOffer.addEventListener("input", clearLowFareReview); els.rideTiming?.addEventListener("change", handleRideTimingChange); updateScheduledRideControls(); els.sendRiderCode.addEventListener("click", () => sendVerificationCode("rider")); els.verifyRiderPhone.addEventListener("click", () => verifyPhone("rider")); els.riderEnablePush?.addEventListener("click", () => enableAccountPushNotifications("rider")); els.riderCountry.addEventListener("change", updateRiderCityOptions); els.riderCity.addEventListener("change", updateRiderAreas); els.riderCarMake.addEventListener("change", () => populateSelect(els.riderCarModel, carMakeCatalog[els.riderCarMake.value] ?? carMakeCatalog.Other, carMakeCatalog[els.riderCarMake.value]?.[0])); els.riderCarBodyType.addEventListener("change", () => { updateRiderVehicleDesignationForBodyType(); saveState(); }); els.riderVehicleDesignation?.addEventListener("change", () => { if (state.rider) state.rider.vehicleDesignation = normalizeRiderVehicleDesignation(els.riderVehicleDesignation.value, els.riderCarBodyType.value); saveState(); }); els.riderActiveCountry.addEventListener("change", updateRiderActiveCityOptions); els.riderActiveCity.addEventListener("change", updateRiderActiveAreas); els.passengerAccountForm.addEventListener("submit", createPassenger); els.passengerAccountUse?.addEventListener("change", updatePassengerInitialBusinessFields); els.passengerPaymentForm.addEventListener("submit", startPassengerPaymentSetup); els.startPassengerPaymentSetup?.addEventListener("click", startPassengerPaymentSetup); els.passengerLocationForm.addEventListener("submit", updatePassengerActiveLocation); els.passengerSupportForm?.addEventListener("submit", submitAccountSupportTicket); if (els.businessAccountForm) els.businessAccountForm.addEventListener("submit", createBusinessAccount); els.capturePickupGps?.addEventListener("click", capturePassengerPickupGps); els.clearPickupGps?.addEventListener("click", clearPassengerPickupGps); els.rideRequestForm.addEventListener("submit", createRideRequest); els.riderAccountForm.addEventListener("submit", createRider); els.riderPaymentForm.addEventListener("submit", (event) => event.preventDefault()); els.startRiderStripePayoutSetup?.addEventListener("click", startRiderStripePayoutSetup); els.riderLocationForm.addEventListener("submit", updateRiderActiveLocation); els.riderSupportForm?.addEventListener("submit", submitAccountSupportTicket); els.riderDestinationScope?.addEventListener("change", () => { state.riderDestinationScope = els.riderDestinationScope.value === "all" ? "all" : "preferred"; saveState(); renderAll(); void refreshMarketplace({ silent: true }); }); els.riderDestinationFilterCountry?.addEventListener("change", refreshRiderDestinationFilterOptions); els.riderDestinationFilterCity?.addEventListener("change", refreshRiderDestinationFilterOptions); els.riderDestinationFilterArea?.addEventListener("change", rememberRiderDestinationFilterControlDraft); els.riderDestinationFilterConsent?.addEventListener("change", rememberRiderDestinationFilterControlDraft); els.riderDestinationFilterText?.addEventListener("input", rememberRiderDestinationFilterControlDraft); els.riderDestinationFilterText?.addEventListener("change", rememberRiderDestinationFilterControlDraft); els.openRiderDestinationFilter?.addEventListener("click", () => setRiderWorkspacePage("destination")); els.riderDestinationFilterApply?.addEventListener("click", applyRiderDestinationFilter); els.riderDestinationFilterClear?.addEventListener("click", clearRiderDestinationFilter); els.riderDestinationFilterText?.addEventListener("keydown", (event) => { if (event.key !== "Enter") return; event.preventDefault(); applyRiderDestinationFilter(); }); els.captureRiderGps.addEventListener("click", captureRiderLiveGps); els.clearRiderGps.addEventListener("click", clearRiderLiveGps); els.startRiderTaxOnboarding?.addEventListener("click", startRiderTaxOnboarding); els.paySubscription.addEventListener("click", paySubscription); els.offerForm.addEventListener("submit", sendOffer); els.acceptFare.addEventListener("click", acceptPassengerFare); const declineRiderRequestFromButton = (event) => { event?.preventDefault?.(); event?.stopPropagation?.(); const sourceButton = event?.target?.closest?.("#dropRiderNegotiation") || event?.currentTarget; const requestId = sourceButton?.dataset?.requestId || selectedRequest()?.id || state.selectedRequestId; if (requestId && typeof ignoreRiderMarketplaceRequest === "function") { void ignoreRiderMarketplaceRequest(requestId); return; } void dropRiderNegotiation(); }; const returnRiderMarketplaceFromButton = (event) => { event?.preventDefault?.(); event?.stopPropagation?.(); if (typeof returnRiderToMarketplace === "function") { returnRiderToMarketplace({ replace: true, refresh: true }); } }; els.dropRiderNegotiation?.addEventListener("click", declineRiderRequestFromButton); els.riderMarketplaceBack?.addEventListener("click", returnRiderMarketplaceFromButton); document.addEventListener("click", (event) => { if (!event.target?.closest?.("#dropRiderNegotiation")) return; declineRiderRequestFromButton(event); }, true); document.addEventListener("click", (event) => { if (!event.target?.closest?.("#riderMarketplaceBack, [data-rider-marketplace-back]")) return; returnRiderMarketplaceFromButton(event); }, true); els.refreshMarket.addEventListener("click", () => refreshMarketplace()); els.chatForm.addEventListener("submit", sendChat); els.safetyReportForm.addEventListener("submit", submitSafetyReport); els.rideRatingForm.addEventListener("submit", submitRideRating); els.installApp.addEventListener("click", installApp); wireDeploymentUpdateEvents(); window.addEventListener("online", updateConnectionStatus); window.addEventListener("offline", updateConnectionStatus); window.addEventListener("hashchange", applyRouteTab); window.addEventListener("popstate", applyRouteTab); window.addEventListener("beforeinstallprompt", (event) => { event.preventDefault(); deferredInstallPrompt = event; updateInstallButton(); }); window.addEventListener("appinstalled", () => { deferredInstallPrompt = null; updateInstallButton(); }); } function closeWorkspaceMenus() { document.querySelectorAll(".workspace-menu-wrap[open]").forEach((menu) => { menu.removeAttribute("open"); }); } function closeWorkspaceMenusFromOutside(event) { if (!event.target?.closest?.(".workspace-menu-wrap")) closeWorkspaceMenus(); } async function installApp() { if (deferredInstallPrompt) { deferredInstallPrompt.prompt(); await deferredInstallPrompt.userChoice; deferredInstallPrompt = null; updateInstallButton(); return; } translatedAlert("androidInstallHelp"); } async function registerServiceWorker() { if (!("serviceWorker" in navigator)) return; try { await clearOldAppCaches(); navigator.serviceWorker.addEventListener("controllerchange", () => { if (serviceWorkerRefreshPending) return; serviceWorkerRefreshPending = true; window.location.reload(); }); const registration = await navigator.serviceWorker.register("sw.js", { updateViaCache: "none" }); registration.addEventListener("updatefound", () => { const installingWorker = registration.installing; if (!installingWorker) return; installingWorker.addEventListener("statechange", () => { if (installingWorker.state === "installed" && navigator.serviceWorker.controller) { handleDeploymentUpdateAvailable(registration); } }); }); await registration.update().catch(() => {}); if (registration.waiting && navigator.serviceWorker.controller) handleDeploymentUpdateAvailable(registration); } catch { if (appConfig.mode === "supabase") { updateConnectionStatus(); } else { setTranslatedStatus(els.connectionStatus, "localMode"); } } } async function finishSupabaseStartup() { await initSupabaseClient(); if (typeof restoreSignedInRoleFromSupabaseSession === "function") { await restoreSignedInRoleFromSupabaseSession(); } if (typeof handlePaymentSetupReturnFromLocation === "function") { await handlePaymentSetupReturnFromLocation(); } updateConnectionStatus(); renderAll(); } async function boot() { installClientRuntimeErrorReporting(); await loadRuntimeConfig(); hardenStateForRuntime(); if (typeof clearStalePasswordResetModeForCurrentLocation === "function") { clearStalePasswordResetModeForCurrentLocation(); } const passwordResetBootRole = typeof preparePasswordResetReturnFromLocation === "function" ? preparePasswordResetReturnFromLocation() : ""; const bootingPasswordReset = Boolean(passwordResetBootRole); const requestedTab = requestedTabFromLocation(); state.activeTab = passwordResetBootRole || availableWorkspaceTab(requestedTab ?? preferredSignedInTab() ?? state.activeTab) || defaultRuntimeTab(); if (state.activeTab === "admin" && typeof applyRequestedAdminWorkspacePageFromLocation === "function") { applyRequestedAdminWorkspacePageFromLocation(); } wireAccountAuthEvents(); populateLocationFields(); hydrateForms(); if (typeof initializeRideStopsInput === "function" && document.querySelector("#rideStops")) { initializeRideStopsInput(); } const showEntryOnBoot = bootingPasswordReset ? false : shouldShowRoleEntry(); switchTab(state.activeTab, { updateUrl: !showEntryOnBoot && !bootingPasswordReset, preserveEntry: showEntryOnBoot, resetAccountMode: Boolean(requestedTab) && !bootingPasswordReset }); updateConnectionStatus(); updateInstallButton(); wireEvents(); renderAll(); if (appConfig.mode === "supabase") { void finishSupabaseStartup().catch((error) => { els.connectionStatus.textContent = error.message; }); } registerServiceWorker(); } void boot().finally(() => { window.WAKA_RUNTIME_READY = true; });