// 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", "Buea", "Kumba", "Limbe", "Bafoussam", "Garoua", "Maroua", "Ngaoundere", "Bertoua", "Ebolowa"],
"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", "District of Columbia", "Virginia", "Pennsylvania", "New York", "California", "Texas", "Florida", "Georgia", "Illinois"],
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 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: [
{ name: "Baltimore", x: 58, y: 28 },
{ name: "Silver Spring", x: 42, y: 61 },
{ name: "Rockville", x: 33, y: 55 },
{ name: "Bethesda", x: 38, y: 64 },
{ name: "College Park", x: 48, y: 58 },
{ name: "Laurel", x: 51, y: 48 },
{ name: "Columbia", x: 50, y: 39 },
{ name: "Annapolis", x: 72, y: 55 },
{ name: "Frederick", x: 22, y: 32 },
{ name: "Gaithersburg", x: 27, y: 49 },
{ name: "Bowie", x: 62, y: 59 },
{ name: "Towson", x: 57, y: 20 }
]
}
};
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 riderProximityLimit = {
car: 7
};
const riderPickupEtaSpeedKmh = {
car: 22
};
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 carBodyTypes = ["Sedan", "SUV"];
const carTypePreferenceOptions = [
{ value: "any", label: "Any car" },
{ value: "sedan", label: "Sedan" },
{ value: "suv", label: "SUV" }
];
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,
benchmarkTripMinutes: "30-36",
benchmarkTripFareUsd: "$25-$27"
};
const kmToMiles = 0.621371;
const metersToMiles = 0.000621371;
const riderMonthlySubscriptionFee = 150;
const businessMonthlySubscriptionFee = 199;
const businessRideServiceFeeRate = 0.1;
const riderFacilitationFeeRate = 0;
const subscriptionRenewalNoticeDays = 3;
const stripeProcessingFeeRate = 0.029;
const stripeProcessingFixedUsd = 0.3;
const destinationUpdateTravelFraction = 2 / 7;
const passengerCancellationFeeConfig = {
graceMinutes: 2,
matchedBaseUsd: 3,
arrivedBaseUsd: 5,
perMinuteUsd: 1,
capFareRatio: 0.35
};
const riderPickupEtaRoadFactor = 1.35;
const riderLiveGpsFreshMinutes = 15;
const riderLiveGpsMaxAccuracyMeters = 100;
const passengerPickupGpsFreshMinutes = 20;
const passengerPickupGpsMaxAccuracyMeters = 50;
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 adminSlowRpcWarningMs = 1000;
const marketplaceSyncLoadLimits = {
ride_requests: 100,
ride_cancellation_charges: 100,
ride_payment_settlements: 100,
ride_tips: 100,
ride_offers: 250,
ride_chats: 250,
admin_notifications: 100,
business_accounts: 50,
business_subscriptions: 50,
rider_tax_identity_references: 50,
rider_tax_documents: 100,
ride_ratings: 250
};
const riderMarketplacePageSize = 40;
const defaultCitySpanKm = 14;
const cityDistanceSpanKm = {
Cameroon: {
Douala: 18,
Yaounde: 16,
Bamenda: 10,
Buea: 9,
Kumba: 10,
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",
projectName: "waka2",
mode: "demo",
mapsMode: "zones-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,
autoPickupGpsEnabled: true,
autoRiderGpsEnabled: true,
runtimeConfigFile: "",
strictProductionMode: false,
enablePhoneOtpSignIn: false,
relaxSmsVerificationForTesting: false,
firstLaunchCountry: "United States",
firstLaunchCity: "Maryland",
enabledLaunchCountries: ["United States"],
phoneVerificationMode: "supabase",
paymentProvider: "stripe",
backgroundCheckProvider: "checkr",
taxOnboardingProvider: "stripe-connect",
taxOnboardingMode: "provider-hosted",
supportPhone: "+13015550100",
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: "Subscription",
paySubscription: "Open automatic subscription 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 automatic subscription checkout",
respondRequest: "Answer ride request",
sendOffer: "Send offer",
passengerSignIn: "Passenger sign-in",
riderSignIn: "Rider sign-in",
signIn: "Sign in"
},
ar: {
tagline: "رحلات دراجة وسيارة قابلة للتفاوض",
passenger: "راكب",
rider: "سائق",
admin: "مشرف",
language: "اللغة",
installApp: "تثبيت",
createPassenger: "انشاء حساب راكب",
savePassenger: "حفظ الراكب",
postRide: "طلب رحلة",
publishRequest: "نشر الطلب",
riderApplication: "طلب السائق",
submitReview: "ارسال للمراجعة",
subscription: "اشتراك",
paySubscription: "Open automatic subscription 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 type",
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: "License or professional credential number",
vehicleRegistration: "Vehicle registration",
driverLicenseDocument: "Driver's license document",
vehicleRegistrationDocument: "Vehicle registration document",
nationalIdDocument: "Insurance document",
subscriptionIntro: "Approved riders receive 30 free days before a monthly platform fee is required.",
paymentProvider: "Payment provider",
paymentPhone: "Payment phone",
transactionReference: "Transaction reference",
subscriptionPaymentHelp: "Waka subscriptions renew automatically through the payment provider. No manual payment reference is accepted.",
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: "Payment transaction reference",
counterFarePlaceholder: "Enter a different counter-offer fare",
counterNotePlaceholder: "Optional: tell the passenger your nearby landmark, ETA, or vehicle note",
supabasePasswordPlaceholder: "Supabase password",
chatPlaceholder: "Chat opens only after passenger chooses a rider",
safetyReportDetailsPlaceholder: "Describe the concern for admin 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: "Supabase sign-in worked, but the Waka profile is missing. Save the account form once to sync profile details.",
wrongProfileRole: "This account is registered as {role}, not {type}.",
signedInPassengerLoaded: "Signed in as {email}. Passenger profile loaded. Link a payment account 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: "Save a passenger payment account before requesting rides.",
riderPaymentRequired: "Save a rider payment account before receiving requests.",
riderDailyRegionsRequired: "Choose today's rider destination regions before receiving requests.",
riderLiveGpsRequired: "Share live rider GPS before receiving requests.",
startingPassengerSupabase: "Starting passenger save in Supabase...",
savingPassenger: "Saving passenger...",
passengerCreated: "{name} passenger account created successfully. Link a payment account next, then request rides.",
passengerCreatedEmailPending: "{name} passenger account created successfully. Link a payment account 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. Link a payment account 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 immediately and monthly subscription is required after the trial.",
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: "Automatic checkout is required for Waka Rider Access.",
subscriptionAlreadyPending: "A provider subscription checkout is already in progress.",
submittingPaymentSupabase: "Opening automatic subscription checkout...",
savingPaymentReference: "Opening automatic subscription checkout...",
paymentReferenceSubmitted: "Subscription checkout opened. The provider will renew access automatically after successful payment.",
paymentReferenceFailed: "Could not open subscription 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 trial or subscription 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.",
safetyReportNeedsDetail: "Add enough detail for admin to understand the concern.",
safetyReportSignInRequired: "Sign in again before submitting a safety report.",
submittingSafetySupabase: "Submitting safety report to Supabase...",
savingSafetyReport: "Saving safety report for admin review...",
safetyReportSubmitted: "Safety report submitted for admin review.",
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",
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: "Type de 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 ou credential",
vehicleRegistration: "Immatriculation vehicule ou moto",
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: "Fournisseur de paiement",
paymentPhone: "Telephone de paiement",
transactionReference: "Reference de transaction",
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",
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: "Reference de paiement",
counterFarePlaceholder: "Entrez un autre prix de contre-offre",
counterNotePlaceholder: "Facultatif: indiquez votre repere proche, votre delai ou une note vehicule",
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: "La connexion Supabase a reussi, mais le profil Waka manque. Enregistrez le formulaire du compte pour synchroniser le profil.",
wrongProfileRole: "Ce compte est enregistre comme {role}, pas {type}.",
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.",
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, l'essai gratuit de 30 jours commence immediatement et l'abonnement mensuel sera requis apres l'essai.",
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.",
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 automatique est requis pour Waka Rider Access.",
subscriptionAlreadyPending: "Une session de paiement automatique est deja en cours.",
submittingPaymentSupabase: "Ouverture du paiement automatique...",
savingPaymentReference: "Ouverture du paiement automatique...",
paymentReferenceSubmitted: "Paiement ouvert. Le fournisseur renouvellera l'acces automatiquement 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 automatic subscription 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 automatic subscription 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: "Car type",
operatingArea: "Area wey you dey work",
subscriptionIntro: "Approved riders get 30 free days before monthly payment.",
paymentProvider: "Payment provider",
paymentPhone: "Payment phone",
transactionReference: "Transaction reference",
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: "Mtoa huduma wa malipo",
paymentPhone: "Simu ya malipo",
transactionReference: "Kumbukumbu ya muamala",
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: "Provedor de pagamento",
paymentPhone: "Telefone de pagamento",
transactionReference: "Referencia da transacao",
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: "Proveedor de pago",
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",
"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",
"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 VIN": "credentialNumber",
"Vehicle registration": "vehicleRegistration",
"Plate number": "vehicleRegistration",
"Driver's license document": "driverLicenseDocument",
"Vehicle registration document": "vehicleRegistrationDocument",
"Vehicle registration document": "vehicleRegistrationDocument",
"Insurance document": "nationalIdDocument",
"Car make": "vehicle",
"Car type/model": "vehicleType",
"Year": "dateOfBirth",
"Color": "vehicle",
"Insurance provider": "paymentProvider",
"Insurance policy number": "transactionReference",
"Approved riders receive 30 free days before a monthly platform fee is required.": "subscriptionIntro",
"Payment provider": "paymentProvider",
"Payment phone": "paymentPhone",
"Transaction reference": "transactionReference",
"Waka subscriptions renew automatically through the payment provider. No manual payment reference is accepted.": "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'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",
"Payment transaction reference": "transactionReferencePlaceholder",
"Enter a different counter-offer fare": "counterFarePlaceholder",
"Optional: tell the passenger your nearby landmark, ETA, or vehicle note": "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 = 90;
const productionLaunchLanguages = ["en", "fr", "es"];
const languageLabels = {
en: "English",
fr: "French",
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]);
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)
};
}
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;
}
function translatedAlert(key, values = {}) {
alert(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 defaultState = {
activeTab: "passenger",
showRoleEntry: true,
accountMode: {
passenger: "signin",
rider: "signin"
},
filter: "all",
language: "en",
verification: {
passenger: null,
rider: null,
passengerSignIn: null,
riderSignIn: null
},
sessions: {
passenger: null,
rider: null
},
adminSession: null,
adminDetail: null,
adminDirectorySearch: "",
adminDirectoryRegion: "",
adminDirectoryPages: {
passengers: 0,
riders: 0
},
selectedRequestId: null,
passenger: null,
rider: null,
passengers: [],
requests: [],
riders: [],
demoSeeded: false,
paymentRequests: [],
paymentAccounts: [],
businessAccounts: [],
businessSubscriptions: [],
rideSettlements: [],
rideTips: [],
riderDayPreferences: [],
backgroundChecks: [],
taxIdentityReferences: [],
taxDocuments: [],
rideRatings: [],
offers: [],
chats: [],
notifications: [],
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"];
let state = normalizeState(loadState());
let storageWriteWarningShown = false;
let stateLookupCache = null;
const phoneOtpCooldowns = new Map();
let pendingPickupGps = null;
let passengerPickupGpsPromise = null;
let selectedDestinationPlace = null;
const destinationPlaceDetailsCache = new Map();
let destinationAutocompleteTimer = null;
let destinationAutocompleteSessionToken = null;
let destinationAutocompleteRequestId = 0;
let riderGpsWatchId = null;
let riderAutoGpsPaused = false;
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
};
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",
"name",
"email",
"preferredLanguage",
"country",
"city",
"area",
"vehicle",
"carBodyType",
"status",
"approvedAt",
"trialEndsAt",
"subscriptionPaidUntil",
"rating",
"backgroundCheckStatus",
"backgroundCheckDecision",
"createdAt"
]);
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 (options.minimizeProfileData) return null;
if (!session) return 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 storageSafeAdminSession(session) {
if (session?.source !== "demo" || !session.email || !demoAdminSignInAllowed()) return null;
return {
email: session.email,
source: "demo",
signedInAt: session.signedInAt ?? new Date().toISOString()
};
}
function stateForStorage(options = {}) {
const minimizeProfileData = options.minimizeProfileData ?? shouldMinimizeStoredProfileData();
const storageOptions = { minimizeProfileData };
const safeAdminSession = storageSafeAdminSession(state.adminSession);
return {
...state,
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(state.passengers, storageOptions),
riders: storageSafeAccounts(state.riders, storageOptions)
};
}
function minimizeRuntimeProfileState() {
const storageOptions = { minimizeProfileData: true };
state.verification = {
passenger: null,
rider: null,
passengerSignIn: null,
riderSignIn: null
};
state.sessions = {
passenger: null,
rider: null
};
state.passenger = storageSafeAccount(state.passenger, storageOptions);
state.rider = storageSafeAccount(state.rider, storageOptions);
state.passengers = storageSafeAccounts(state.passengers, storageOptions);
state.riders = storageSafeAccounts(state.riders, storageOptions);
}
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 normalizeState(nextState) {
nextState.language ||= "en";
nextState.showRoleEntry = nextState.showRoleEntry !== false;
if (!["all", "car"].includes(nextState.filter)) nextState.filter = "all";
nextState.accountMode ||= { passenger: "signin", rider: "signin" };
nextState.accountMode.passenger = nextState.accountMode.passenger === "create" ? "create" : "signin";
nextState.accountMode.rider = nextState.accountMode.rider === "create" ? "create" : "signin";
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;
nextState.adminDirectorySearch ||= "";
nextState.adminDirectoryRegion ||= "";
nextState.adminDirectoryPages ||= { passengers: 0, riders: 0 };
nextState.adminDirectoryPages.passengers ||= 0;
nextState.adminDirectoryPages.riders ||= 0;
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),
rideStops: normalizeRideStops(request.rideStops ?? request.ride_stops),
estimatedDistanceMiles: request.estimatedDistanceMiles ?? request.estimated_distance_miles ?? null,
estimatedTravelMinutes: request.estimatedTravelMinutes ?? request.estimated_travel_minutes ?? null,
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.paymentRequests ||= [];
nextState.paymentAccounts ||= [];
nextState.businessAccounts ||= [];
nextState.businessSubscriptions ||= [];
nextState.rideSettlements ||= [];
nextState.rideTips ||= [];
nextState.riderDayPreferences ||= [];
nextState.backgroundChecks ||= [];
nextState.taxIdentityReferences ||= [];
nextState.taxDocuments ||= [];
nextState.rideRatings ||= [];
nextState.safetyReports ||= [];
return nextState;
}
function saveState() {
clearStateLookupIndexes();
try {
localStorage.setItem(storageKey, JSON.stringify(stateForStorage()));
} catch (error) {
if (!storageWriteWarningShown) {
console.warn("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.
const els = {
roleEntry: document.querySelector("#roleEntry"),
workspace: document.querySelector("#workspace"),
backToRoleEntry: document.querySelector("#backToRoleEntry"),
roleTabs: document.querySelector("#roleTabs"),
connectionStatus: document.querySelector("#connectionStatus"),
installApp: document.querySelector("#installApp"),
languageSelect: document.querySelector("#languageSelect"),
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"),
passengerSignInStatus: document.querySelector("#passengerSignInStatus"),
passengerAccountStage: document.querySelector("#passengerAccountStage"),
passengerSessionCard: document.querySelector("#passengerSessionCard"),
passengerSessionTitle: document.querySelector("#passengerSessionTitle"),
passengerSessionSummary: document.querySelector("#passengerSessionSummary"),
passengerSignOut: document.querySelector("#passengerSignOut"),
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"),
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"),
businessAccountStatus: document.querySelector("#businessAccountStatus"),
businessAccountList: document.querySelector("#businessAccountList"),
passengerNoticePanel: document.querySelector("#passengerNoticePanel"),
passengerNoticeList: document.querySelector("#passengerNoticeList"),
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"),
passengerCountry: document.querySelector("#passengerCountry"),
passengerCity: document.querySelector("#passengerCity"),
passengerStatus: document.querySelector("#passengerStatus"),
passengerSaveButton: document.querySelector("#passengerSaveButton"),
rideRequestForm: document.querySelector("#rideRequestForm"),
pickupArea: document.querySelector("#pickupArea"),
pickupDescription: document.querySelector("#pickupDescription"),
capturePickupGps: document.querySelector("#capturePickupGps"),
clearPickupGps: document.querySelector("#clearPickupGps"),
pickupGpsStatus: document.querySelector("#pickupGpsStatus"),
destinationArea: document.querySelector("#destinationArea"),
destination: document.querySelector("#destination"),
destinationSuggestions: document.querySelector("#destinationSuggestions"),
destinationPlaceStatus: document.querySelector("#destinationPlaceStatus"),
rideStops: document.querySelector("#rideStops"),
rideBillingAccount: document.querySelector("#rideBillingAccount"),
rideTiming: document.querySelector("#rideTiming"),
scheduledAt: document.querySelector("#scheduledAt"),
vehiclePreference: document.querySelector("#vehiclePreference"),
fareOffer: document.querySelector("#fareOffer"),
fareGuidance: document.querySelector("#fareGuidance"),
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"),
riderSignInStatus: document.querySelector("#riderSignInStatus"),
riderAccountStage: document.querySelector("#riderAccountStage"),
riderSessionCard: document.querySelector("#riderSessionCard"),
riderSessionTitle: document.querySelector("#riderSessionTitle"),
riderSessionSummary: document.querySelector("#riderSessionSummary"),
riderPaymentForm: document.querySelector("#riderPaymentForm"),
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"),
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"),
riderFlowCard: document.querySelector("#riderFlowCard"),
riderFlowTitle: document.querySelector("#riderFlowTitle"),
riderFlowSummary: document.querySelector("#riderFlowSummary"),
riderFlowSteps: document.querySelector("#riderFlowSteps"),
riderFlowMeta: document.querySelector("#riderFlowMeta"),
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"),
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"),
riderCarYear: document.querySelector("#riderCarYear"),
riderCarColor: document.querySelector("#riderCarColor"),
riderCountry: document.querySelector("#riderCountry"),
riderCity: document.querySelector("#riderCity"),
riderArea: document.querySelector("#riderArea"),
riderCredential: document.querySelector("#riderCredential"),
riderVehicleVin: document.querySelector("#riderVehicleVin"),
riderRegistration: document.querySelector("#riderRegistration"),
riderInsuranceProvider: document.querySelector("#riderInsuranceProvider"),
riderInsuranceNumber: document.querySelector("#riderInsuranceNumber"),
riderBackgroundConsent: document.querySelector("#riderBackgroundConsent"),
riderLicenseDocument: document.querySelector("#riderLicenseDocument"),
riderRegistrationDocument: document.querySelector("#riderRegistrationDocument"),
riderInsuranceDocument: document.querySelector("#riderInsuranceDocument"),
riderStatus: document.querySelector("#riderStatus"),
riderSubmitButton: document.querySelector("#riderSubmitButton"),
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"),
subscriptionPaymentStatus: document.querySelector("#subscriptionPaymentStatus"),
paySubscription: document.querySelector("#paySubscription"),
offerForm: document.querySelector("#offerForm"),
offerRequestContext: document.querySelector("#offerRequestContext"),
counterFare: document.querySelector("#counterFare"),
counterNote: document.querySelector("#counterNote"),
acceptFare: document.querySelector("#acceptFare"),
selectedSummary: document.querySelector("#selectedSummary"),
marketPanel: document.querySelector("#marketPanel"),
marketLocation: document.querySelector("#marketLocation"),
refreshMarket: document.querySelector("#refreshMarket"),
marketFilters: document.querySelector("#marketFilters"),
cityMap: document.querySelector("#cityMap"),
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"),
requestCount: document.querySelector("#requestCount"),
offerCount: document.querySelector("#offerCount"),
gpsWriteMetric: document.querySelector("#gpsWriteMetric"),
googleCallMetric: document.querySelector("#googleCallMetric"),
routeCacheMetric: document.querySelector("#routeCacheMetric"),
slowRpcMetric: document.querySelector("#slowRpcMetric"),
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"),
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;
}
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();
return ["sedan", "suv"].includes(normalized) ? normalized : "sedan";
}
function carBodyTypeLabel(value) {
const normalized = normalizeCarBodyType(value);
return normalized === "suv" ? "SUV" : "Sedan";
}
function normalizeCarTypePreference(value) {
const normalized = String(value ?? "").trim().toLowerCase();
return ["sedan", "suv"].includes(normalized) ? normalized : "any";
}
function carTypePreferenceLabel(value) {
const normalized = normalizeCarTypePreference(value);
if (normalized === "suv") return "SUV";
if (normalized === "sedan") return "Sedan";
return "Any car";
}
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 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 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 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 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() {
return enabledLaunchCountries();
}
function cityNames(country = defaultLaunchCountry()) {
return Object.keys(countries[country] ?? {});
}
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() {
els.languageSelect.value = state.language;
if (state.sessions.passenger) {
els.passengerSignInEmail.value = state.sessions.passenger.email ?? "";
els.passengerSignInPhone.value = state.sessions.passenger.phone;
setTranslatedStatus(els.passengerSignInStatus, "signedInAs", { identity: state.sessions.passenger.email ?? state.sessions.passenger.phone });
}
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;
els.passengerStatus.textContent = `${state.passenger.name} is ready to request rides in ${state.passenger.city}. Phone verified.`;
els.passengerLocationStatus.textContent = `Ride requests publish in ${state.passenger.city}, ${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);
}
}
if (state.sessions.rider) {
els.riderSignInEmail.value = state.sessions.rider.email ?? "";
els.riderSignInPhone.value = state.sessions.rider.phone;
setTranslatedStatus(els.riderSignInStatus, "signedInAs", { identity: state.sessions.rider.email ?? state.sessions.rider.phone });
}
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 = 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 = carBodyTypeLabel(state.rider.carBodyType);
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;
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.riderBackgroundConsent) els.riderBackgroundConsent.checked = Boolean(state.rider.backgroundCheckConsentAt);
els.riderLocationStatus.textContent = riderServiceAreaSummary(state.rider);
els.riderGpsStatus.textContent = gpsStatusLabel(riderCurrentGps(state.rider));
const riderPayment = paymentAccountFor("rider", state.rider.id);
if (riderPayment) {
els.riderPaymentProvider.value = riderPayment.provider;
els.riderBankName.value = riderPayment.institutionName ?? "";
els.riderAccountHolder.value = riderPayment.accountHolder ?? "";
els.riderAccountLast4.value = riderPayment.accountLast4 ?? "";
els.riderPaymentReference.value = riderPayment.reference ?? "";
els.riderPaymentStatus.textContent = paymentAccountSummary("rider", state.rider);
}
populateRiderDailyRegionOptions(state.rider.country, state.rider.city);
renderRiderDailyRegionStatus(state.rider);
}
}
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 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 nextFare = Number(String(input.value).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);
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;
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;
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),
estimatedDistanceMiles: request.estimated_distance_miles ?? null,
estimatedTravelMinutes: request.estimated_travel_minutes ?? null,
routeEstimateSource: request.route_estimate_source ?? null,
routeEstimateProvider: request.route_estimate_provider ?? null,
routeEstimateCached: Boolean(request.route_estimate_cached),
routeEstimateKey: request.route_estimate_key ?? null,
routeEstimateCreatedAt: request.route_estimate_created_at ?? null,
fareOffer: request.fare_offer_xaf,
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: Boolean(request.pickup_location),
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,
createdAt: offer.created_at
};
}
function mapPassengerApproachFromDatabase(row) {
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,
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"),
contactPhone: "",
contactRelayPhone: row.relay_phone ?? "",
contactRelayStatus: row.relay_status ?? "relay_not_configured",
contactProviderSessionId: row.provider_session_id ?? ""
};
}
function mapChatFromDatabase(message) {
return {
id: message.id,
requestId: message.ride_request_id,
senderId: message.sender_id,
sender: message.sender_id === state.rider?.id ? "rider" : message.sender_id === state.passenger?.id ? "passenger" : "system",
text: message.body,
createdAt: message.created_at
};
}
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,
createdAt: profile.created_at
};
}
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,
created_at: row.created_at
});
}
function directoryRowToRider(row) {
const documents = parseRiderDocuments(row.document_path);
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),
carYear: row.car_year ?? "",
carColor: row.car_color ?? "",
vehicleVin: row.vehicle_vin ?? "",
insuranceProvider: row.insurance_provider ?? "",
insuranceNumber: row.insurance_number ?? "",
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,
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 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),
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),
status: state.rider?.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;
}
const sdkReady = await loadSupabaseSdk();
if (!sdkReady) {
updateConnectionStatus();
return;
}
supabaseClient = window.supabase.createClient(appConfig.supabaseUrl, appConfig.supabaseAnonKey, {
auth: {
persistSession: true,
autoRefreshToken: true,
detectSessionInUrl: true
}
});
updateConnectionStatus();
}
function usesManualPhoneVerification() {
return appConfig.phoneVerificationMode === "manual";
}
function smsVerificationRelaxedForTesting() {
return configFlagEnabled(appConfig.relaxSmsVerificationForTesting) && !strictProductionModeEnabled();
}
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 demoToolsAllowed() {
return appConfig.mode === "demo" && isLocalDevelopmentHost() && !strictProductionModeEnabled();
}
function phoneOtpSignInEnabled() {
return configFlagEnabled(appConfig.enablePhoneOtpSignIn);
}
function shouldBlockClientFallbackWrites() {
return strictProductionModeEnabled();
}
function assertClientFallbackAllowed(feature, sqlFile) {
if (!shouldBlockClientFallbackWrites()) return;
throw new Error(`${feature} requires ${sqlFile} in strict production mode. 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 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", "1");
const rows = await withSupabaseTimeout(
supabaseRestRequest(`/rest/v1/rider_applications?${params.toString()}`, { accessToken }),
"Loading the rider application",
supabaseProfileSaveTimeoutMs
);
return Array.isArray(rows) ? rows[0] ?? null : 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 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;
console.warn("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;
console.warn("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";
return true;
}
async function clearRiderLiveGpsWithRpc() {
await callSupabaseRpc(
"rider_clear_live_gps",
{},
"Stopping rider live GPS",
optionalSupabaseRequestTimeoutMs
);
lastLocationUpdateSource = "location update RPC";
return true;
}
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;
console.warn("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 clearedRider = clearRiderLiveGpsFields(rider);
saveCurrentRiderRecord(clearedRider);
await clearRiderLiveGpsInSupabase(clearedRider);
if (activeRole() === "rider") {
els.riderGpsStatus.textContent = "Live GPS expired or became inaccurate; refreshing automatically before receiving requests.";
}
return true;
}
async function updateRiderApplicationReviewInSupabase(riderId, status, reviewedAt) {
const payload = {
status,
reviewed_by: state.adminSession.userId,
reviewed_at: reviewedAt
};
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;
console.warn("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: riderCanSeeRequests(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
);
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
);
} catch (error) {
console.warn("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) {
console.warn("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) {
console.warn("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) {
console.warn("Stale Supabase session could not be signed out.", error);
}
}
async function getSupabaseUser() {
if (supabaseRestSession?.user && !supabaseClient) return supabaseRestSession.user;
if (!isSupabaseMode()) return null;
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 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) console.warn("Phone verification audit event was not saved.", error);
} catch (error) {
console.warn("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_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_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";
}
async function ensureSupabaseAuthUser(profile, onStage) {
reportSupabaseStep(onStage, "Checking current Supabase session...");
const existingUser = await getSupabaseUser();
if (authUserMatchesVerifiedPhone(existingUser, profile)) {
return attachEmailPasswordToVerifiedPhoneUser(existingUser, profile, onStage);
}
if (authUserEmail(existingUser) === profile.email) 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) 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),
"Creating the Supabase auth account"
);
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)) {
throw new Error("This email already has a Supabase account. Use the correct password to continue; Waka will not create a duplicate account.");
}
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;
console.warn("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;
}
async function saveProfileToSupabase(profile, onStage, options = {}) {
if (!isSupabaseMode()) return null;
let user = await ensureSupabaseAuthUser(profile, onStage);
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}`);
console.warn("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 (!profileOnboardingRpcUnavailable.photo) {
try {
await saveProfilePhotoPathWithRpc(profilePhotoPath);
return;
} catch (error) {
if (!adminDirectoryRpcMissing(error)) throw error;
profileOnboardingRpcUnavailable.photo = true;
console.warn("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) => {
console.warn("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);
}
function storagePathCanBeSigned(path) {
return Boolean(path && typeof path === "string" && path.includes("/"));
}
function storageReviewButton(label, bucket, path) {
if (!storagePathCanBeSigned(path)) return "";
return `Open ${escapeHtml(label)} `;
}
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;
}
try {
els.adminStatus.textContent = `Creating secure link for ${label}...`;
const { data, error } = await withSupabaseTimeout(
supabaseClient.storage.from(bucket).createSignedUrl(path, 300),
`Creating secure link for ${label}`,
optionalSupabaseRequestTimeoutMs
);
if (error) throw error;
const opened = 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) {
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)))
};
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),
car_year: rider.carYear ? Number(rider.carYear) : null,
car_color: rider.carColor,
vehicle_vin: rider.vehicleVin,
insurance_provider: rider.insuranceProvider,
insurance_number: rider.insuranceNumber,
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;
console.warn("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")
.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) {
lastProfileOnboardingSource = "direct rider application update fallback";
const { error: updateError } = await withSupabaseTimeout(
supabaseClient.from("rider_applications").update(applicationPayload).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;
}
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,
status: row.status,
issuedAt: row.issued_at ?? null,
createdAt: 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 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,
comment: row.comment ?? "",
createdAt: row.created_at
};
}
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;
}
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) {
console.warn("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 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"
};
}
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;
saveState();
populateLocationFields();
hydrateForms();
switchTab(type);
setTranslatedStatus(meta.status, "signedInAs", { identity: account.email ?? account.phone });
if (type === "passenger") setTranslatedStatus(els.passengerSessionSummary, "readyToRequestRides");
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;
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) {
setTranslatedStatus(meta.status, "supabaseProfileMissing");
return true;
}
if (profile.role !== type) {
setTranslatedStatus(meta.status, "wrongProfileRole", { role: profile.role, type });
return true;
}
applySignedInProfile(type, profile, user);
state.accountMode[type] = "signin";
state.activeTab = type;
saveState();
if (type === "rider") {
await hydrateProfileFromSupabase(type);
} else {
populateLocationFields();
hydrateForms();
switchTab(type);
}
await loadMarketplaceFromSupabase();
renderAll();
setTranslatedStatus(meta.status, type === "passenger" ? "signedInPassengerLoaded" : "signedInRiderLoaded", { email });
if (type === "passenger") setTranslatedStatus(els.passengerSessionSummary, "signedInPassengerLoaded", { email });
if (type === "rider") els.riderSessionSummary.textContent = riderWorkspaceStatusMessage(currentRiderRecord());
} catch (error) {
meta.status.textContent = error.message;
}
return true;
}
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;
const user = await getSupabaseUser();
if (!user) return;
let data = null;
if (supabaseClient) {
const { data: profileData, error } = await supabaseClient
.from("profiles")
.select("*")
.eq("id", user.id)
.maybeSingle();
if (error) return;
data = profileData;
} else {
data = await selectProfileRest(user.id, "*", supabaseRestSession?.access_token);
}
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,
createdAt: data.created_at
};
state.passengers = upsertById(state.passengers, state.passenger);
}
if (type === "rider") {
let application = null;
let subscription = null;
let taxIdentityReference = null;
if (supabaseClient) {
const { data: applicationData } = await supabaseClient
.from("rider_applications")
.select("*")
.eq("rider_id", user.id)
.order("created_at", { ascending: false })
.limit(1)
.maybeSingle();
const { data: subscriptionData } = await supabaseClient
.from("rider_subscriptions")
.select("*")
.eq("rider_id", user.id)
.maybeSingle();
const { data: taxIdentityData } = await supabaseClient
.from("rider_tax_identity_references")
.select("*")
.eq("rider_id", user.id)
.maybeSingle();
application = applicationData;
subscription = subscriptionData;
taxIdentityReference = taxIdentityData;
} else {
application = await selectRiderApplicationRest(user.id, supabaseRestSession?.access_token);
subscription = await selectRiderSubscriptionRest(user.id, supabaseRestSession?.access_token);
const rows = await supabaseRestRequest(`/rest/v1/rider_tax_identity_references?rider_id=eq.${user.id}&select=*&limit=1`, {
accessToken: supabaseRestSession?.access_token
}).catch(() => []);
taxIdentityReference = rows?.[0] ?? null;
}
const documents = parseRiderDocuments(application?.document_path ?? state.rider?.documentName);
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,
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: normalizeCarBodyType(application?.car_body_type ?? state.rider?.carBodyType),
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 ?? "",
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: application?.document_path ?? state.rider?.documentName ?? "",
documents,
driverLicenseDocumentPath: documents.driverLicense,
vehicleRegistrationDocumentPath: documents.vehicleRegistration,
insuranceDocumentPath: documents.insurance,
status: application?.status ?? state.rider?.status ?? "pending",
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)
);
}
}
saveState();
populateLocationFields();
hydrateForms();
switchTab(type);
renderAll();
}
// Marketplace matching, GPS/proximity, routing links, and live market refresh helpers.
let marketRefreshInFlight = false;
let lastMarketRefreshAt = null;
let lastMarketplaceSyncSource = "not refreshed";
let lastPassengerApproachSource = "not refreshed";
let areaProximityRpcUnavailable = false;
let gpsMatchingRpcUnavailable = false;
let riderMarketplaceRpcUnavailable = false;
let passengerApproachRpcUnavailable = false;
let activeRideContactRpcUnavailable = false;
let rideRequestRpcUnavailable = false;
let lastRidePostSource = "not used";
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 fareGuidanceFromDistance(distanceMiles, minutes, stops = [], meta = {}) {
const stopCount = normalizeRideStops(stops).length;
const cleanDistanceMiles = Math.max(0.6, Number(distanceMiles) || 0);
const cleanMinutes = Math.max(1, Math.ceil(Number(minutes) || cleanDistanceMiles * 4));
const midpoint = Math.max(
fareGuidanceConfig.minFareUsd,
(fareGuidanceConfig.baseFareUsd
+ cleanDistanceMiles * fareGuidanceConfig.perMileUsd
+ cleanMinutes * fareGuidanceConfig.perMinuteUsd
+ stopCount * fareGuidanceConfig.perStopUsd)
* 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,
estimatedAt: meta.estimatedAt ?? new Date().toISOString()
};
}
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" });
}
function fareGuidanceMessage(guidance) {
if (!guidance) return "Select pickup, destination, and GPS to see the suggested fare range before publishing.";
const stops = guidance.stopCount ? `, ${guidance.stopCount} stop${guidance.stopCount === 1 ? "" : "s"}` : "";
const source = guidance.source === "google-routes"
? `${guidance.cached ? "cached " : ""}driving route`
: "local zone estimate";
return `Suggested fare range: $${guidance.min}-$${guidance.max} based on ${formatDistanceMiles(guidance.distanceMiles)}, about ${guidance.minutes} minutes${stops}, and fuel factor ${fareGuidanceConfig.fuelIndex.toFixed(2)} (${source}). Benchmark: ${fareGuidanceConfig.benchmarkTripFareUsd} for about ${fareGuidanceConfig.benchmarkTripMinutes} minutes in this market.`;
}
function updateFareGuidance() {
if (!els.fareGuidance) 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, els.rideStops?.value);
els.fareGuidance.textContent = fareGuidanceMessage(guidance);
return guidance;
}
function routeEstimatesEnabled() {
return String(appConfig.routeEstimatesProvider || "zone").toLowerCase() === "google-routes";
}
function accurateRouteEstimateRequired() {
return routeEstimatesEnabled() && (configFlagEnabled(appConfig.requireRouteEstimateBeforePublish) || strictProductionModeEnabled());
}
function routeEstimateFunctionName() {
return String(appConfig.routeEstimateFunctionName || "route-estimate").trim() || "route-estimate";
}
function destinationRouteAddress(destination, destinationAreaName, city, country) {
return compactLocationQuery([destination, 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 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);
return {
placeId: placeId || null,
displayName: displayName || formattedAddress || null,
formattedAddress: formattedAddress || null,
latitude: Number.isFinite(latitude) ? latitude : null,
longitude: Number.isFinite(longitude) ? longitude : null,
selectedAt: place.selectedAt ?? new Date().toISOString()
};
}
function destinationPlaceMatchesInput(place, destination) {
if (!place) return false;
const input = String(destination ?? "").trim().toLowerCase();
if (!input) return false;
return [place.displayName, place.formattedAddress].some((value) => String(value ?? "").trim().toLowerCase() === input);
}
function destinationPlaceForRoute(destination = els.destination?.value) {
const place = normalizedPlaceSelection(selectedDestinationPlace);
return destinationPlaceMatchesInput(place, destination) ? place : null;
}
function routeEstimateRequestBody(country, city, pickupGps, destinationAreaName, destination, stops = []) {
const selectedPlace = destinationPlaceForRoute(destination);
return {
origin: {
latitude: Number(pickupGps?.latitude),
longitude: Number(pickupGps?.longitude)
},
destination: {
address: 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),
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.");
const functionName = routeEstimateFunctionName();
if (supabaseClient?.functions?.invoke) {
const { data, error } = await withSupabaseTimeout(
supabaseClient.functions.invoke(functionName, { body }),
"Fetching accurate route estimate",
optionalSupabaseRequestTimeoutMs
);
if (error) throw error;
return data;
}
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 = []) {
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,
estimatedAt: routeEstimate.estimatedAt ?? new Date().toISOString()
}
);
}
async function accurateFareGuidanceForRide(country, city, pickupAreaName, destinationAreaName, destination, pickupGps = pendingPickupGps, stops = []) {
const fallback = fareGuidanceForRide(country, city, pickupAreaName, destinationAreaName, pickupGps, stops);
if (!routeEstimatesEnabled()) return fallback;
if (!pickupGps) {
if (accurateRouteEstimateRequired()) throw new Error("Exact pickup GPS is required before accurate route pricing.");
return fallback;
}
const body = routeEstimateRequestBody(country, city, pickupGps, destinationAreaName, destination, stops);
if (!body.destination.address) {
if (accurateRouteEstimateRequired()) throw new Error("Destination address is required before accurate route pricing.");
return fallback;
}
try {
const estimate = await fetchRouteEstimateFromEdge(body);
return fareGuidanceFromRouteEstimate(estimate, stops) ?? fallback;
} catch (error) {
console.warn("Accurate route estimate failed; falling back to zone guidance.", error);
if (accurateRouteEstimateRequired()) {
throw new Error(`Accurate driving distance is required before publishing: ${error.message}`);
}
return fallback;
}
}
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);
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 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;
const accuracy = point.accuracyMeters ? `, about ${point.accuracyMeters} m accuracy` : "";
return `GPS captured${accuracy}`;
}
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 "Pickup GPS capture time is unavailable. Capture it again so nearby riders are matched from a verified pickup point.";
}
if (ageMinutes > passengerPickupGpsFreshMinutes) {
return `Pickup GPS is ${ageMinutes} minutes old. Capture it again so nearby riders are matched to the current pickup point.`;
}
if (pickupGps.accuracyMeters == null) {
return "Pickup GPS accuracy is unavailable. Capture it again or use the area/landmark fallback.";
}
if (pickupGps.accuracyMeters > passengerPickupGpsMaxAccuracyMeters) {
return `Pickup GPS accuracy is about ${pickupGps.accuracyMeters} m. Move closer to the pickup point or use the area/landmark fallback.`;
}
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 "Live rider GPS accuracy is unavailable. Capture it again before sharing live GPS for matching.";
}
if (liveGps.accuracyMeters > riderLiveGpsMaxAccuracyMeters) {
return `Live rider GPS accuracy is about ${liveGps.accuracyMeters} m. Move into a clearer spot before sharing live GPS for 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;
const accuracyMeters = request.pickupGpsAccuracyMeters ?? pickupGps?.accuracyMeters;
const capturedAtValue = request.pickupGpsCapturedAt ?? pickupGps?.capturedAt;
const accuracy = accuracyMeters
? `about ${accuracyMeters} m accuracy`
: "accuracy unknown";
const capturedAt = capturedAtValue
? formatGpsAgeLabel({ capturedAt: capturedAtValue })
: "capture time unknown";
return `Pickup GPS: ${accuracy}, ${capturedAt}`;
}
function requestPickupGps(request) {
return normalizeGpsPoint(request?.pickupGps ?? {
latitude: request?.pickupLatitude,
longitude: request?.pickupLongitude,
accuracyMeters: request?.pickupGpsAccuracyMeters,
capturedAt: request?.pickupGpsCapturedAt
});
}
function pickupGpsIsUsableForMatching(request) {
const pickupGps = requestPickupGps(request);
if (!pickupGps) return false;
if (pickupGps.accuracyMeters == null || pickupGps.accuracyMeters > passengerPickupGpsMaxAccuracyMeters) 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()) {
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: riderProximityLimit[rider.vehicle] ?? riderProximityLimit.car,
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);
}
function riderWithinGpsProximity(request, rider = currentRiderRecord()) {
if (!request || !rider) return false;
const distanceKm = gpsDistanceKmForRequest(request, rider);
return distanceKm != null && distanceKm <= riderServiceRadius(rider);
}
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 riderCanReviewAnotherImmediateRequest(request, rider = currentRiderRecord()) {
const activeRide = riderActiveImmediateRide(rider);
return !activeRide || activeRide.id === request?.id || isScheduledRequest(request);
}
function selectedRequest() {
return stateLookupIndexes().requestMap.get(state.selectedRequestId) ?? null;
}
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() {
return state.rider ? stateLookupIndexes().riderMap.get(state.rider.id) ?? state.rider : null;
}
function requestBelongsToPassenger(request) {
return Boolean(request && state.passenger && request.passengerId === state.passenger.id);
}
function offerBelongsToRider(offer) {
return Boolean(state.rider && offer.riderId === state.rider.id);
}
function selectedRiderIdForRequest(request) {
if (!request) return null;
const selectedOffer = stateLookupIndexes().offerMap.get(request.selectedOfferId);
return request.selectedRiderId ?? selectedOffer?.riderId ?? null;
}
function selectedRiderNameForRequest(request) {
if (!request) return null;
const selectedRiderId = selectedRiderIdForRequest(request);
const rider = stateLookupIndexes().riderMap.get(selectedRiderId);
return request.selectedRiderName ?? rider?.name ?? 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 selectedRiderIdForRequest(request) === state.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 requestMatchesRiderVehicle(request, rider = currentRiderRecord()) {
if (!request || !rider) return false;
if (request.vehicle !== "car" || rider.vehicle !== "car") return false;
const preference = normalizeCarTypePreference(request.carTypePreference);
return preference === "any" || normalizeCarBodyType(rider.carBodyType) === preference;
}
function isScheduledRequest(request) {
return Boolean(request?.scheduledAt);
}
function scheduleChip(request) {
return isScheduledRequest(request) ? `Scheduled: ${formatDateTime(request.scheduledAt)}` : "Immediate ride";
}
function rideStatusLabel(request) {
return {
open: "Open",
matched: "Matched",
arrived: "Rider arrived",
in_progress: "Ride in progress",
completed: "Completed",
cancelled: "Cancelled"
}[request?.status] ?? request?.status ?? "Unknown";
}
function proximityChip(request, rider = currentRiderRecord()) {
if (activeRole() !== "rider") return null;
if (selectedRiderIdForRequest(request) === rider?.id) return `Matched to you: ${request.pickupArea}`;
const model = pickupProximityModel(request, rider);
if (!model) return null;
return `${model.source}: ${formatDistanceKm(model.distanceKm)}, ${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;
const source = pickupDistanceSourceLabel(offer.distanceSource);
return `Offer distance: ${formatDistanceKm(distanceKm)} from pickup, ${formatPickupEta(pickupEtaMinutes(distanceKm, rider))} (${source})`;
}
const model = pickupProximityModel(request, rider);
if (!model) return null;
return `Rider is ${formatDistanceKm(model.distanceKm)} from pickup; ${formatPickupEta(model.etaMinutes)}`;
}
function pickupDistanceSourceLabel(source) {
return {
postgis: "GPS/PostGIS",
gps: "GPS",
area_estimate: "area estimate"
}[source] ?? source ?? "area estimate";
}
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 (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),
capturedAt: request.riderApproachCapturedAt ?? null,
accuracyMeters: request.riderApproachAccuracyMeters ?? null
};
}
const selectedOffer = selectedOfferForRequest(request);
if (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",
capturedAt: null,
accuracyMeters: null
};
}
const model = pickupProximityModel(request, rider);
if (!model) return null;
return {
source: model.source,
distanceKm: model.distanceKm,
etaMinutes: model.etaMinutes,
isLive: model.source === "GPS/PostGIS" || model.source === "GPS",
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: ${formatDistanceKm(model.distanceKm)}, ${formatPickupEta(model.etaMinutes)} (${model.source})`;
}
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 destinationMapsQuery(request) {
return compactLocationQuery([request?.destination, request?.city, request?.country]);
}
function googleMapsSearchUrl(query) {
if (!query) return "";
return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(query)}`;
}
function googleMapsDirectionsUrl(destination, origin = null) {
if (!destination) return "";
const params = new URLSearchParams({
api: "1",
destination,
travelmode: "driving"
});
if (origin) params.set("origin", origin);
return `https://www.google.com/maps/dir/?${params.toString()}`;
}
function riderPickupNavigationUrl(request, rider = currentRiderRecord()) {
return googleMapsDirectionsUrl(pickupMapsDestination(request), mapsCoordinate(riderCurrentFreshGps(rider)));
}
function wazeNavigationUrl(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);
}
return `https://waze.com/ul?${params.toString()}`;
}
function riderPickupWazeUrl(request) {
return wazeNavigationUrl(pickupMapsDestination(request));
}
function pickupMapUrl(request) {
return googleMapsSearchUrl(pickupMapsDestination(request));
}
function destinationMapUrl(request) {
return googleMapsSearchUrl(destinationMapsQuery(request));
}
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)
&& paymentAccountReady("rider", rider)
&& riderDailyRegionsReady(rider));
}
function riderCanSeeRequests(rider = currentRiderRecord()) {
return Boolean(riderBaseReadyForRequests(rider) && riderCurrentFreshGps(rider));
}
function roleCanSeeRequest(request) {
if (!request) return false;
if (activeRole() === "passenger") {
return requestBelongsToPassenger(request);
}
if (activeRole() === "rider") {
const rider = currentRiderRecord();
if (!riderCanSeeRequests(rider)) return false;
const vehicleMatches = requestMatchesRiderVehicle(request, rider);
const ownMatchedRequest = requestHasRiderMatch(request);
const isActionable = request.status === "open" || ownMatchedRequest;
const gpsDistanceKm = gpsDistanceKmForRequest(request, rider);
const isNearEnough = ownMatchedRequest
|| (gpsDistanceKm != null
? gpsDistanceKm <= riderServiceRadius(rider)
: riderWithinRequestProximity(request, rider));
const destinationAllowed = ownMatchedRequest || requestDestinationMatchesDailyRegions(request, rider);
const notBusyElsewhere = ownMatchedRequest || riderCanReviewAnotherImmediateRequest(request, rider);
return vehicleMatches && isActionable && isNearEnough && destinationAllowed && notBusyElsewhere;
}
return false;
}
function visibleRequestsForRole() {
const { country, city } = activeMarketLocation();
return state.requests
.filter((item) => item.country === country && item.city === city)
.filter(requestMatchesVehicleFilter)
.filter(roleCanSeeRequest)
.sort((a, b) => {
if (activeRole() === "rider") {
const rider = currentRiderRecord();
const aPickupEta = pickupProximitySortValue(a, rider);
const bPickupEta = pickupProximitySortValue(b, rider);
if (aPickupEta !== bPickupEta) return aPickupEta - bPickupEta;
}
return new Date(b.createdAt) - new Date(a.createdAt);
});
}
function visibleOffersForRole(request) {
if (!request || !roleCanSeeRequest(request)) return [];
const offers = offersForRequest(request.id);
if (activeRole() === "rider") {
return offers.filter(offerBelongsToRider);
}
return sortOffersForPassenger(offers, request);
}
function canChatOnRequest(request) {
if (!request || !rideLifecycleChatStatuses.includes(request.status)) return false;
if (activeRole() === "passenger") return requestBelongsToPassenger(request);
if (activeRole() === "rider") return selectedRiderIdForRequest(request) === state.rider?.id;
return false;
}
function offerFareDifference(offer, request) {
return Math.abs(Number(offer?.fare ?? 0) - Number(request?.fareOffer ?? 0));
}
function sortOffersForPassenger(offers, request) {
return [...offers].sort((a, b) => {
const difference = offerFareDifference(a, request) - offerFareDifference(b, request);
if (difference !== 0) return difference;
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;
const delta = Number(offer?.fare ?? 0) - Number(request.fareOffer ?? 0);
if (!Number.isFinite(delta)) return null;
if (delta === 0) return "Matches passenger offer";
const direction = delta > 0 ? "above" : "below";
return `${formatMoney(Math.abs(delta))} ${direction} passenger offer`;
}
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 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 supabaseRestRequest("/rest/v1/rpc/rider_marketplace_requests", {
method: "POST",
body
});
}
const { data, error } = await supabaseClient.rpc("rider_marketplace_requests", body);
if (error) throw error;
return data ?? [];
}
async function fetchRiderMarketplaceGpsRpcRows(rider) {
const body = riderMarketplaceRpcBody(rider);
if (!supabaseClient) {
return supabaseRestRequest("/rest/v1/rpc/rider_marketplace_requests_gps", {
method: "POST",
body
});
}
const { data, error } = await supabaseClient.rpc("rider_marketplace_requests_gps", body);
if (error) throw error;
return data ?? [];
}
async function fetchPassengerApproachRows() {
if (!supabaseClient) {
return supabaseRestRequest("/rest/v1/rpc/passenger_active_ride_approach", {
method: "POST",
body: {}
});
}
const { data, error } = await supabaseClient.rpc("passenger_active_ride_approach");
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;
console.warn("Passenger active ride approach RPC is not installed yet. Falling back to offer distance snapshots.", error);
return false;
}
}
async function fetchActiveRideContactRows() {
if (!supabaseClient) {
return supabaseRestRequest("/rest/v1/rpc/active_ride_contacts", {
method: "POST",
body: {}
});
}
const { data, error } = await supabaseClient.rpc("active_ride_contacts");
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;
console.warn("Active ride contact RPC is not installed yet. Text chat still works after rider selection.", error);
return false;
}
}
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: rows,
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;
console.warn(
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: rows,
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;
console.warn(
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";
return selectRuntimeTable("ride_requests", "*", "created_at", runtimeTableLoadOptions("ride_requests", marketplaceSyncLoadLimits));
}
async function loadMarketplaceFromSupabase() {
if (!hasSupabaseRuntime() || (!hasSignedIn("passenger") && !hasSignedIn("rider"))) return;
const [requestsResult, offersResult, chatsResult, notificationsResult, paymentAccountsResult, businessAccountsResult, businessSubscriptionsResult, riderDayPreferencesResult, taxIdentityResult, taxDocumentsResult, rideRatingsResult, rideSettlementsResult, rideTipsResult] = await Promise.all([
selectMarketplaceRequests(),
selectRuntimeTable("ride_offers", "*", "created_at", runtimeTableLoadOptions("ride_offers", marketplaceSyncLoadLimits)),
selectRuntimeTable("ride_chats", "*", "created_at", runtimeTableLoadOptions("ride_chats", marketplaceSyncLoadLimits)),
selectRuntimeTable("admin_notifications", "*", "created_at", runtimeTableLoadOptions("admin_notifications", marketplaceSyncLoadLimits)),
selectRuntimeTable("payment_accounts", "*", "updated_at", runtimeTableLoadOptions("payment_accounts", marketplaceSyncLoadLimits)),
selectRuntimeTable("business_accounts", "*", "updated_at", runtimeTableLoadOptions("business_accounts", marketplaceSyncLoadLimits)),
selectRuntimeTable("business_subscriptions", "*", "updated_at", runtimeTableLoadOptions("business_subscriptions", marketplaceSyncLoadLimits)),
selectRuntimeTable("rider_day_preferences", "*", "updated_at", runtimeTableLoadOptions("rider_day_preferences", marketplaceSyncLoadLimits)),
selectRuntimeTable("rider_tax_identity_references", "*", "updated_at", runtimeTableLoadOptions("rider_tax_identity_references", marketplaceSyncLoadLimits)),
selectRuntimeTable("rider_tax_documents", "*", "created_at", runtimeTableLoadOptions("rider_tax_documents", marketplaceSyncLoadLimits)),
selectRuntimeTable("ride_ratings", "*", "created_at", runtimeTableLoadOptions("ride_ratings", marketplaceSyncLoadLimits)),
selectRuntimeTable("ride_payment_settlements", "*", "created_at", runtimeTableLoadOptions("ride_payment_settlements", marketplaceSyncLoadLimits)),
selectRuntimeTable("ride_tips", "*", "created_at", runtimeTableLoadOptions("ride_tips", marketplaceSyncLoadLimits))
]);
const offers = (offersResult.data ?? []).map(mapOfferFromDatabase);
const offerMap = new Map(offers.map((offer) => [offer.id, offer]));
const requests = (requestsResult.data ?? []).map((request) => mapRideRequestFromDatabase(request, new Map(), offerMap));
const chats = (chatsResult.data ?? []).map(mapChatFromDatabase);
const notifications = (notificationsResult.data ?? []).map(mapAdminNotificationFromDatabase);
const paymentAccounts = (paymentAccountsResult.data ?? []).map((account) => mapPaymentAccountFromDatabase(account));
const businessAccounts = (businessAccountsResult.data ?? []).map((account) => mapBusinessAccountFromDatabase(account));
const businessSubscriptions = (businessSubscriptionsResult.data ?? []).map(mapBusinessSubscriptionFromDatabase);
const riderDayPreferences = (riderDayPreferencesResult.data ?? []).map((preference) => mapRiderDayPreferenceFromDatabase(preference));
const taxIdentityReferences = (taxIdentityResult.data ?? []).map((reference) => mapTaxIdentityReferenceFromDatabase(reference));
const taxDocuments = (taxDocumentsResult.data ?? []).map((document) => mapTaxDocumentFromDatabase(document));
const rideRatings = (rideRatingsResult.data ?? []).map((rating) => mapRideRatingFromDatabase(rating));
const rideSettlements = (rideSettlementsResult.data ?? []).map((settlement) => mapRideSettlementFromDatabase(settlement));
const rideTips = (rideTipsResult.data ?? []).map((tip) => mapRideTipFromDatabase(tip));
requests.forEach((request) => {
state.requests = upsertById(state.requests, request);
});
offers.forEach((offer) => {
state.offers = upsertById(state.offers, offer);
});
chats.forEach((message) => {
state.chats = upsertById(state.chats, message);
});
notifications.forEach((notification) => {
state.notifications = upsertById(state.notifications, notification);
});
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);
});
riderDayPreferences.forEach((preference) => {
state.riderDayPreferences = upsertById(
state.riderDayPreferences.filter((item) => !(item.riderId === preference.riderId && item.serviceDate === preference.serviceDate)),
preference
);
});
taxIdentityReferences.forEach((reference) => {
state.taxIdentityReferences = upsertById(
state.taxIdentityReferences.filter((item) => item.riderId !== reference.riderId),
reference
);
});
taxDocuments.forEach((document) => {
state.taxDocuments = upsertById(state.taxDocuments, document);
});
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);
});
await loadPassengerApproachFromSupabase();
await loadActiveRideContactsFromSupabase();
saveState();
}
async function refreshMarketplace({ silent = false } = {}) {
if (!canRefreshMarketplace() || marketRefreshInFlight) return;
marketRefreshInFlight = true;
els.refreshMarket.disabled = true;
if (!silent) els.selectedSummary.textContent = "Refreshing shared marketplace from Supabase...";
try {
await expireRiderLiveGpsIfNeeded();
await loadMarketplaceFromSupabase();
lastMarketRefreshAt = new Date();
if (!silent) {
els.selectedSummary.textContent = `Marketplace refreshed ${lastMarketRefreshAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.`;
}
renderAll();
} catch (error) {
if (!silent) els.selectedSummary.textContent = `Market refresh failed: ${error.message}`;
} finally {
marketRefreshInFlight = false;
els.refreshMarket.disabled = false;
}
}
function clearSelectedRequestOutsideLocation(country, city) {
const request = selectedRequest();
if (request && (request.country !== country || request.city !== city)) {
state.selectedRequestId = null;
}
}
// Payment preferences, subscriptions, business billing, settlements, tips, ratings, and tax access helpers.
const subscriptionFee = riderMonthlySubscriptionFee;
const trialDays = 30;
const riderSubscriptionPlans = {
monthly: {
label: "Waka Rider Access",
amount: riderMonthlySubscriptionFee,
description: `$${riderMonthlySubscriptionFee}/month after the first free month, 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 agreedFareForRequest(request) {
return Number(request?.agreedFare ?? offersForRequest(request?.id).find((offer) => offer.id === request?.selectedOfferId)?.fare ?? request?.fareOffer ?? 0);
}
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 cancellationFeeText(request) {
const amount = Number(request?.cancellationFeeAmount ?? 0);
if (amount > 0) {
return `Passenger cancellation fee: ${formatMoney(amount, request.country)} (${request.cancellationFeeStatus ?? "pending"}).`;
}
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) {
return request?.businessAccountId ? Math.ceil(Number(fareCents || 0) * businessRideServiceFeeRate) : 0;
}
function rideFinancialBreakdown(request, tipAmount = 0) {
const rider = selectedRiderForRequest(request);
const fareCents = dollarsToCents(agreedFareForRequest(request));
const tipCents = dollarsToCents(tipAmount);
const grossCents = fareCents + tipCents;
const stripeFeeCents = 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)}.`
: "";
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",
agree_before_ride: "decide",
online_card: "online_card",
online_wallet: "online_wallet"
}[value] ?? "decide";
}
function paymentToDatabase(value) {
return {
cash: "cash",
mtn: "mtn_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 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: "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 currentAccountNotifications(type) {
const account = type === "passenger" ? state.passenger : state.rider;
if (!account?.id) return [];
return state.notifications
.filter((notification) => notification.recipientId === account.id)
.sort((a, b) => new Date(b.createdAt ?? 0) - new Date(a.createdAt ?? 0));
}
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;
}
function paymentAccountReady(role, account) {
const userId = account?.id;
return Boolean(paymentAccountFor(role, userId)?.status === "linked");
}
function paymentAccountSummary(role, account) {
const paymentAccount = paymentAccountFor(role, account?.id);
if (!account) return "Sign in before saving a payment account.";
if (!paymentAccount) return "Payment account required before using rides.";
return `${paymentAccount.provider} ${paymentAccount.accountType} ending ${paymentAccount.accountLast4} is ${paymentAccount.status}.`;
}
function selectedSubscriptionPlanKey() {
const value = els.subscriptionPlan?.value || "monthly";
return riderSubscriptionPlans[value] ? value : "monthly";
}
function riderPlanSummary() {
const plan = riderSubscriptionPlans.monthly;
return `${plan.label}: ${plan.description}. Waka takes $0 from each ride fare; rider payout is fare minus Stripe/payment processing only.`;
}
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 businessAccountCanRequest(account) {
const subscription = businessSubscriptionFor(account?.id);
const paidUntil = subscription?.paidUntil ? new Date(subscription.paidUntil) : null;
return Boolean(account?.status === "active"
&& subscription?.status === "active"
&& paidUntil
&& paidUntil.getTime() >= Date.now());
}
function businessAccountSummary(account) {
const subscription = businessSubscriptionFor(account?.id);
const fee = formatMoney(businessMonthlySubscriptionFee);
const serviceFee = `${Math.round(businessRideServiceFeeRate * 100)}% business ride service fee`;
if (!account) return `Business rides require a ${fee} monthly subscription plus a ${serviceFee} on completed business rides.`;
if (businessAccountCanRequest(account)) return `${account.businessName} is active for business ride billing until ${formatDate(subscription.paidUntil)}; completed business rides add a ${serviceFee}.`;
if (subscription) return `${account.businessName} is ${account.status}; ${fee}/month subscription is ${subscription.status}. Admin or Stripe webhook must activate it before business rides can be posted.`;
return `${account.businessName} is ${account.status}; ${fee}/month subscription setup is pending.`;
}
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 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) => Number(b.taxYear) - Number(a.taxYear));
}
function rideRatingRecords() { return state.rideRatings; }
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 averageRatingForRider(riderId) {
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 };
}
function ratingSummaryForRider(riderId) {
const rating = averageRatingForRider(riderId);
return rating ? `${rating.average.toFixed(1)} stars from ${rating.count} rating${rating.count === 1 ? "" : "s"}` : "new";
}
function requestDestinationText(request) {
const area = request?.destinationArea;
const detail = request?.destination;
const destinationText = area && detail && area !== detail ? `${area} - ${detail}` : 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;
if (["open", "matched", "arrived"].includes(request.status)) return true;
if (request.status !== "in_progress" || !request.startedAt) return false;
const elapsedMinutes = (Date.now() - new Date(request.startedAt).getTime()) / 60000;
return elapsedMinutes <= destinationUpdateWindowMinutes(request);
}
function requestDestinationMatchesDailyRegions(request, rider = currentRiderRecord()) {
if (!request || !rider) return false;
if (requestHasRiderMatch(request)) 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 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 trial = rider.trialEndsAt ? new Date(rider.trialEndsAt) : null;
const paid = rider.subscriptionPaidUntil ? new Date(rider.subscriptionPaidUntil) : null;
if (paid && (!trial || paid > trial)) return rider.subscriptionPaidUntil;
return rider.trialEndsAt ?? rider.subscriptionPaidUntil ?? null;
}
function riderAccessLabel(rider) {
if (!rider?.subscriptionPaidUntil) return "free trial";
const paid = new Date(rider.subscriptionPaidUntil);
const trial = rider.trialEndsAt ? new Date(rider.trialEndsAt) : null;
return !trial || paid > trial ? "subscription" : "free trial";
}
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,
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,
status: row.status,
createdAt: row.created_at,
updatedAt: row.updated_at
};
}
function mapBusinessSubscriptionFromDatabase(row) {
return {
id: row.id,
businessAccountId: row.business_account_id,
amount: centsToDollars(row.amount_cents),
provider: row.provider,
reference: row.provider_reference ?? "",
paidUntil: row.paid_until ?? null,
status: row.status,
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 ?? "",
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 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 : [],
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;
console.warn("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;
console.warn("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
};
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;
console.warn("Business account RPC is not installed yet. Falling back to direct business account insert.", error);
}
}
assertClientFallbackAllowed("Business account setup", "supabase-business-accounts.sql");
lastBusinessAccountSource = "direct business account insert fallback";
const payload = {
owner_id: account.ownerId,
business_name: account.businessName,
billing_email: account.billingEmail,
status: account.status ?? "pending"
};
if (supabaseClient) {
const { data, error } = await withSupabaseTimeout(
supabaseClient
.from("business_accounts")
.insert(payload)
.select("*")
.single(),
"Creating the business account",
optionalSupabaseRequestTimeoutMs
);
if (error) throw error;
return mapBusinessAccountFromDatabase(data, new Map([[account.ownerId, { full_name: account.ownerName }]]));
}
const data = await withSupabaseTimeout(
supabaseRestRequest("/rest/v1/business_accounts", {
method: "POST",
body: payload,
headers: { Prefer: "return=representation" }
}),
"Creating the business account",
optionalSupabaseRequestTimeoutMs
);
return mapBusinessAccountFromDatabase(Array.isArray(data) ? data[0] : data, new Map([[account.ownerId, { full_name: account.ownerName }]]));
}
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;
console.warn("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,
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();
return {
id: paymentAccountFor(type, account?.id)?.id ?? makeId("payacct"),
userId: account?.id,
userName: account?.name,
role: type,
provider: els[`${prefix}PaymentProvider`].value,
accountType: "bank_account",
accountHolder: els[`${prefix}AccountHolder`].value.trim(),
accountLast4: els[`${prefix}AccountLast4`].value.trim(),
institutionName: els[`${prefix}BankName`].value.trim(),
reference: els[`${prefix}PaymentReference`].value.trim(),
status: "linked",
createdAt: paymentAccountFor(type, account?.id)?.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 = "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 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 automatic subscription checkout for ${account.businessName}...`;
const checkout = await startSubscriptionCheckout("business_subscription", account.id);
els.businessAccountStatus.textContent = "Business subscription checkout opened. Access renews automatically after successful payment.";
window.location.assign(checkout.url);
} catch (error) {
els.businessAccountStatus.textContent = `Could not open business subscription checkout: ${error.message}`;
}
}
async function startSubscriptionCheckout(kind, entityId) {
if (!hasSupabaseRuntime()) {
throw new Error("Automatic subscription checkout requires the Supabase production runtime.");
}
const body = kind === "business_subscription"
? { kind, businessAccountId: entityId }
: { kind, riderId: entityId };
let responsePayload = null;
if (supabaseClient?.functions?.invoke) {
const { data, error } = await withSupabaseTimeout(
supabaseClient.functions.invoke("subscription-checkout-start", { body }),
"Starting automatic subscription 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 automatic subscription 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;
try {
setTranslatedStatus(els.subscriptionPaymentStatus, "submittingPaymentSupabase");
const checkout = await startSubscriptionCheckout("rider_subscription", rider.id);
saveState();
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";
let marketplaceActionRpcUnavailable = {
fare: false,
offer: false,
selection: false,
chat: false
};
let lastMarketplaceActionSource = "not used";
let safetyReportRpcUnavailable = {
submit: false,
review: false
};
let lastSafetyReportSource = "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),
estimated_distance_miles: request.estimatedDistanceMiles,
estimated_travel_minutes: request.estimatedTravelMinutes,
route_estimate_source: request.routeEstimateSource ?? "zone",
route_estimate_provider: request.routeEstimateProvider ?? "zone",
route_estimate_cached: Boolean(request.routeEstimateCached),
route_estimate_key: request.routeEstimateKey ?? null,
route_estimate_created_at: request.routeEstimateCreatedAt ?? null,
fare_offer_xaf: request.fareOffer,
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_estimated_distance_miles: request.estimatedDistanceMiles,
p_estimated_travel_minutes: request.estimatedTravelMinutes,
p_route_estimate_source: request.routeEstimateSource ?? "zone",
p_route_estimate_provider: request.routeEstimateProvider ?? "zone",
p_route_estimate_cached: Boolean(request.routeEstimateCached),
p_route_estimate_key: request.routeEstimateKey ?? null,
p_route_estimate_created_at: request.routeEstimateCreatedAt ?? null,
p_fare_offer_xaf: request.fareOffer,
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) return mapRideRequestFromDatabase(row);
} catch (error) {
if (!adminDirectoryRpcMissing(error)) throw error;
rideRequestRpcUnavailable = true;
console.warn("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);
}
async function updateRideRequestFareInSupabase(requestId, fareOffer) {
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) 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;
console.warn("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;
console.warn(
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 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;
console.warn("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;
}
async function saveChatMessageToSupabase(message) {
if (!hasSupabaseRuntime()) return;
const senderId = currentActorIdForChat();
if (!senderId) return;
const body = message.sender === "system" ? `[System] ${message.text}` : message.text;
try {
if (!marketplaceActionRpcUnavailable.chat) {
try {
const rpcBody = {
p_ride_request_id: message.requestId,
p_body: body
};
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";
return;
} catch (error) {
if (!adminDirectoryRpcMissing(error)) throw error;
marketplaceActionRpcUnavailable.chat = true;
console.warn("Ride chat RPC is not installed yet. Falling back to direct chat insert.", error);
}
}
const payload = {
ride_request_id: message.requestId,
sender_id: senderId,
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;
}
const { error } = await withSupabaseTimeout(
supabaseClient.from("ride_chats").insert(payload),
"Saving the chat message",
optionalSupabaseRequestTimeoutMs
);
if (error) throw error;
} catch (error) {
console.warn("Chat message was not synced to Supabase.", 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;
console.warn("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 saveRideRatingToSupabase(rating) {
if (!hasSupabaseRuntime()) return rating;
const rpcBody = {
p_ride_request_id: rating.requestId,
p_rated_user_id: rating.ratedUserId,
p_score: rating.score,
p_comment: rating.comment
};
try {
const data = supabaseClient
? await withSupabaseTimeout(
supabaseClient.rpc("submit_ride_rating", rpcBody),
"Submitting the ride rating",
optionalSupabaseRequestTimeoutMs
)
: await withSupabaseTimeout(
supabaseRestRequest("/rest/v1/rpc/submit_ride_rating", {
method: "POST",
body: rpcBody
}),
"Submitting the ride rating",
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 mapRideRatingFromDatabase(row);
throw new Error("Ride rating RPC did not return a saved rating.");
} catch (error) {
if (adminDirectoryRpcMissing(error)) {
console.warn("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;
}
}
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)) {
console.warn("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) {
if (!hasSupabaseRuntime()) return null;
const body = {
p_request_id: request.id,
p_destination_area: request.destinationArea,
p_destination: nextDestination,
p_destination_place_id: guidance?.destinationPlaceId ?? null,
p_destination_formatted_address: guidance?.destinationFormattedAddress ?? null,
p_destination_lat: guidance?.destinationLatitude ?? null,
p_destination_lng: guidance?.destinationLongitude ?? null,
p_ride_stops: normalizeRideStops(request.rideStops),
p_estimated_distance_miles: guidance?.distanceMiles ?? request.estimatedDistanceMiles ?? null,
p_estimated_travel_minutes: guidance?.minutes ?? request.estimatedTravelMinutes ?? null,
p_route_estimate_source: guidance?.source ?? request.routeEstimateSource ?? "zone",
p_route_estimate_provider: guidance?.provider ?? request.routeEstimateProvider ?? "zone",
p_route_estimate_cached: Boolean(guidance?.cached ?? request.routeEstimateCached),
p_route_estimate_key: guidance?.routeKey ?? request.routeEstimateKey ?? 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;
}
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 request = state.requests.find((item) => item.id === requestId);
if (!canUpdateRideDestination(request)) {
status.textContent = "Destination updates are closed for this ride.";
return;
}
const nextDestination = input.value.trim();
if (nextDestination.length < 3) {
status.textContent = "Enter a clearer destination before updating.";
return;
}
let guidance = fareGuidanceForRide(
request.country,
request.city,
request.pickupArea,
request.destinationArea,
requestPickupGps(request),
request.rideStops
);
try {
if (routeEstimatesEnabled()) {
status.textContent = "Checking updated driving distance...";
guidance = await accurateFareGuidanceForRide(
request.country,
request.city,
request.pickupArea,
request.destinationArea,
nextDestination,
requestPickupGps(request),
request.rideStops
);
}
status.textContent = "Updating destination...";
const saved = await updateRideDestinationInSupabase(request, nextDestination, guidance);
updateRequestById(request.id, (item) => ({
...item,
destination: saved?.destination ?? nextDestination,
destinationArea: saved?.destination_area ?? item.destinationArea,
destinationPlaceId: saved?.destination_place_id ?? null,
destinationFormattedAddress: saved?.destination_formatted_address ?? null,
destinationLatitude: saved?.destination_lat ?? null,
destinationLongitude: saved?.destination_lng ?? null,
rideStops: normalizeRideStops(saved?.ride_stops ?? item.rideStops),
estimatedDistanceMiles: saved?.estimated_distance_miles ?? guidance?.distanceMiles ?? item.estimatedDistanceMiles,
estimatedTravelMinutes: saved?.estimated_travel_minutes ?? guidance?.minutes ?? item.estimatedTravelMinutes,
routeEstimateSource: saved?.route_estimate_source ?? guidance?.source ?? item.routeEstimateSource,
routeEstimateProvider: saved?.route_estimate_provider ?? guidance?.provider ?? item.routeEstimateProvider,
routeEstimateCached: saved?.route_estimate_cached ?? guidance?.cached ?? item.routeEstimateCached,
routeEstimateKey: saved?.route_estimate_key ?? guidance?.routeKey ?? item.routeEstimateKey,
routeEstimateCreatedAt: saved?.route_estimate_created_at ?? guidance?.estimatedAt ?? item.routeEstimateCreatedAt
}));
pushSystemChat(request.id, `Passenger updated the destination to ${nextDestination}.`);
saveState();
renderAll();
} catch (error) {
status.textContent = `Destination update failed: ${error.message}`;
}
}
function canCancelBeforeStart(request) {
if (!request || !preStartCancellationStatuses.includes(request.status)) return false;
if (activeRole() === "passenger") return requestBelongsToPassenger(request);
return activeRole() === "rider" && selectedRiderIdForRequest(request) === state.rider?.id;
}
function canSeeRideLifecycleActions(request) {
if (!request) return false;
if (activeRole() === "passenger") return requestBelongsToPassenger(request);
return activeRole() === "rider" && selectedRiderIdForRequest(request) === state.rider?.id;
}
function activeRideForRole(preferredRequest = selectedRequest()) {
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) => selectedRiderIdForRequest(request) === state.rider.id
&& ["matched", "arrived", "in_progress"].includes(request.status)) ?? null;
}
return null;
}
function rideLifecycleActionSummary(request) {
if (!request) return "";
if (request.status === "open") return "Passenger can cancel this open request before choosing a rider.";
if (request.status === "matched") return activeRole() === "rider"
? "You can cancel before start or mark arrival at the pickup point."
: `You can cancel before start while the rider is on the way. ${cancellationFeeText(request)}`;
if (request.status === "arrived") return activeRole() === "rider"
? "Waiting for passenger to start the ride; cancellation is still available before start."
: `Start the ride when you are with the rider, or cancel before start. ${cancellationFeeText(request)}`;
if (request.status === "in_progress") return activeRole() === "passenger"
? "Ride is in progress. Mark it complete only when the trip is finished."
: "Ride is in progress. The passenger must mark it complete before settlement is created.";
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 ride moves through matching, arrival, start, and completion.";
}
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 selectedRiderIdForRequest(request) === state.rider?.id;
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),
relayPhone: request.contactRelayPhone ?? "",
relayStatus: request.contactRelayStatus ?? "relay_not_configured"
};
}
function phoneCallHref(phone) {
const normalized = String(phone ?? "").replace(/[^\d+]/g, "");
return normalized ? `tel:${normalized}` : "";
}
function appendContactActions(container, request) {
const contact = activeRideContactForRequest(request);
if (!contact) return;
const panel = document.createElement("div");
panel.className = "contact-actions";
const title = document.createElement("strong");
title.textContent = `Text and call ${contact.name}`;
panel.append(title);
const detail = document.createElement("small");
detail.textContent = contact.relayPhone
? `Masked Waka relay active: ${contact.relayPhone}`
: "Real phone numbers stay hidden. Use in-app text chat until Waka relay calling is activated for this ride.";
panel.append(detail);
if (contact.relayPhone) {
const callLink = document.createElement("a");
callLink.className = "secondary-action";
callLink.href = phoneCallHref(contact.relayPhone);
callLink.textContent = "Call through Waka";
panel.append(callLink);
}
container.append(panel);
}
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;
}
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 (!riderDailyRegionsReady(rider)) {
translatedAlert("riderDailyRegionsRequired");
return null;
}
if (!riderCurrentFreshGps(rider)) {
translatedAlert("riderLiveGpsRequired");
return null;
}
if (!roleCanSeeRequest(request)) {
translatedAlert("selectNearbyRequest");
return null;
}
if (request.status !== "open") {
translatedAlert("requestClosed");
return null;
}
return { request, rider };
}
async function saveRiderOffer({ request, rider, fare, type }) {
const note = els.counterNote.value.trim();
if (note.length > riderOfferNoteMaxLength) {
els.offerRequestContext.textContent = `Keep rider notes to ${riderOfferNoteMaxLength} characters or less.`;
return;
}
const existing = state.offers.find((offer) => offer.requestId === request.id && offer.riderId === rider.id);
const offer = {
id: existing?.id ?? makeId("offer"),
requestId: request.id,
riderId: rider.id,
fare,
type,
note,
...offerPickupDistanceSnapshot(request, rider),
createdAt: new Date().toISOString()
};
try {
const savedOffer = await saveOfferToSupabase(offer);
state.offers = state.offers.filter((item) => item.id !== offer.id && item.id !== savedOffer.id);
state.offers.unshift(savedOffer);
els.offerForm.reset();
saveState();
renderAll();
void refreshMarketplace({ silent: true });
} catch (error) {
translatedAlert("offerSendFailed", { message: error.message });
}
}
async function acceptPassengerFare() {
const context = currentEligibleOfferContext();
if (!context) return;
await saveRiderOffer({
...context,
fare: context.request.fareOffer,
type: "accepted"
});
}
async function sendOffer(event) {
event.preventDefault();
const context = currentEligibleOfferContext();
if (!context) return;
const requestedFare = Number(String(els.counterFare.value).replace(/[^\d]/g, ""));
if (!requestedFare) {
els.offerRequestContext.textContent = "Enter your counter-offer fare, or use Accept passenger fare.";
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;
}
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;
}
const rider = state.riders.find((item) => item.id === offer.riderId);
let savedRequest = null;
try {
savedRequest = await chooseOfferInSupabase(requestToMatch, offer);
} catch (error) {
translatedAlert("chooseRiderFailed", { message: error.message });
return;
}
state.requests = state.requests.map((request) => {
if (request.id !== offer.requestId) return request;
const serverState = savedRequest?.id === request.id ? savedRequest : {};
return {
...request,
...serverState,
status: "matched",
selectedOfferId: offer.id,
agreedFare: offer.fare,
selectedRiderId: offer.riderId,
selectedRiderName: firstNameOnly(rider?.name, "Rider"),
riderConfirmationStatus: isScheduledRequest(request) ? "not_requested" : null,
riderConfirmationRequestedAt: null,
riderConfirmedAt: null,
releasedAt: null,
matchedAt: new Date().toISOString()
};
});
state.selectedRequestId = offer.requestId;
const systemMessage = {
id: makeId("chat"),
requestId: offer.requestId,
sender: "system",
text: isScheduledRequest(requestToMatch)
? `Scheduled ride matched at ${formatMoney(offer.fare)} for ${formatDateTime(requestToMatch.scheduledAt)}. Passenger can request confirmation before travel.`
: `Ride matched at ${formatMoney(offer.fare)}. Confirm pickup and ${paymentLabel(requestToMatch.paymentPreference).toLowerCase()} before the ride starts.`,
createdAt: new Date().toISOString()
};
state.chats.push(systemMessage);
void saveChatMessageToSupabase(systemMessage);
saveState();
renderAll();
void refreshMarketplace({ silent: true });
}
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 = "") {
if (!hasSupabaseRuntime()) return;
try {
const data = await callSupabaseRpcResult(
"change_ride_state",
{
request_id: requestId,
action_name: actionName,
reason
},
"Updating the ride status",
optionalSupabaseRequestTimeoutMs
);
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 missingFunction = /change_ride_state|schema cache|function/i.test(error.message);
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 rideLifecycleMessage(request, actionName) {
const cancellationFee = actionName === "cancel" && activeRole() === "passenger"
? passengerCancellationFeeEstimate(request)
: null;
return {
arrive: `${selectedRiderFirstNameForRequest(request)} marked arrival at the pickup point.`,
start: "Passenger confirmed the ride has started.",
complete: "Ride completed.",
cancel: activeRole() === "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 = "") {
if (actionName === "cancel") {
if (activeRole() === "rider") {
state.offers = state.offers.filter((offer) => !(offer.requestId === request.id && offer.riderId === state.rider?.id));
return {
...request,
status: "open",
selectedOfferId: null,
agreedFare: null,
selectedRiderId: null,
selectedRiderName: null,
riderConfirmationStatus: isScheduledRequest(request) ? "released" : null,
riderConfirmationRequestedAt: null,
riderConfirmedAt: null,
releasedAt: new Date().toISOString(),
cancelReason: reason
};
}
const cancellationFee = passengerCancellationFeeEstimate(request);
return {
...request,
status: "cancelled",
cancelledBy: currentActorIdForChat(),
cancelledAt: new Date().toISOString(),
cancelReason: reason,
cancellationFeeAmount: cancellationFee.amount,
cancellationFeeCurrency: cancellationFee.currency,
cancellationFeeStatus: cancellationFee.status,
cancellationFeeRiderId: selectedRiderIdForRequest(request),
cancellationFeeElapsedMinutes: cancellationFee.elapsedMinutes
};
}
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,
completedAt: actionName === "complete" ? nowIso : request.completedAt
};
}
function createLocalRideSettlement(request) {
if (!request || request.status === "completed" || rideSettlementRecords().some((settlement) => settlement.requestId === request.id)) return;
const breakdown = rideFinancialBreakdown(request);
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: centsToDollars(breakdown.fareCents),
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;
try {
const savedRequest = await changeRideStateInSupabase(requestId, actionName, reason);
updateRequestById(requestId, (item) => {
const localState = applyRideLifecycleState(item, actionName, reason);
return savedRequest?.id === requestId ? { ...localState, ...savedRequest } : localState;
});
} catch (error) {
alert(error.message);
return;
}
if (actionName === "complete") createLocalRideSettlement(request);
pushSystemChat(requestId, rideLifecycleMessage(request, actionName));
saveState();
renderAll();
void refreshMarketplace({ silent: true });
}
async function cancelRideBeforeStart(requestId) {
const request = state.requests.find((item) => item.id === requestId);
if (!canCancelBeforeStart(request)) return;
if (activeRole() === "passenger") {
const estimate = passengerCancellationFeeEstimate(request);
if (estimate.amount > 0) {
const ok = confirm(`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 = prompt("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();
}
function sendChat(event) {
event.preventDefault();
const request = selectedRequest();
const text = els.chatInput.value.trim();
if (!request || !canChatOnRequest(request) || !text) return;
const message = {
id: makeId("chat"),
requestId: request.id,
sender: state.activeTab,
text,
createdAt: new Date().toISOString()
};
state.chats.push(message);
void saveChatMessageToSupabase(message);
els.chatInput.value = "";
saveState();
renderChat();
}
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 });
}
}
async function submitRideRating(event) {
event.preventDefault();
const request = selectedRequest();
if (!canRateRequest(request)) {
els.rideRatingStatus.textContent = "Ratings open after selecting a completed ride that has not already been rated.";
return;
}
const target = ratingTargetForRequest(request);
const reviewerId = currentActorIdForChat();
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),
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) 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.
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 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);
panel.hidden = !signedIn;
list.innerHTML = "";
if (!signedIn) return;
const notices = currentAccountNotifications(type);
if (!notices.length) {
list.append(emptyState("No admin 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)}
`;
list.append(item);
});
}
function renderBusinessAccountPanel() {
if (!els.businessAccountForm) return;
const passengerSignedIn = Boolean(state.sessions.passenger && state.passenger);
els.businessAccountForm.hidden = !passengerSignedIn;
if (!passengerSignedIn) return;
const accounts = passengerBusinessAccounts();
els.businessAccountStatus.textContent = accounts.length
? businessAccountSummary(accounts[0])
: `Optional business rides cost ${formatMoney(businessMonthlySubscriptionFee)} per month plus ${Math.round(businessRideServiceFeeRate * 100)}% on completed business rides after activation.`;
els.businessAccountList.innerHTML = "";
if (!accounts.length) {
els.businessAccountList.append(emptyState("No business account is linked to this passenger yet."));
return;
}
accounts.forEach((account) => {
const subscription = businessSubscriptionFor(account.id);
const item = document.createElement("article");
item.className = "notice-item";
item.innerHTML = `
${escapeHtml(account.businessName)}
${escapeHtml(businessAccountSummary(account))}
${escapeHtml(account.billingEmail)} - ${escapeHtml(subscription?.provider ?? "stripe")} - ${escapeHtml(subscription?.reference || "provider confirmation pending")}
Open business subscription checkout
`;
const checkoutButton = item.querySelector(".business-subscription-checkout");
checkoutButton.disabled = businessAccountCanRequest(account);
checkoutButton.hidden = businessAccountCanRequest(account);
checkoutButton.addEventListener("click", () => startBusinessSubscriptionCheckout(account.id));
els.businessAccountList.append(item);
});
}
function updateBusinessBillingOptions() {
if (!els.rideBillingAccount) return;
const selected = 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}`
: `Business pending: ${account.businessName}`;
option.disabled = !businessAccountCanRequest(account);
option.selected = selected === account.id && !option.disabled;
els.rideBillingAccount.append(option);
});
}
function renderAccountWorkspaces() {
const passengerSignedIn = Boolean(state.sessions.passenger && state.passenger);
if (els.passengerSignInOtpPanel) els.passengerSignInOtpPanel.hidden = !phoneOtpSignInEnabled();
renderAccountModeButtons("passenger", passengerSignedIn);
els.passengerSignInForm.hidden = passengerSignedIn || accountMode("passenger") !== "signin";
els.passengerAccountForm.hidden = passengerSignedIn || accountMode("passenger") !== "create";
els.passengerSessionCard.hidden = !passengerSignedIn;
els.passengerPaymentForm.hidden = !passengerSignedIn;
els.passengerLocationForm.hidden = !passengerSignedIn;
els.rideRequestForm.hidden = !passengerSignedIn || !paymentAccountReady("passenger", state.passenger);
if (els.businessAccountForm) els.businessAccountForm.hidden = !passengerSignedIn;
if (passengerSignedIn) {
els.passengerSessionTitle.textContent = state.passenger.name || "Passenger signed in";
els.passengerSessionSummary.textContent = `${state.sessions.passenger.email ?? state.passenger.phone} - ${state.passenger.city}, ${state.passenger.country}. ${paymentAccountReady("passenger", state.passenger) ? "You can request rides." : "Add a payment account before requesting rides."}`;
els.passengerPaymentStatus.textContent = paymentAccountSummary("passenger", state.passenger);
}
renderBusinessAccountPanel();
updateBusinessBillingOptions();
renderAccountNotices("passenger");
const riderSignedIn = Boolean(state.sessions.rider && state.rider);
if (els.riderSignInOtpPanel) els.riderSignInOtpPanel.hidden = !phoneOtpSignInEnabled();
renderAccountModeButtons("rider", riderSignedIn);
const rider = currentRiderRecord();
const riderApproved = riderSignedIn && rider?.status === "approved";
const riderOperational = riderApproved && isSubscriptionActive(rider);
els.riderSignInForm.hidden = riderSignedIn || accountMode("rider") !== "signin";
els.riderAccountForm.hidden = riderSignedIn || accountMode("rider") !== "create";
els.riderSessionCard.hidden = !riderSignedIn;
els.riderPaymentForm.hidden = !riderSignedIn;
els.riderLocationForm.hidden = !riderSignedIn;
els.offerForm.hidden = !riderCanSeeRequests(rider);
els.subscriptionText.closest(".subscription-card").hidden = !riderApproved;
if (riderSignedIn) {
els.riderSessionTitle.textContent = state.rider.name || "Rider signed in";
els.riderSessionSummary.textContent = riderWorkspaceStatusMessage(rider);
els.riderPaymentStatus.textContent = paymentAccountSummary("rider", rider);
renderRiderDailyRegionStatus(rider);
}
renderAccountNotices("rider");
renderRiderTaxDocuments();
}
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}-`));
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 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) || window.location.hash === `#${tab}`) return;
if (routePathTab() === tab) return;
window.history.replaceState(null, "", `${window.location.pathname}${window.location.search}#${tab}`);
}
function applyRouteTab() {
const tab = requestedTabFromLocation();
if (tab && tab !== state.activeTab) switchTab(tab, { updateUrl: false });
renderEntryExperience();
}
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 shouldShowRoleEntry() {
return !requestedTabFromLocation();
}
function renderEntryExperience() {
const roleEntryVisible = shouldShowRoleEntry();
const publicTabsAvailable = runtimeAllowsWorkspaceTab("passenger") && runtimeAllowsWorkspaceTab("rider");
if (els.roleEntry) els.roleEntry.hidden = !roleEntryVisible;
if (els.workspace) els.workspace.hidden = roleEntryVisible;
if (els.roleTabs) els.roleTabs.hidden = roleEntryVisible || activeRole() === "admin" || !publicTabsAvailable;
}
function setAccountMode(type, mode) {
if (!["passenger", "rider"].includes(type)) return;
state.accountMode[type] = mode === "create" ? "create" : "signin";
saveState();
renderAll();
}
function accountMode(type) {
return state.accountMode?.[type] === "create" ? "create" : "signin";
}
function renderAccountModeButtons(type, signedIn) {
const stage = type === "passenger" ? els.passengerAccountStage : els.riderAccountStage;
if (stage) stage.hidden = signedIn;
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 } = options;
state.activeTab = 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 showRoleEntryScreen() {
state.showRoleEntry = true;
if (window.location.hash) window.history.replaceState({}, "", window.location.pathname || "./");
saveState();
renderAll();
}
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");
els.boardGrid.classList.add(`role-${role}`);
const passengerSignedIn = roleHasSignedInAccount("passenger");
const riderSignedIn = roleHasSignedInAccount("rider");
const adminSignedIn = roleHasSignedInAccount("admin");
if (els.seedDemo) els.seedDemo.hidden = !demoToolsAllowed();
if (els.clearDemo) els.clearDemo.hidden = !demoToolsAllowed();
els.marketPanel.hidden = (role === "passenger" && !passengerSignedIn)
|| (role === "rider" && (!riderSignedIn || !riderOperational))
|| (role === "admin" && !adminSignedIn);
els.workspace?.classList.toggle("account-only", els.marketPanel.hidden);
if (els.marketPanel.hidden) return;
document.querySelectorAll(".role-market-section").forEach((section) => {
const allowedRoles = (section.dataset.roles ?? "").split(/\s+/);
section.hidden = !allowedRoles.includes(role) || (role === "rider" && !riderOperational);
});
els.cityMap.hidden = role === "admin" || (role === "rider" && !riderOperational);
els.marketFilters.hidden = role === "admin" || role === "rider" || (role === "rider" && !riderOperational);
els.refreshMarket.hidden = !canRefreshMarketplace();
els.refreshMarket.disabled = marketRefreshInFlight;
els.refreshMarket.textContent = lastMarketRefreshAt
? `Refresh market (${lastMarketRefreshAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })})`
: "Refresh market";
if (role === "passenger") {
els.requestBoardTitle.textContent = "My ride requests";
els.offerBoardTitle.textContent = "Offers for my request";
return;
}
if (role === "rider") {
const vehicleName = "car";
els.requestBoardTitle.textContent = `Nearby ${vehicleName} requests`;
els.offerBoardTitle.textContent = `My ${vehicleName} offers`;
if (!riderOperational) {
els.marketLocation.textContent = rider?.city && rider?.country ? `${rider.city}, ${rider.country}` : "Rider workspace";
els.selectedSummary.textContent = riderWorkspaceStatusMessage(rider);
} 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 renderRequests() {
const visible = visibleRequestsForRole();
els.requestList.innerHTML = "";
if (!visible.length) {
if (activeRole() === "passenger") {
els.requestList.append(emptyState(state.passenger
? "No ride requests from this passenger yet."
: "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."
: !riderDailyRegionsReady(rider)
? "Save today's destination regions before receiving requests."
: !riderCurrentFreshGps(rider)
? "Live GPS is starting automatically before requests appear."
: activeRide
? "Complete or cancel your active immediate ride before taking another immediate request."
: `No car requests within about ${riderServiceRadius(rider)} km of ${rider.area}, ${rider.city} and today's destination regions yet.`;
els.requestList.append(emptyState(message));
}
}
visible.forEach((item) => {
const node = els.requestTemplate.content.firstElementChild.cloneNode(true);
const button = node.querySelector(".card-select");
node.classList.toggle("selected", item.id === state.selectedRequestId);
node.querySelector(".card-kicker").textContent = `${item.vehicle.toUpperCase()} ${isScheduledRequest(item) ? "scheduled" : "request"}`;
node.querySelector("strong").textContent = `${item.pickupArea} to ${requestDestinationText(item)}`;
node.querySelector("small").textContent = `${passengerFirstNameForRequest(item)} offered ${formatMoney(item.fareOffer)} - ${scheduleChip(item)}`;
node.querySelector("p").textContent = item.pickupDescription;
node.querySelector(".chip-row").innerHTML = [
paymentLabel(item.paymentPreference),
`${offersForRequest(item.id).length} offers`,
rideStatusLabel(item),
riderApproachChip(item),
proximityChip(item),
pickupGpsQualityChip(item),
confirmationChip(item),
item.destinationArea ? `Destination: ${item.destinationArea}` : "Text details",
item.businessAccountId ? "Business ride" : null,
`Car: ${carTypePreferenceLabel(item.carTypePreference)}`,
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
].filter(Boolean).map(chip).join("");
renderRideGuidance(node, item);
renderScheduledRideActions(node, item);
renderRideLifecycleActions(node, item);
renderPassengerFareBoost(node, item);
button.addEventListener("click", () => selectRequest(item.id));
els.requestList.append(node);
});
els.requestCount.textContent = `${visible.length}`;
}
function canBoostPassengerFare(request) {
return Boolean(activeRole() === "passenger"
&& request?.status === "open"
&& requestBelongsToPassenger(request)
&& request.id === state.selectedRequestId);
}
function renderPassengerFareBoost(node, request) {
if (!canBoostPassengerFare(request)) return;
const form = document.createElement("form");
form.className = "fare-boost-form";
form.innerHTML = `
Increase passenger fare
Update fare to attract riders
Open requests can be boosted before a rider is chosen.
`;
form.addEventListener("submit", (event) => updatePassengerFareOffer(event, request.id));
node.append(form);
}
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 addActionLink(container, label, className, href) {
if (!href) return;
const link = document.createElement("a");
link.className = className;
link.href = href;
link.target = "_blank";
link.rel = "noopener";
link.textContent = label;
container.append(link);
}
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 shouldShow = request.id === state.selectedRequestId || selectedRiderIdForRequest(request) === state.rider?.id;
if (!shouldShow) return;
const model = pickupProximityModel(request);
title.textContent = "Pickup guidance";
detail.textContent = model
? `${request.pickupArea}: ${formatDistanceKm(model.distanceKm)}, ${formatPickupEta(model.etaMinutes)} (${model.source}).`
: `${request.pickupArea}, ${request.city}.`;
addActionLink(actions, "Navigate to pickup", "secondary-action map-action", riderPickupNavigationUrl(request));
addActionLink(actions, "Waze pickup", "ghost-action map-action", riderPickupWazeUrl(request));
addActionLink(actions, "Pickup map", "ghost-action map-action", pickupMapUrl(request));
} else {
if (!requestBelongsToPassenger(request)) return;
const model = riderApproachModel(request);
const selectedRiderId = selectedRiderIdForRequest(request);
title.textContent = selectedRiderId ? "Rider approach" : "Pickup point";
detail.textContent = selectedRiderId
? model
? `${selectedRiderFirstNameForRequest(request)} is ${formatDistanceKm(model.distanceKm)} away, ${formatPickupEta(model.etaMinutes)}. ${model.isLive ? "Live GPS" : model.source}.`
: `${selectedRiderFirstNameForRequest(request)} selected; approach update pending.`
: `${request.pickupArea}, ${request.city}.`;
addActionLink(actions, "Pickup map", "secondary-action map-action", pickupMapUrl(request));
if (request.status === "in_progress") addActionLink(actions, "Destination map", "ghost-action map-action", destinationMapUrl(request));
}
copy.append(title, detail);
guidance.append(copy, 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 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 (activeRole() === "rider" && selectedRiderIdForRequest(request) === state.rider?.id && request.status === "matched") {
addActionButton(actions, "I have arrived", "secondary-action", () => changeRideLifecycle(request.id, "arrive"));
}
if (activeRole() === "passenger" && requestBelongsToPassenger(request) && request.status === "arrived") {
addActionButton(actions, "Start ride", "secondary-action", () => changeRideLifecycle(request.id, "start"));
}
const canComplete = request.status === "in_progress"
&& activeRole() === "passenger"
&& requestBelongsToPassenger(request);
if (canComplete) {
addActionButton(actions, "Complete ride", "secondary-action", () => changeRideLifecycle(request.id, "complete"));
}
copy.append(title, detail);
panel.append(copy);
if (actions.children.length) panel.append(actions);
node.append(panel);
}
function renderRideTipForm(node, request) {
if (!canTipRequest(request)) return;
const form = document.createElement("form");
form.className = "fare-boost-form";
form.innerHTML = `
Tip rider
Send tip
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";
const windowText = request.status === "in_progress"
? `Available for about the first ${destinationUpdateWindowMinutes(request)} minutes after pickup.`
: "Available until pickup; after pickup it remains open for 2/7 of estimated travel time.";
form.innerHTML = `
Update destination
Update destination
${windowText}
`;
form.addEventListener("submit", (event) => submitDestinationUpdate(event, request.id));
node.append(form);
}
function renderPersistentRideActions(request = selectedRequest()) {
if (!els.rideActionPanel) return null;
els.rideActionPanel.innerHTML = "";
const actionRequest = activeRideForRole(request);
if (actionRequest && canSeeRideLifecycleActions(actionRequest)) {
renderRideLifecycleActions(els.rideActionPanel, actionRequest);
renderDestinationUpdateForm(els.rideActionPanel, actionRequest);
renderRideTipForm(els.rideActionPanel, actionRequest);
return actionRequest;
}
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."
: "Cancel appears before the ride starts. Complete appears once the ride is in progress.";
copy.append(title, detail);
panel.append(copy);
els.rideActionPanel.append(panel);
return null;
}
function renderOffers() {
const request = selectedRequest();
const visibleOffers = visibleOffersForRole(request);
els.offerList.innerHTML = "";
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 (!visibleOffers.length) {
els.offerList.append(emptyState(activeRole() === "rider"
? "You have not sent an offer for this request yet."
: "No rider offers yet."));
}
const riderMap = stateLookupIndexes().riderMap;
visibleOffers.forEach((offer) => {
const rider = riderMap.get(offer.riderId);
const node = els.offerTemplate.content.firstElementChild.cloneNode(true);
node.querySelector(".card-kicker").textContent = offer.type === "accepted" ? "Accepted passenger fare" : "Counter-offer";
node.querySelector("strong").textContent = `${firstNameOnly(rider?.name, "Rider")} asks ${formatMoney(offer.fare)}`;
node.querySelector("small").textContent = `${rider?.vehicle ?? "vehicle"} in ${rider?.area ?? "nearby"} - rating ${rider?.rating ?? "new"}`;
node.querySelector("p").textContent = offer.note || "No extra note.";
node.querySelector(".chip-row").innerHTML = [
isSubscriptionActive(rider) ? "Subscription active" : "Not eligible",
offerFareDeltaChip(offer, request),
offerDistanceChip(offer, request),
"Phone hidden",
formatDate(offer.createdAt)
].filter(Boolean).map(chip).join("");
const choose = node.querySelector(".choose-offer");
choose.hidden = activeRole() !== "passenger";
choose.disabled = activeRole() !== "passenger" || request.status !== "open" || !requestBelongsToPassenger(request);
choose.addEventListener("click", () => chooseOffer(offer.id));
els.offerList.append(node);
});
els.offerCount.textContent = `${visibleOffers.length}`;
}
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 to see pickup, destination, fare, payment, and distance before offering.";
return;
}
const payment = request.paymentPreference === "mtn"
? "MTN Money"
: paymentLabel(request.paymentPreference);
const distance = proximityChip(request) ?? "Distance and pickup ETA not estimated";
const stops = normalizeRideStops(request.rideStops);
const stopsText = stops.length ? ` Stops: ${stops.join("; ")}.` : "";
els.offerRequestContext.textContent = `${request.pickupArea} to ${requestDestinationText(request)}. Passenger offered ${formatMoney(request.fareOffer)} for ${carTypePreferenceLabel(request.carTypePreference)}. Payment: ${payment}. ${distance}.${stopsText}`;
}
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 = selectedRequest();
if (!request || !roleCanSeeRequest(request)) {
renderOfferRequestContext(null);
els.selectedSummary.textContent = activeRole() === "rider"
? `Select a nearby ${currentRiderRecord()?.vehicle ?? "vehicle"} request to make a fare offer`
: "Publish or select one of your ride requests";
return;
}
const approach = riderApproachChip(request);
els.selectedSummary.textContent = `${request.pickupArea} to ${requestDestinationText(request)} - offer ${formatMoney(request.fareOffer)} - ${scheduleChip(request)}${approach ? ` - ${approach}` : ""}`;
if (els.counterFare) els.counterFare.placeholder = `Counter offer, passenger offered ${formatMoney(request.fareOffer)}`;
renderOfferRequestContext(request);
}
function renderSafetyReportForm(request) {
const canReport = canReportOnRequest(request);
const disabledReason = !request
? "Select a ride before filing a report."
: activeRole() === "rider"
? "Rider reports open after the passenger chooses your offer."
: "Passenger reports open after choosing a rider.";
const target = canReport ? reportTargetForRequest(request) : null;
els.safetyReportCategory.disabled = !canReport;
els.safetyReportSeverity.disabled = !canReport;
els.safetyReportDetails.disabled = !canReport;
els.safetyReportForm.querySelector("button").disabled = !canReport;
els.safetyReportStatus.textContent = canReport
? `${activeRole() === "rider" ? "Rider" : "Passenger"} report about ${target.name}. Admin reviews safety, no-show, route, payment, identity, and behavior concerns.`
: disabledReason;
}
function renderRideRatingForm(request) {
if (!els.rideRatingForm) return;
const target = ratingTargetForRequest(request);
const canRate = canRateRequest(request);
els.rideRatingScore.disabled = !canRate;
els.rideRatingComment.disabled = !canRate;
els.rideRatingForm.querySelector("button").disabled = !canRate;
if (!request) {
els.rideRatingStatus.textContent = "Ratings open after selecting a completed ride.";
} else if (existingRatingForRequest(request)) {
els.rideRatingStatus.textContent = "Rating already submitted for this ride.";
} else if (canRate) {
els.rideRatingStatus.textContent = `Rate ${target.name} for accountability after the completed ride.`;
} else {
els.rideRatingStatus.textContent = "Ratings open after the ride is marked complete.";
}
}
function renderChat() {
const request = selectedRequest();
const isOpen = canChatOnRequest(request);
els.chatStatus.textContent = isOpen ? "Open" : "Locked";
els.chatInput.disabled = !isOpen;
els.chatForm.querySelector("button").disabled = !isOpen;
els.chatInput.placeholder = isOpen ? "Write to the selected rider or passenger" : "Chat opens only after passenger chooses a rider";
els.chatThread.innerHTML = "";
const lifecycleRequest = renderPersistentRideActions(request);
renderSafetyReportForm(reportableRideForRole(request) ?? lifecycleRequest ?? request);
renderRideRatingForm(lifecycleRequest ?? request);
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;
}
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.`));
return;
}
messages.forEach((message) => {
const bubble = document.createElement("div");
bubble.className = `chat-bubble ${message.sender === state.activeTab ? "self" : ""}`;
bubble.textContent = message.text;
els.chatThread.append(bubble);
});
}
function offersForRequest(requestId) {
return stateLookupIndexes().offersByRequestId.get(requestId) ?? [];
}
function selectRequest(id) {
state.selectedRequestId = id;
saveState();
renderAll();
}
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()
&& activeRole() === "passenger"
&& Boolean(state.passenger)
&& hasSignedIn("passenger")
&& els.rideRequestForm
&& !els.rideRequestForm.hidden;
}
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 ? "Setting exact pickup pin..." : "Refreshing pickup pin...";
}
passengerPickupGpsPromise = getCurrentGpsPoint();
pendingPickupGps = await passengerPickupGpsPromise;
const qualityIssue = pickupGpsQualityIssue(pendingPickupGps);
if (els.pickupGpsStatus) {
els.pickupGpsStatus.textContent = qualityIssue
? qualityIssue
: `${gpsStatusLabel(pendingPickupGps)}. Pickup pin is ready.`;
}
updateFareGuidance();
return pendingPickupGps;
} catch (error) {
pendingPickupGps = null;
if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = error.message;
updateFareGuidance();
return null;
} finally {
passengerPickupGpsPromise = null;
}
}
async function ensurePassengerPickupGpsForPublish() {
if (!pendingPickupGps || pickupGpsQualityIssue(pendingPickupGps)) {
await capturePassengerPickupGps({ automatic: true });
}
}
function clearPassengerPickupGps() {
pendingPickupGps = null;
if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = "Pickup pin cleared.";
updateFareGuidance();
}
function ensurePassengerPickupGpsAutoCapture() {
if (!passengerPickupAutoReady()) return;
if (pendingPickupGps && !pickupGpsQualityIssue(pendingPickupGps)) return;
void capturePassengerPickupGps({ automatic: true });
}
function destinationAutocompleteReady() {
return placesAutocompleteEnabled()
&& hasSupabaseRuntime()
&& Boolean(state.passenger)
&& hasSignedIn("passenger");
}
function destinationSessionToken() {
if (!destinationAutocompleteSessionToken) {
destinationAutocompleteSessionToken = makeId("place-session");
}
return destinationAutocompleteSessionToken;
}
function clearDestinationPlaceSelection(message = "") {
selectedDestinationPlace = null;
if (els.destinationPlaceStatus && message) els.destinationPlaceStatus.textContent = message;
}
function hideDestinationSuggestions() {
if (!els.destinationSuggestions) return;
els.destinationSuggestions.hidden = true;
els.destinationSuggestions.replaceChildren();
}
async function fetchPlaceAutocomplete(body) {
if (!destinationAutocompleteReady()) throw new Error("Destination autocomplete needs passenger sign-in and Supabase.");
const functionName = placesAutocompleteFunctionName();
if (supabaseClient?.functions?.invoke) {
const { data, error } = await withSupabaseTimeout(
supabaseClient.functions.invoke(functionName, { body }),
"Fetching destination suggestions",
optionalSupabaseRequestTimeoutMs
);
if (error) throw error;
return data;
}
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) throw new Error(payload?.error || "Destination autocomplete failed.");
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;
}
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);
button.addEventListener("click", () => selectDestinationSuggestion(suggestion));
els.destinationSuggestions.append(button);
}
els.destinationSuggestions.hidden = false;
}
function handleDestinationInput() {
if (!destinationPlaceMatchesInput(selectedDestinationPlace, els.destination.value)) {
clearDestinationPlaceSelection(destinationAutocompleteReady()
? "Choose a suggested place for the most accurate route."
: "Destination text will be routed as typed.");
}
updateFareGuidance();
scheduleDestinationAutocomplete();
}
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 places.";
return;
}
const requestId = ++destinationAutocompleteRequestId;
destinationAutocompleteTimer = window.setTimeout(async () => {
try {
if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = "Searching destination places...";
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 place suggestion found; refine the destination text.";
}
} catch (error) {
hideDestinationSuggestions();
if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = error.message;
}
}, 350);
}
async function selectDestinationSuggestion(suggestion) {
try {
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;
els.destination.value = place.formattedAddress || place.displayName || suggestion.text;
hideDestinationSuggestions();
destinationAutocompleteSessionToken = null;
if (els.destinationPlaceStatus) {
els.destinationPlaceStatus.textContent = `Selected: ${place.displayName || place.formattedAddress}`;
}
updateFareGuidance();
} catch (error) {
if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = error.message;
}
}
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();
if (businessName.length < 2 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(billingEmail)) {
els.businessAccountStatus.textContent = "Enter a business name and valid billing email.";
return;
}
const localAccount = {
id: makeId("business"),
ownerId: state.passenger.id,
ownerName: state.passenger.name,
businessName,
billingEmail,
status: hasSupabaseRuntime() ? "pending" : "active",
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);
if (!hasSupabaseRuntime()) {
state.businessSubscriptions = upsertById(state.businessSubscriptions, {
id: makeId("bizsub"),
businessAccountId: savedAccount.id,
amount: businessMonthlySubscriptionFee,
provider: "stripe",
reference: "local-demo-active",
paidUntil: daysFromNow(30),
status: "active",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
});
}
els.businessName.value = "";
els.businessBillingEmail.value = "";
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 {
els.passengerLocationStatus.textContent = "Updating passenger city...";
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}, ${country}.`;
} catch (error) {
els.passengerLocationStatus.textContent = error.message;
}
}
async function createPassenger(event) {
event.preventDefault();
setTranslatedStatus(els.passengerStatus, "checkingPassengerAccount");
const phone = els.passengerPhone.value.trim();
const dateOfBirth = normalizeDateOfBirthInput(els.passengerDob);
const profilePhotoName = els.passengerPhoto.files[0]?.name ?? state.passenger?.profilePhotoName ?? "";
if (!validateAccountForm(els.passengerAccountForm, els.passengerStatus)) return;
if (!validDateOfBirth(dateOfBirth)) {
setTranslatedStatus(els.passengerStatus, "validDateOfBirthRequired");
return;
}
if (!(await ensureVerifiedPhoneForAccount("passenger", phone, els.passengerStatus))) return;
const passenger = {
id: state.passenger?.id ?? makeId("passenger"),
name: els.passengerName.value.trim(),
email: els.passengerEmail.value.trim().toLowerCase(),
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",
nationalId: 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");
const user = await saveProfileToSupabase({ ...passenger, role: "passenger" }, setPassengerStage, { waitForProfile: true });
state.passenger = {
...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()
};
els.passengerPassword.value = "";
els.passengerPhoto.value = "";
state.passengers = upsertById(state.passengers, state.passenger);
state.accountMode.passenger = "signin";
saveState();
renderAll();
const passengerCreatedKey = user?.emailSetupPending ? "passengerCreatedEmailPending" : "passengerCreated";
setTranslatedStatus(els.passengerStatus, passengerCreatedKey, { name: state.passenger.name });
setTranslatedStatus(els.passengerSessionSummary, passengerCreatedKey, { name: state.passenger.name });
} catch (error) {
setTranslatedStatus(els.passengerStatus, "passengerAccountFailed", { message: error.message });
} finally {
setButtonBusy(els.passengerSaveButton, false);
}
}
async function createRideRequest(event) {
event.preventDefault();
if (!state.passenger) {
translatedAlert("passengerAccountRequired");
return;
}
if (!hasSignedIn("passenger")) {
translatedAlert("passengerSignInRequired");
return;
}
if (!state.passenger.phoneVerified) {
translatedAlert("passengerPhoneRequired");
return;
}
if (!paymentAccountReady("passenger", state.passenger)) {
translatedAlert("passengerPaymentRequired");
return;
}
const fareOffer = Number(String(els.fareOffer.value).replace(/[^\d]/g, ""));
if (!fareOffer || fareOffer < minimumFareOffer(state.passenger.country)) {
translatedAlert("realisticFareRequired");
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 ensurePassengerPickupGpsForPublish();
if (!pendingPickupGps) {
els.pickupGpsStatus.textContent = "Exact pickup GPS is required before publishing in Maryland.";
return;
}
const pickupGpsIssue = pickupGpsQualityIssue(pendingPickupGps);
if (pickupGpsIssue) {
els.pickupGpsStatus.textContent = pickupGpsIssue;
return;
}
let guidance = updateFareGuidance();
if (routeEstimatesEnabled()) {
if (els.fareGuidance) els.fareGuidance.textContent = "Checking accurate driving distance before publishing...";
try {
guidance = await accurateFareGuidanceForRide(
state.passenger.country,
state.passenger.city,
els.pickupArea.value,
els.destinationArea.value,
els.destination.value.trim(),
pendingPickupGps,
els.rideStops.value
);
if (els.fareGuidance) els.fareGuidance.textContent = fareGuidanceMessage(guidance);
} catch (error) {
if (els.fareGuidance) els.fareGuidance.textContent = fareGuidanceMessage(guidance);
translatedAlert("publishRideFailed", { message: error.message });
return;
}
}
if (guidance && fareOffer < guidance.min) {
const continueBelowRange = translatedConfirm("fareBelowGuidance", { min: `$${guidance.min}`, max: `$${guidance.max}` });
if (!continueBelowRange) return;
}
const paymentPreference = validPaymentPreferenceForCountry(els.paymentPreference.value, state.passenger.country);
els.paymentPreference.value = paymentPreference;
const rideStops = normalizeRideStops(els.rideStops.value);
const destinationPlace = destinationPlaceForRoute(els.destination.value.trim());
const businessAccountId = els.rideBillingAccount?.value || null;
const businessAccount = businessAccountId ? passengerBusinessAccounts().find((account) => account.id === businessAccountId) : null;
if (businessAccountId && !businessAccountCanRequest(businessAccount)) {
translatedAlert("publishRideFailed", { message: "Business subscription must be active 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: els.pickupArea.value,
pickupDescription: els.pickupDescription.value.trim(),
destinationArea: els.destinationArea.value,
destination: els.destination.value.trim(),
destinationPlaceId: destinationPlace?.placeId ?? null,
destinationFormattedAddress: destinationPlace?.formattedAddress ?? null,
destinationLatitude: destinationPlace?.latitude ?? null,
destinationLongitude: destinationPlace?.longitude ?? null,
vehicle: "car",
carTypePreference: normalizeCarTypePreference(els.vehiclePreference.value),
rideStops,
fareOffer,
estimatedDistanceMiles: guidance?.distanceMiles ?? null,
estimatedTravelMinutes: guidance?.minutes ?? null,
routeEstimateSource: guidance?.source ?? "zone",
routeEstimateProvider: guidance?.provider ?? "zone",
routeEstimateCached: Boolean(guidance?.cached),
routeEstimateKey: guidance?.routeKey ?? null,
routeEstimateCreatedAt: guidance?.estimatedAt ?? null,
paymentPreference,
pickupGps: pendingPickupGps,
pickupLatitude: pendingPickupGps?.latitude ?? null,
pickupLongitude: pendingPickupGps?.longitude ?? null,
pickupGpsAccuracyMeters: pendingPickupGps?.accuracyMeters ?? null,
pickupGpsCapturedAt: pendingPickupGps?.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);
state.requests.unshift(savedRequest);
state.selectedRequestId = savedRequest.id;
pendingPickupGps = null;
selectedDestinationPlace = null;
hideDestinationSuggestions();
els.pickupGpsStatus.textContent = "Auto pickup GPS starts when this form opens";
if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = "Start typing a destination place, landmark, or address.";
els.rideRequestForm.reset();
populateLocationFields();
saveState();
renderAll();
void refreshMarketplace({ silent: true });
setTranslatedStatus(els.passengerSessionSummary, hasSupabaseRuntime() ? "ridePublishedSupabase" : "ridePublishedLocal");
} catch (error) {
translatedAlert("publishRideFailed", { message: error.message });
}
}
async function signOutRole(type) {
if (type === "rider") {
stopAutomaticRiderGps();
const rider = currentRiderRecord();
if (riderCurrentGps(rider)) {
try {
await clearRiderLiveGpsInSupabase(clearRiderLiveGpsFields(rider));
} catch (error) {
console.warn("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;
selectedDestinationPlace = null;
hideDestinationSuggestions();
els.passengerSignInPassword.value = "";
setTranslatedStatus(els.passengerSignInStatus, "signedOut");
}
if (type === "rider") {
riderAutoGpsPaused = false;
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, live GPS, and marketplace UI.
const riderDocumentLabels = {
driverLicense: "Driver's license",
vehicleRegistration: "Vehicle registration",
insurance: "Insurance document"
};
function emptyRiderDocuments() {
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?.driverLicenseDocumentPath) documents.driverLicense = rider.driverLicenseDocumentPath;
if (rider?.vehicleRegistrationDocumentPath) documents.vehicleRegistration = rider.vehicleRegistrationDocumentPath;
if (rider?.insuranceDocumentPath) documents.insurance = rider.insuranceDocumentPath;
return documents;
}
function selectedRiderDocumentFiles() {
return {
driverLicense: els.riderLicenseDocument.files[0] ?? null,
vehicleRegistration: els.riderRegistrationDocument.files[0] ?? null,
insurance: els.riderInsuranceDocument.files[0] ?? null
};
}
function riderDocumentPayload(documents) {
return JSON.stringify({ ...emptyRiderDocuments(), ...documents });
}
function missingRiderDocumentLabels(documents) {
return Object.entries({ ...emptyRiderDocuments(), ...documents })
.filter(([, value]) => !value)
.map(([key]) => riderDocumentLabels[key]);
}
function riderDocumentSummary(rider) {
const documents = riderDocuments(rider);
return Object.entries(riderDocumentLabels)
.map(([key, label]) => `${label}: ${documents[key] || "missing"}`)
.join(". ");
}
function riderWorkspaceStatusMessage(rider = currentRiderRecord()) {
if (!rider) return "Sign in or submit an application to access the rider platform.";
if (rider.status === "pending") {
return "Your rider application is pending approval by admin. Rider tools, ride requests, offers, and chat unlock only after approval.";
}
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.";
}
const end = riderAccessEnd(rider);
const remaining = daysUntil(end);
const label = riderAccessLabel(rider);
if (isSubscriptionActive(rider)) {
const setupGaps = [
paymentAccountReady("rider", rider) ? null : "payment account",
riderDailyRegionsReady(rider) ? null : "today's destination regions",
riderCurrentFreshGps(rider) ? null : "live GPS"
].filter(Boolean);
if (label === "subscription" && remaining > subscriptionRenewalNoticeDays) {
return setupGaps.length
? `Approved. Subscription active until ${formatDate(end)}. Complete ${setupGaps.join(", ")} before requests appear.`
: `Approved. Subscription active until ${formatDate(end)}. Renewal reminder appears 3 days before expiry.`;
}
const reminder = remaining <= subscriptionRenewalNoticeDays ? " Renewal is due soon; the provider will renew automatically if the payment method is active." : "";
const setup = setupGaps.length ? ` Complete ${setupGaps.join(", ")} before requests appear.` : "";
return `Approved. Your ${label} has ${pluralDays(remaining)} left, until ${formatDate(end)}.${reminder}${setup}`;
}
return `Approved, but your ${label} expired on ${formatDate(end)}. Pay the monthly subscription to receive and respond 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) {
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 review", "Admin review starts after submission."],
["locked", "Trial or subscription", "Access starts only after approval."],
["locked", "Ride requests", "Vehicle-matching requests unlock after approval and active access."]
]
};
}
if (rider.status === "pending") {
return {
title: "Application pending",
summary: "Admin review is required before ride requests, offers, and chat unlock.",
meta: baseMeta,
steps: [
["complete", "Application submitted", "Profile and rider documents are saved for review."],
["current", "Admin review", "Admin must approve the rider application."],
["locked", "30-day trial", "The free trial starts only after approval."],
["locked", "Ride requests", "Marketplace access is blocked while pending."]
]
};
}
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."],
["locked", "Admin decision", "The current decision is declined."],
["locked", "Trial or subscription", "No rider access is active."],
["locked", "Ride requests", "Marketplace access remains blocked."]
]
};
}
if (rider.status === "approved" && isSubscriptionActive(rider)) {
const end = riderAccessEnd(rider);
const remaining = daysUntil(end);
const label = riderAccessLabel(rider);
const paidSubscriptionHealthy = label === "subscription" && remaining > subscriptionRenewalNoticeDays;
const paymentReady = paymentAccountReady("rider", rider);
const regionsReady = riderDailyRegionsReady(rider);
const gpsReady = Boolean(riderCurrentFreshGps(rider));
const readyForRequests = paymentReady && regionsReady && gpsReady;
return {
title: readyForRequests ? `${vehicleName} rider platform active` : "Rider setup required",
summary: readyForRequests
? (paidSubscriptionHealthy
? `Subscription active until ${formatDate(end)}.`
: `${label === "free trial" ? "Free trial" : "Subscription"} has ${pluralDays(remaining)} left.`)
: "Payment account, today's destination regions, and live GPS are required before requests appear.",
meta: [
...baseMeta,
`Access until ${formatDate(end)}`,
remaining <= subscriptionRenewalNoticeDays ? "Renewal reminder active" : "Renewal reminder off",
paymentReady ? "Payment linked" : "Payment needed",
regionsReady ? "Daily regions set" : "Daily regions needed",
gpsReady ? "Live GPS active" : "Live GPS needed"
],
steps: [
["complete", "Application approved", "Admin approval is complete."],
["current", label === "free trial" ? "30-day free trial" : "Rider plan", paidSubscriptionHealthy ? "Renewal reminder appears 3 days before expiry." : `${pluralDays(remaining)} left before renewal is required.`],
[paymentReady ? "complete" : "current", "Payment account", paymentReady ? "Bank or processor reference is saved." : "Save a payment account before receiving requests."],
[regionsReady ? "complete" : "locked", "Today's destination regions", regionsReady ? riderDailyDestinationRegions(rider).join(", ") : "Choose where you are willing to take rides today."],
[gpsReady ? "complete" : "locked", "Live GPS", gpsReady ? "Fresh live GPS is active." : "Share fresh live GPS before requests appear."],
[readyForRequests ? "complete" : "locked", "Ride requests", readyForRequests ? `${vehicleName} rider sees matching passenger requests.` : "Marketplace access waits for setup."]
]
};
}
return {
title: "Subscription required",
summary: "Rider access is paused until the provider confirms monthly Waka Rider Access payment.",
meta: [...baseMeta, riderPlanSummary()],
steps: [
["complete", "Application approved", "Admin approval is complete."],
["locked", "Trial expired", "Free trial or subscription period has ended."],
["current", "Rider plan payment", "Open automatic Waka Rider Access checkout before rider tools unlock again."],
["locked", "Ride requests", "Marketplace access is blocked until subscription is active."]
]
};
}
function renderRiderFlow() {
const riderSignedIn = Boolean(state.sessions.rider && state.rider);
els.riderFlowCard.hidden = !riderSignedIn;
if (!riderSignedIn) return;
const model = riderFlowModel(currentRiderRecord());
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("");
}
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));
}
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);
populateSelect(els.riderCarBodyType, carBodyTypes, carBodyTypeLabel(rider?.carBodyType));
populateSelect(els.riderCarYear, vehicleYearOptions(), String(rider?.carYear ?? new Date().getFullYear()));
populateSelect(els.riderCarColor, carColors, rider?.carColor ?? carColors[0]);
}
function renderRiderTaxDocuments() {
if (!els.riderTaxPanel || !els.riderTaxList) return;
const signedIn = Boolean(state.sessions.rider && state.rider);
els.riderTaxPanel.hidden = !signedIn;
els.riderTaxList.innerHTML = "";
if (!signedIn) return;
const rider = currentRiderRecord() ?? state.rider;
const taxIdentity = taxIdentityForRider(rider?.id);
const provider = appConfig.taxOnboardingProvider || "tax provider";
const riderApproved = rider?.status === "approved";
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 = storageReviewButton(`${taxDocument.documentType} ${taxDocument.taxYear}`, appConfig.buckets.riderDocuments, taxDocument.storagePath);
item.innerHTML = `
${escapeHtml(taxDocument.documentType)} - ${escapeHtml(String(taxDocument.taxYear))}
Status: ${escapeHtml(taxDocument.status)}. Provider: ${escapeHtml(taxDocument.provider || "Waka")}.
${taxDocument.issuedAt ? `Issued ${formatDate(taxDocument.issuedAt)}` : "Not issued yet"}
${openButton}
`;
els.riderTaxList.append(item);
});
wireStorageReviewButtons(els.riderTaxList);
}
function riderServiceRadius(rider = currentRiderRecord()) {
return riderProximityLimit[rider?.vehicle] ?? riderProximityLimit.car;
}
function riderServiceAreaSummary(rider = currentRiderRecord()) {
if (!rider) return "Nearby requests use your current operating area.";
const regions = riderDailyDestinationRegions(rider);
const destinations = regions.length ? ` Today's destinations: ${regions.join(", ")}.` : " Choose today's destination regions.";
return `Car requests within about ${riderServiceRadius(rider)} km of ${rider.area}, ${rider.city}.${destinations} ${riderLiveGpsStatusSummary(rider)}`;
}
function renderRiderDailyRegionStatus(rider = currentRiderRecord()) {
if (!els.riderDailyRegionStatus) return;
if (!rider) {
els.riderDailyRegionStatus.textContent = "Choose destination regions before receiving requests today.";
return;
}
const regions = riderDailyDestinationRegions(rider);
const remaining = riderDailyRegionUpdatesRemaining(rider);
els.riderDailyRegionStatus.textContent = regions.length
? `Today: ${regions.join(", ")}. ${remaining} update${remaining === 1 ? "" : "s"} remaining today.`
: "Choose destination regions before receiving requests today.";
}
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() {
if (!state.rider) {
els.riderStatus.textContent = "No rider application saved yet.";
els.subscriptionText.textContent = "Approved riders receive 30 free days, then pay $150/month upfront for Waka Rider Access.";
els.subscriptionPaymentStatus.textContent = "Create and approve a rider account before opening automatic subscription checkout.";
els.paySubscription.disabled = true;
return;
}
const rider = state.riders.find((item) => item.id === state.rider.id) ?? state.rider;
const statusText = {
pending: "waiting for admin review",
approved: "approved",
declined: "declined by admin"
}[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 = "Rider plan checkout opens only after admin approval.";
els.paySubscription.disabled = true;
return;
}
const end = riderAccessEnd(rider);
const remaining = daysUntil(end);
const label = riderAccessLabel(rider);
if (isSubscriptionActive(rider)) {
const paidSubscriptionHealthy = label === "subscription" && remaining > subscriptionRenewalNoticeDays;
if (paidSubscriptionHealthy) {
els.subscriptionText.textContent = `Rider plan active until ${formatDate(end)}. ${riderPlanSummary()} Renewal reminder appears 3 days before expiry.`;
} else {
const reminder = remaining <= subscriptionRenewalNoticeDays
? ` Renewal is due soon; Waka will extend access automatically after the provider confirms payment.`
: ` Automatic subscription checkout opens when ${subscriptionRenewalNoticeDays} days or fewer remain.`;
els.subscriptionText.textContent = `${label === "free trial" ? "Free trial" : "Rider plan renewal"}: ${pluralDays(remaining)} left, until ${formatDate(end)}. ${riderPlanSummary()}${reminder}`;
}
els.subscriptionPaymentStatus.textContent = paidSubscriptionHealthy
? "No renewal notification needed. Automatic provider renewal will extend access before expiry."
: "Renewal reminder: keep your provider payment method active or open checkout to update billing.";
els.paySubscription.disabled = paidSubscriptionHealthy;
} else {
els.subscriptionText.textContent = `${label === "free trial" ? "Free trial" : "Rider plan"} expired on ${formatDate(end)}. ${riderPlanSummary()} Open checkout to continue receiving and responding to ride requests.`;
els.subscriptionPaymentStatus.textContent = "Open automatic subscription checkout. Access extends after the provider confirms payment.";
els.paySubscription.disabled = false;
}
}
async function startRiderTaxOnboarding() {
const status = els.riderTaxOnboardingStatus;
const rider = currentRiderRecord() ?? state.rider;
if (!rider || !state.sessions.rider) {
if (status) status.textContent = "Sign in as a rider before starting tax setup.";
return;
}
if (rider.status !== "approved") {
if (status) status.textContent = "Tax setup opens after admin approval.";
return;
}
if (!hasSupabaseRuntime()) {
if (status) status.textContent = "Hosted tax setup requires the Supabase production runtime.";
return;
}
try {
if (status) {
status.dataset.busy = "true";
status.textContent = `Opening ${appConfig.taxOnboardingProvider || "provider"} hosted tax setup...`;
}
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
);
saveState();
}
if (!responsePayload?.url) throw new Error("The tax provider did not return a hosted onboarding URL.");
if (status) status.textContent = "Redirecting to the provider-hosted tax setup...";
window.location.assign(responsePayload.url);
} catch (error) {
if (status) status.textContent = `Could not start hosted tax setup: ${error.message}`;
} finally {
if (status) delete status.dataset.busy;
}
}
function automaticRiderGpsReady() {
return autoRiderGpsEnabled()
&& !riderAutoGpsPaused
&& activeRole() === "rider"
&& riderBaseReadyForRequests(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()
? `${gpsStatusLabel(currentGps)} and shared automatically for matching.`
: `${gpsStatusLabel(currentGps)} for this local workspace.`;
}
return savedRider;
})();
try {
return await riderAutoGpsSyncPromise;
} finally {
riderAutoGpsSyncPromise = null;
}
}
function stopAutomaticRiderGps() {
if (riderGpsWatchId != null && navigator.geolocation?.clearWatch) {
navigator.geolocation.clearWatch(riderGpsWatchId);
}
riderGpsWatchId = null;
}
function ensureAutomaticRiderGps() {
if (!autoRiderGpsEnabled()) return;
if (!navigator.geolocation) {
if (activeRole() === "rider" && els.riderGpsStatus) els.riderGpsStatus.textContent = "GPS is not available in this browser.";
return;
}
if (!automaticRiderGpsReady()) {
stopAutomaticRiderGps();
return;
}
if (riderGpsWatchId != null) return;
if (els.riderGpsStatus) els.riderGpsStatus.textContent = "Starting automatic live GPS...";
riderGpsWatchId = navigator.geolocation.watchPosition(
(position) => {
const currentGps = gpsPointFromPosition(position);
if (!currentGps) return;
void saveRiderLiveGpsPoint(currentGps, { automatic: true });
},
() => {
if (els.riderGpsStatus) els.riderGpsStatus.textContent = "GPS permission was denied or live location could not be refreshed.";
},
{
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 5000
}
);
}
async function captureRiderLiveGps() {
riderAutoGpsPaused = false;
const rider = currentRiderRecord();
if (!rider || !hasSignedIn("rider")) {
els.riderGpsStatus.textContent = "Sign in as a rider before sharing GPS.";
return;
}
if (!riderBaseReadyForRequests(rider)) {
els.riderGpsStatus.textContent = riderWorkspaceStatusMessage(rider);
return;
}
try {
els.riderGpsStatus.textContent = "Refreshing live GPS...";
const currentGps = await getCurrentGpsPoint();
await saveRiderLiveGpsPoint(currentGps, { automatic: false, force: true });
ensureAutomaticRiderGps();
} catch (error) {
els.riderGpsStatus.textContent = error.message;
}
}
async function clearRiderLiveGps() {
riderAutoGpsPaused = true;
stopAutomaticRiderGps();
const rider = currentRiderRecord();
if (!rider || !hasSignedIn("rider")) {
els.riderGpsStatus.textContent = "Sign in as a rider before changing GPS sharing.";
return;
}
try {
els.riderGpsStatus.textContent = "Stopping live GPS sharing...";
const clearedRider = clearRiderLiveGpsFields(rider);
await clearRiderLiveGpsInSupabase(clearedRider);
saveCurrentRiderRecord(clearedRider);
renderAll();
void refreshMarketplace({ silent: true });
els.riderGpsStatus.textContent = "Live GPS paused. Refresh live GPS to resume automatic matching.";
} catch (error) {
els.riderGpsStatus.textContent = error.message;
}
}
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;
const regions = selectedMultiValues(els.riderDailyRegions);
if (!regions.length) {
els.riderDailyRegionStatus.textContent = "Choose at least one destination region for today.";
return;
}
const existingPreference = riderDayPreferenceFor(state.rider);
const updatesUsed = existingPreference?.updatesUsed ?? 0;
if (updatesUsed >= 2) {
els.riderDailyRegionStatus.textContent = "Today's destination regions were already set and updated once. Try again tomorrow.";
return;
}
try {
els.riderLocationStatus.textContent = "Saving today's rider regions...";
const riderId = state.rider.supabaseUserId ?? state.rider.id;
await updateRiderCurrentAreaInSupabase(riderId, country, city, area);
const preference = {
id: existingPreference?.id ?? makeId("day"),
riderId: state.rider.id,
riderName: state.rider.name,
serviceDate: localDateKey(),
country,
city,
originArea: area,
regions,
updatesUsed: updatesUsed + 1,
createdAt: existingPreference?.createdAt ?? new Date().toISOString(),
updatedAt: new Date().toISOString()
};
const savedPreference = await saveRiderDayPreferenceToSupabase(preference);
state.rider = clearRiderLiveGpsFields({
...state.rider,
country,
city,
area,
dailyRegions: savedPreference
});
state.riders = upsertById(state.riders, state.rider);
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 });
els.riderGpsStatus.textContent = "GPS not shared";
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 country = els.riderCountry.value;
const documentFiles = selectedRiderDocumentFiles();
const documentNames = Object.fromEntries(Object.entries(documentFiles).map(([key, file]) => [key, file?.name ?? ""]));
const profilePhotoName = els.riderPhoto.files[0]?.name ?? state.rider?.profilePhotoName ?? "";
const phone = els.riderPhone.value.trim();
const dateOfBirth = normalizeDateOfBirthInput(els.riderDob);
const missingDocuments = missingRiderDocumentLabels(documentNames);
if (!validateAccountForm(els.riderAccountForm, els.riderStatus)) return;
if (!validDateOfBirth(dateOfBirth)) {
setTranslatedStatus(els.riderStatus, "validDateOfBirthRequired");
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(),
registration: els.riderRegistration.value.trim(),
carMake: els.riderCarMake.value,
carModel: els.riderCarModel.value,
carBodyType: normalizeCarBodyType(els.riderCarBodyType.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(),
backgroundCheckConsentAt: els.riderBackgroundConsent?.checked ? new Date().toISOString() : null,
backgroundCheckProvider: appConfig.backgroundCheckProvider || "checkr",
backgroundCheckConsentVersion: "maryland-2026-05",
profilePhotoName,
profilePhotoPath: state.rider?.profilePhotoPath ?? null,
documentName: riderDocumentPayload(documentNames),
documents: documentNames,
driverLicenseDocumentName: documentNames.driverLicense,
vehicleRegistrationDocumentName: documentNames.vehicleRegistration,
insuranceDocumentName: documentNames.insurance,
backgroundCheckStatus: "not requested",
backgroundCheckDecision: "pending",
status: "pending",
approvedAt: null,
trialEndsAt: null,
subscriptionPaidUntil: null,
rating: "new",
createdAt: new Date().toISOString()
};
try {
setButtonBusy(els.riderSubmitButton, true);
const setRiderStage = (message) => {
els.riderStatus.textContent = message;
};
setTranslatedStatus(els.riderStatus, isSupabaseMode() ? "startingRiderSupabase" : "savingRiderApplication");
const user = await saveProfileToSupabase({ ...rider, role: "rider" }, setRiderStage, { waitForProfile: true });
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: riderDocumentPayload(savedDocuments),
documents: savedDocuments,
driverLicenseDocumentPath: savedDocuments.driverLicense,
vehicleRegistrationDocumentPath: savedDocuments.vehicleRegistration,
insuranceDocumentPath: savedDocuments.insurance,
supabaseUserId: user?.id ?? null
};
state.sessions.rider = {
phone: state.rider.phone,
email: state.rider.email,
userId: state.rider.supabaseUserId,
signedInAt: new Date().toISOString()
};
els.riderPassword.value = "";
els.riderPhoto.value = "";
els.riderLicenseDocument.value = "";
els.riderRegistrationDocument.value = "";
els.riderInsuranceDocument.value = "";
state.riders = state.riders.filter((item) => item.id !== rider.id && item.id !== user?.id);
state.riders.unshift(state.rider);
state.accountMode.rider = "signin";
saveState();
renderAll();
setTranslatedStatus(els.riderStatus, "riderCreatedPending", { name: state.rider.name });
setTranslatedStatus(els.riderSessionSummary, "riderCreatedPending", { name: state.rider.name });
} catch (error) {
setTranslatedStatus(els.riderStatus, "riderAccountFailed", { message: riderApplicationErrorMessage(error) });
} finally {
setButtonBusy(els.riderSubmitButton, false);
}
}
// Runtime render loop, event wiring, install flow, service worker registration, and startup.
function renderAll() {
applyLanguage();
renderEntryExperience();
renderAccountWorkspaces();
renderRiderFlow();
renderRiderStatus();
renderRoleWorkspace();
renderMap();
renderRequests();
renderOffers();
renderSelectedSummary();
renderChat();
updateConnectionStatus();
}
function ensureAutomaticLocationServices() {
if (typeof ensurePassengerPickupGpsAutoCapture === "function") ensurePassengerPickupGpsAutoCapture();
if (typeof ensureAutomaticRiderGps === "function") ensureAutomaticRiderGps();
}
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 chip(text) {
return `${escapeHtml(text)} `;
}
function wireEvents() {
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));
});
els.backToRoleEntry?.addEventListener("click", showRoleEntryScreen);
document.querySelectorAll("[data-account-type][data-account-mode]").forEach((button) => {
button.addEventListener("click", () => setAccountMode(button.dataset.accountType, button.dataset.accountMode));
});
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);
wireDateOfBirthInput(els.riderDob);
els.passengerCountry.addEventListener("change", updatePassengerCityOptions);
els.passengerCity.addEventListener("change", updatePickupOptions);
els.pickupArea.addEventListener("change", updateFareGuidance);
els.destinationArea.addEventListener("change", updateFareGuidance);
els.rideStops.addEventListener("input", updateFareGuidance);
els.vehiclePreference.addEventListener("change", renderAll);
els.passengerActiveCountry.addEventListener("change", updatePassengerActiveCityOptions);
els.languageSelect.addEventListener("change", () => {
state.language = els.languageSelect.value;
saveState();
renderAll();
});
els.sendPassengerCode.addEventListener("click", () => sendVerificationCode("passenger"));
els.verifyPassengerPhone.addEventListener("click", () => verifyPhone("passenger"));
els.passengerSignInForm.addEventListener("submit", (event) => {
event.preventDefault();
verifySignIn("passenger");
});
els.sendPassengerSignInCode.addEventListener("click", () => sendSignInCode("passenger"));
els.verifyPassengerSignIn.addEventListener("click", () => verifySignIn("passenger"));
els.passengerSignOut.addEventListener("click", () => signOutRole("passenger"));
els.sendRiderCode.addEventListener("click", () => sendVerificationCode("rider"));
els.verifyRiderPhone.addEventListener("click", () => verifyPhone("rider"));
els.riderSignInForm.addEventListener("submit", (event) => {
event.preventDefault();
verifySignIn("rider");
});
els.sendRiderSignInCode.addEventListener("click", () => sendSignInCode("rider"));
els.verifyRiderSignIn.addEventListener("click", () => verifySignIn("rider"));
els.riderSignOut.addEventListener("click", () => signOutRole("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.riderActiveCountry.addEventListener("change", updateRiderActiveCityOptions);
els.riderActiveCity.addEventListener("change", updateRiderActiveAreas);
els.passengerAccountForm.addEventListener("submit", createPassenger);
els.passengerPaymentForm.addEventListener("submit", (event) => savePaymentSetup("passenger", event));
els.passengerLocationForm.addEventListener("submit", updatePassengerActiveLocation);
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) => savePaymentSetup("rider", event));
els.riderLocationForm.addEventListener("submit", updateRiderActiveLocation);
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);
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);
window.addEventListener("online", updateConnectionStatus);
window.addEventListener("offline", updateConnectionStatus);
window.addEventListener("hashchange", applyRouteTab);
window.addEventListener("beforeinstallprompt", (event) => {
event.preventDefault();
deferredInstallPrompt = event;
updateInstallButton();
});
window.addEventListener("appinstalled", () => {
deferredInstallPrompt = null;
updateInstallButton();
});
}
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 navigator.serviceWorker.register("sw.js");
} catch {
if (appConfig.mode === "supabase") {
updateConnectionStatus();
} else {
setTranslatedStatus(els.connectionStatus, "localMode");
}
}
}
async function finishSupabaseStartup() {
await initSupabaseClient();
updateConnectionStatus();
renderAll();
}
async function boot() {
await loadRuntimeConfig();
hardenStateForRuntime();
state.activeTab = availableWorkspaceTab(requestedTabFromLocation() ?? preferredSignedInTab() ?? state.activeTab) ?? defaultRuntimeTab();
populateLocationFields();
hydrateForms();
const showEntryOnBoot = shouldShowRoleEntry();
switchTab(state.activeTab, { updateUrl: !showEntryOnBoot, preserveEntry: showEntryOnBoot });
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;
});