// 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",
"Alabama",
"Alaska",
"Arizona",
"Arkansas",
"California",
"Colorado",
"Connecticut",
"Delaware",
"District of Columbia",
"Florida",
"Georgia",
"Hawaii",
"Idaho",
"Illinois",
"Indiana",
"Iowa",
"Kansas",
"Kentucky",
"Louisiana",
"Maine",
"Massachusetts",
"Michigan",
"Minnesota",
"Mississippi",
"Missouri",
"Montana",
"Nebraska",
"Nevada",
"New Hampshire",
"New Jersey",
"New Mexico",
"New York",
"North Carolina",
"North Dakota",
"Ohio",
"Oklahoma",
"Oregon",
"Pennsylvania",
"Rhode Island",
"South Carolina",
"South Dakota",
"Tennessee",
"Texas",
"Utah",
"Vermont",
"Virginia",
"Washington",
"West Virginia",
"Wisconsin",
"Wyoming"
],
Uruguay: ["Montevideo", "Salto", "Paysandu", "Las Piedras", "Maldonado"],
Venezuela: ["Caracas", "Maracaibo", "Valencia", "Barquisimeto", "Maracay"],
Albania: ["Tirana", "Durres", "Vlore", "Shkoder", "Fier"],
Austria: ["Vienna", "Graz", "Linz", "Salzburg", "Innsbruck"],
Belgium: ["Brussels", "Antwerp", "Ghent", "Charleroi", "Liege"],
Bulgaria: ["Sofia", "Plovdiv", "Varna", "Burgas", "Ruse"],
Croatia: ["Zagreb", "Split", "Rijeka", "Osijek", "Zadar"],
Czechia: ["Prague", "Brno", "Ostrava", "Plzen", "Liberec"],
Denmark: ["Copenhagen", "Aarhus", "Odense", "Aalborg", "Esbjerg"],
Finland: ["Helsinki", "Espoo", "Tampere", "Vantaa", "Turku"],
France: ["Paris", "Marseille", "Lyon", "Toulouse", "Nice", "Nantes"],
Germany: ["Berlin", "Hamburg", "Munich", "Cologne", "Frankfurt", "Stuttgart"],
Greece: ["Athens", "Thessaloniki", "Patras", "Heraklion", "Larissa"],
Hungary: ["Budapest", "Debrecen", "Szeged", "Miskolc", "Pecs"],
Ireland: ["Dublin", "Cork", "Limerick", "Galway", "Waterford"],
Italy: ["Rome", "Milan", "Naples", "Turin", "Palermo", "Florence"],
Netherlands: ["Amsterdam", "Rotterdam", "The Hague", "Utrecht", "Eindhoven"],
Norway: ["Oslo", "Bergen", "Trondheim", "Stavanger", "Drammen"],
Poland: ["Warsaw", "Krakow", "Lodz", "Wroclaw", "Poznan"],
Portugal: ["Lisbon", "Porto", "Braga", "Coimbra", "Faro"],
Romania: ["Bucharest", "Cluj-Napoca", "Timisoara", "Iasi", "Constanta"],
Serbia: ["Belgrade", "Novi Sad", "Nis", "Kragujevac", "Subotica"],
Spain: ["Madrid", "Barcelona", "Valencia", "Seville", "Bilbao", "Malaga"],
Sweden: ["Stockholm", "Gothenburg", "Malmo", "Uppsala", "Vasteras"],
Switzerland: ["Zurich", "Geneva", "Basel", "Lausanne", "Bern"],
Turkey: ["Istanbul", "Ankara", "Izmir", "Bursa", "Antalya"],
Ukraine: ["Kyiv", "Kharkiv", "Odesa", "Dnipro", "Lviv"],
"United Kingdom": ["London", "Birmingham", "Manchester", "Glasgow", "Liverpool", "Leeds"],
Bangladesh: ["Dhaka", "Chittagong", "Khulna", "Sylhet", "Rajshahi"],
China: ["Shanghai", "Beijing", "Guangzhou", "Shenzhen", "Chengdu", "Wuhan"],
India: ["Delhi", "Mumbai", "Bengaluru", "Hyderabad", "Chennai", "Kolkata"],
Indonesia: ["Jakarta", "Surabaya", "Bandung", "Medan", "Makassar"],
Iran: ["Tehran", "Mashhad", "Isfahan", "Shiraz", "Tabriz"],
Iraq: ["Baghdad", "Basra", "Mosul", "Erbil", "Najaf"],
Israel: ["Tel Aviv", "Jerusalem", "Haifa", "Beersheba", "Netanya"],
Japan: ["Tokyo", "Osaka", "Yokohama", "Nagoya", "Sapporo", "Fukuoka"],
Jordan: ["Amman", "Zarqa", "Irbid", "Aqaba", "Madaba"],
Kazakhstan: ["Almaty", "Astana", "Shymkent", "Karaganda", "Aktobe"],
Kuwait: ["Kuwait City", "Hawalli", "Salmiya", "Farwaniya", "Jahra"],
Lebanon: ["Beirut", "Tripoli", "Sidon", "Tyre", "Zahle"],
Malaysia: ["Kuala Lumpur", "George Town", "Johor Bahru", "Ipoh", "Kota Kinabalu"],
Nepal: ["Kathmandu", "Pokhara", "Lalitpur", "Biratnagar", "Birgunj"],
Pakistan: ["Karachi", "Lahore", "Islamabad", "Rawalpindi", "Faisalabad"],
Philippines: ["Manila", "Quezon City", "Cebu City", "Davao City", "Caloocan"],
Qatar: ["Doha", "Al Rayyan", "Al Wakrah", "Umm Salal", "Al Khor"],
"Saudi Arabia": ["Riyadh", "Jeddah", "Mecca", "Medina", "Dammam"],
Singapore: ["Singapore", "Jurong East", "Tampines", "Woodlands", "Yishun"],
"South Korea": ["Seoul", "Busan", "Incheon", "Daegu", "Daejeon"],
"Sri Lanka": ["Colombo", "Kandy", "Galle", "Jaffna", "Negombo"],
Thailand: ["Bangkok", "Chiang Mai", "Pattaya", "Phuket", "Nonthaburi"],
"United Arab Emirates": ["Dubai", "Abu Dhabi", "Sharjah", "Ajman", "Al Ain"],
Uzbekistan: ["Tashkent", "Samarkand", "Bukhara", "Namangan", "Andijan"],
Vietnam: ["Ho Chi Minh City", "Hanoi", "Da Nang", "Can Tho", "Hai Phong"]
};
const africanRidePaymentCountries = new Set([
"Algeria",
"Angola",
"Benin",
"Botswana",
"Burkina Faso",
"Burundi",
"Cabo Verde",
"Cameroon",
"Central African Republic",
"Chad",
"Comoros",
"Congo",
"Democratic Republic of the Congo",
"Cote d'Ivoire",
"Djibouti",
"Egypt",
"Equatorial Guinea",
"Eritrea",
"Eswatini",
"Ethiopia",
"Gabon",
"Gambia",
"Ghana",
"Guinea",
"Guinea-Bissau",
"Kenya",
"Lesotho",
"Liberia",
"Libya",
"Madagascar",
"Malawi",
"Mali",
"Mauritania",
"Mauritius",
"Morocco",
"Mozambique",
"Namibia",
"Niger",
"Nigeria",
"Rwanda",
"Sao Tome and Principe",
"Senegal",
"Seychelles",
"Sierra Leone",
"Somalia",
"South Africa",
"South Sudan",
"Sudan",
"Tanzania",
"Togo",
"Tunisia",
"Uganda",
"Zambia",
"Zimbabwe"
]);
const onlineRidePaymentValues = new Set(["online_card", "online_wallet"]);
const productionOnlineRidePaymentProviderPattern = /\b(stripe|adyen|paypal|checkout|paystack|flutterwave|rapyd)\b/i;
const 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", "Hatchback", "Minivan", "Wagon", "Pickup", "Coupe", "Convertible", "Luxury"];
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 = 100;
const passengerApproachRefreshIntervalMs = 15 * 1000;
const riderMarketplaceRefreshIntervalMs = 20 * 1000;
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,
relaxPaymentSetupForTesting: 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 what Waka should review",
offlineReady: "Offline-ready",
onlineDemo: "Online demo",
localMode: "Local mode",
supabaseReady: "Supabase ready",
supabaseConfigNeeded: "Supabase config needed",
supabaseConnecting: "Supabase connecting",
supabaseSdkUnavailable: "Supabase SDK unavailable",
manualPhoneVerified: "Manual pilot mode: phone marked verified. Configure SMS OTP before public launch.",
smsVerificationRelaxedForTesting: "Testing mode: SMS phone verification skipped. Email/password account creation can continue; enable real SMS OTP before public launch.",
validPhoneRequired: "Enter a valid phone number before requesting a code.",
validDateOfBirthRequired: "Enter a valid date of birth as YYYY-MM-DD. You can type only digits and Waka will add the dashes.",
checkingPassengerAccount: "Checking passenger account details...",
checkingRiderApplication: "Checking rider application details...",
accountMissingFields: "Complete these fields before saving: {fields}.",
phoneOtpCooldown: "Please wait {seconds}s before requesting another phone code.",
phoneOtpRateLimited: "Too many phone-code attempts. Wait a while before requesting another code, and check Supabase Auth rate limits if this continues.",
sendingVerificationCode: "Sending verification code...",
verificationCodeSent: "Verification code sent to {phone}.",
demoCode: "Demo code: {code} for {phone}",
freshVerificationCodeRequired: "Request a fresh verification code for this phone number.",
verifyingPhoneNumber: "Verifying phone number...",
phoneNumberVerified: "Phone number verified. This only verifies the phone; press Save or Submit to finish creating the Waka account.",
verificationCodeIncorrect: "Verification code is not correct.",
phoneOtpManualSignIn: "Phone OTP sign-in is disabled in manual pilot mode. Use email and password to sign in.",
sendingSignInCode: "Sending sign-in code...",
signInCodeSent: "Sign-in code sent to {phone}.",
demoSignInCode: "Demo sign-in code: {code} for {phone}",
signingInPassword: "Signing in with email and password...",
loadingWakaProfile: "Loading Waka profile...",
supabaseProfileMissing: "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. Add a passenger payment method before requesting rides.",
signedInRiderLoaded: "Signed in as {email}. Rider profile loaded.",
signedInAs: "Signed in as {identity}.",
freshSignInCodeRequired: "Request a fresh sign-in code for this phone number.",
signInCodeRequired: "Use email and password to sign in. Phone OTP is only for first-time phone verification.",
passwordSignInOnly: "Use email and password to sign in. Phone OTP is only for first-time phone verification.",
signInEmailPasswordRequired: "Enter the email and password for this account. Phone-code sign-in is disabled in manual pilot mode.",
signingIn: "Signing in...",
signInCodeIncorrect: "Sign-in code is not correct.",
localSignInAccountMissing: "No saved {type} account matches this phone number. Create and save the {type} account first, then sign in.",
signedOut: "Signed out.",
passengerPhoneBeforeSave: "Verify the passenger phone number before saving the account.",
riderPhoneBeforeReview: "Verify the rider phone number before submitting for review.",
passengerPaymentRequired: "Add a passenger payment method under Payment before publishing rides.",
riderPaymentRequired: "Save a rider payment account before receiving requests.",
riderDailyRegionsRequired: "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. Add a passenger payment method next, then request rides.",
passengerCreatedEmailPending: "{name} passenger account created successfully. Add a passenger payment method next; email/password sign-in may need Supabase email confirmation or setup.",
passengerAccountFailed: "Passenger account was not created: {message}",
passengerSyncing: "{name} passenger account created successfully. Add a passenger payment method next, then request rides.",
startingRiderSupabase: "Starting rider save in Supabase...",
savingRiderApplication: "Saving rider application...",
submittingRiderApplication: "Submitting rider application for admin approval...",
riderCreatedPending: "{name} account created. Rider application is pending admin approval. If approved, the 30-day free trial starts 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. Contact Waka opens after a rider is selected.",
safetyReportNeedsDetail: "Add enough detail for Waka to understand the request.",
safetyReportSignInRequired: "Sign in again before contacting Waka.",
submittingSafetySupabase: "Sending message to Waka...",
savingSafetyReport: "Saving message for Waka review...",
safetyReportSubmitted: "Safety report submitted for admin review. Message sent to Waka.",
safetyReportFailed: "Could not submit report: {message}",
suspendRiderConfirm: "Suspend this rider? They will stop seeing and accepting ride requests immediately.",
clearDemoConfirm: "Clear all locally stored demo data?",
requestConfirmationFailed: "Could not request confirmation: {message}",
confirmScheduledFailed: "Could not confirm this scheduled ride: {message}",
reopenScheduledFailed: "Could not reopen this scheduled ride: {message}",
stop: "Stop",
androidInstallHelp: "On Android, open this site in Chrome, tap the menu, then choose Add to Home screen or Install app."
},
fr: {
admin: "Administrateur",
pageTitle: "Waka - Courses negociees",
installed: "Installee",
chooseAccountType: "Choisissez le type de compte",
continueAsPassenger: "Continuer comme passager",
continueAsRider: "Continuer comme conducteur",
signInOrCreate: "Connexion ou creation de compte",
createAccount: "Creer un compte",
paySubscription: "Ouvrir le paiement automatique",
passengerPanelSubtitle: "Demandez une course et choisissez la meilleure offre",
riderPanelSubtitle: "Postulez, abonnez-vous, puis negociez les courses",
email: "E-mail",
password: "Mot de passe",
phoneNumber: "Numero de telephone",
otpCode: "Code OTP",
sendOtp: "Envoyer OTP",
sendCode: "Envoyer le code",
verify: "Verifier",
signOut: "Se deconnecter",
fullName: "Nom complet",
profilePicture: "Photo de profil",
phoneVerificationCode: "Code de verification telephone",
nationalIdNumber: "Numero d'identification nationale",
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: "choice",
rider: "choice"
},
passengerPage: "request",
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 selectedCurrentPickupGps = null;
let passengerPickupGpsPromise = null;
let passengerPickupGpsWatchId = null;
let selectedPickupPlace = null;
let selectedDestinationPlace = null;
const destinationPlaceDetailsCache = new Map();
const placeAutocompleteCache = new Map();
let placesAutocompleteRateLimitedUntil = 0;
let pickupAutocompleteTimer = null;
let pickupAutocompleteSessionToken = null;
let pickupAutocompleteRequestId = 0;
let destinationAutocompleteTimer = null;
let destinationAutocompleteSessionToken = null;
let destinationAutocompleteRequestId = 0;
let fareGuidanceTimer = null;
let fareGuidanceRequestId = 0;
let fareGuidanceInFlightKey = "";
let lastRouteFareGuidance = null;
let lastRouteFareGuidanceKey = "";
let lastRouteEstimateError = null;
let pendingLowFareOverrideKey = "";
let useCurrentPickupActivationInFlight = false;
let passengerApproachRefreshTimer = null;
let riderMarketplaceRefreshTimer = null;
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",
"preferredLanguage",
"country",
"city",
"area",
"vehicle",
"carBodyType",
"status",
"approvedAt",
"trialEndsAt",
"subscriptionPaidUntil",
"rating",
"backgroundCheckStatus",
"backgroundCheckDecision",
"createdAt"
]);
function minimizedPrivateStatePatch() {
return {
adminDetail: null,
adminDirectorySearch: "",
adminDirectoryRegion: "",
adminDirectoryPages: {
passengers: 0,
riders: 0
},
selectedRequestId: null,
requests: [],
paymentRequests: [],
paymentAccounts: [],
businessAccounts: [],
businessSubscriptions: [],
rideSettlements: [],
rideTips: [],
riderDayPreferences: [],
backgroundChecks: [],
taxIdentityReferences: [],
taxDocuments: [],
rideRatings: [],
offers: [],
chats: [],
notifications: [],
safetyReports: []
};
}
function shouldMinimizeStoredProfileData() {
return appConfig.mode === "supabase" || strictProductionModeEnabled();
}
function minimalStorageAccount(record) {
return Object.fromEntries(
Object.entries(record).filter(([key, value]) => storageMinimalAccountKeys.has(key) && value !== undefined)
);
}
function storageSafeAccount(record, options = {}) {
if (!record || typeof record !== "object") return record ?? null;
const copy = { ...record };
delete copy.password;
delete copy.passcode;
delete copy.code;
delete copy.access_token;
delete copy.refresh_token;
return options.minimizeProfileData ? minimalStorageAccount(copy) : copy;
}
function storageSafeAccounts(records = [], options = {}) {
return Array.isArray(records) ? records.map((record) => storageSafeAccount(record, options)).filter(Boolean) : [];
}
function storageSafeVerification(verification, options = {}) {
if (options.minimizeProfileData) return null;
if (!verification?.verifiedAt) return null;
const phone = verification.phone ?? verification.verifiedPhone ?? "";
if (!phone) return null;
return {
phone,
phoneDigits: verification.phoneDigits ?? phoneDigits(phone),
verifiedPhone: verification.verifiedPhone ?? phone,
verifiedAt: verification.verifiedAt,
userId: verification.userId ?? null,
provider: verification.provider ?? "unknown"
};
}
function storageSafeSession(session, options = {}) {
if (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 = minimizeProfileData ? null : storageSafeAdminSession(state.adminSession);
return {
...state,
...(minimizeProfileData ? minimizedPrivateStatePatch() : {}),
verification: {
passenger: storageSafeVerification(state.verification?.passenger, storageOptions),
rider: storageSafeVerification(state.verification?.rider, storageOptions),
passengerSignIn: null,
riderSignIn: null
},
sessions: {
passenger: storageSafeSession(state.sessions?.passenger, storageOptions),
rider: storageSafeSession(state.sessions?.rider, storageOptions)
},
adminSession: safeAdminSession,
adminDetail: safeAdminSession ? state.adminDetail : null,
passenger: storageSafeAccount(state.passenger, storageOptions),
rider: storageSafeAccount(state.rider, storageOptions),
passengers: storageSafeAccounts(minimizeProfileData ? [state.passenger].filter(Boolean) : state.passengers, storageOptions),
riders: storageSafeAccounts(minimizeProfileData ? [state.rider].filter(Boolean) : state.riders, storageOptions)
};
}
function minimizeRuntimeProfileState() {
const storageOptions = { minimizeProfileData: true };
state.verification = {
passenger: null,
rider: null,
passengerSignIn: null,
riderSignIn: null
};
state.sessions = {
passenger: null,
rider: null
};
Object.assign(state, minimizedPrivateStatePatch());
state.passenger = storageSafeAccount(state.passenger, storageOptions);
state.rider = storageSafeAccount(state.rider, storageOptions);
state.passengers = storageSafeAccounts([state.passenger].filter(Boolean), storageOptions);
state.riders = storageSafeAccounts([state.rider].filter(Boolean), storageOptions);
clearStateLookupIndexes();
}
function hardenStateForRuntime() {
const safeAdminSession = storageSafeAdminSession(state.adminSession);
let shouldRewriteStoredState = shouldMinimizeStoredProfileData();
if (shouldRewriteStoredState) minimizeRuntimeProfileState();
if (!safeAdminSession) {
shouldRewriteStoredState ||= Boolean(state.adminSession || state.adminDetail);
state.adminSession = null;
state.adminDetail = null;
if (typeof resetAdminData === "function") resetAdminData();
} else {
state.adminSession = safeAdminSession;
}
if (shouldRewriteStoredState) saveState();
}
function normalizeState(nextState) {
nextState.language ||= "en";
nextState.showRoleEntry = nextState.showRoleEntry !== false;
if (!["all", "car"].includes(nextState.filter)) nextState.filter = "all";
nextState.passengerPage = ["request", "trips", "payment", "business", "profile", "notices"].includes(nextState.passengerPage)
? nextState.passengerPage
: "request";
nextState.accountMode ||= { passenger: "choice", rider: "choice" };
nextState.accountMode.passenger = ["signin", "create"].includes(nextState.accountMode.passenger) ? nextState.accountMode.passenger : "choice";
nextState.accountMode.rider = ["signin", "create"].includes(nextState.accountMode.rider) ? nextState.accountMode.rider : "choice";
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) {
logClientWarning("Waka local state could not be saved. Continuing with in-memory state for this session.", error);
storageWriteWarningShown = true;
}
}
}
function clearStateLookupIndexes() {
stateLookupCache = null;
}
function stateLookupIndexes() {
const requests = state.requests ?? [];
const riders = state.riders ?? [];
const offers = state.offers ?? [];
if (
stateLookupCache
&& stateLookupCache.requests === requests
&& stateLookupCache.riders === riders
&& stateLookupCache.offers === offers
&& stateLookupCache.requestCount === requests.length
&& stateLookupCache.riderCount === riders.length
&& stateLookupCache.offerCount === offers.length
) {
return stateLookupCache;
}
const requestMap = new Map(requests.map((request) => [request.id, request]));
const riderMap = new Map(riders.map((rider) => [rider.id, rider]));
const offerMap = new Map(offers.map((offer) => [offer.id, offer]));
const offersByRequestId = new Map();
offers.forEach((offer) => {
if (!offersByRequestId.has(offer.requestId)) offersByRequestId.set(offer.requestId, []);
offersByRequestId.get(offer.requestId).push(offer);
});
offersByRequestId.forEach((requestOffers) => {
requestOffers.sort((a, b) => Number(a.fare) - Number(b.fare));
});
stateLookupCache = {
requests,
riders,
offers,
requestCount: requests.length,
riderCount: riders.length,
offerCount: offers.length,
requestMap,
riderMap,
offerMap,
offersByRequestId
};
return stateLookupCache;
}
function isBundledDemoRider(record) {
const email = String(record?.email ?? "").toLowerCase();
const phone = phoneDigits(record?.phone);
const nationalId = String(record?.nationalId ?? record?.national_id_number ?? "").toUpperCase();
return bundledDemoRiderIds.has(record?.id)
|| bundledDemoRiderEmails.has(email)
|| bundledDemoRiderPhones.has(phone)
|| bundledDemoRiderNationalIds.has(nationalId);
}
function stripBundledDemoData(nextState) {
if (isBundledDemoRider(nextState.rider)) {
nextState.rider = null;
if (nextState.sessions) nextState.sessions.rider = null;
}
nextState.riders = (nextState.riders ?? []).filter((rider) => !isBundledDemoRider(rider));
nextState.requests = (nextState.requests ?? []).filter((request) => !bundledDemoRequestIds.has(request.id));
nextState.offers = (nextState.offers ?? []).filter((offer) => !bundledDemoOfferIds.has(offer.id) && !bundledDemoRiderIds.has(offer.riderId));
nextState.taxIdentityReferences = (nextState.taxIdentityReferences ?? []).filter((reference) => !bundledDemoRiderIds.has(reference.riderId));
if (bundledDemoRequestIds.has(nextState.selectedRequestId)) nextState.selectedRequestId = null;
return nextState;
}
function upsertById(items, item) {
return [item, ...items.filter((existing) => existing.id !== item.id)];
}
function makeId(prefix) {
return crypto.randomUUID ? crypto.randomUUID() : `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
// Shared formatting, validation, DOM handles, routing, and UI utility helpers.
function redactClientLogText(value) {
return String(value ?? "")
.replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[redacted-jwt]")
.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, "[redacted-email]")
.replace(/\+?\d[\d\s().-]{7,}\d/g, "[redacted-phone]")
.replace(/\b(access_token|refresh_token|authorization|apikey|api[_-]?key|secret|password|client[_-]?secret|card|cvc|setup_intent)=([^&\s"'<>]+)/gi, "$1=[redacted]")
.slice(0, 500);
}
function safeClientLogDetail(detail) {
if (!detail) return detail;
if (detail instanceof Error) {
return {
name: redactClientLogText(detail.name || "Error"),
message: redactClientLogText(detail.message || ""),
code: redactClientLogText(detail.code || detail.status || "")
};
}
if (typeof detail === "string" || typeof detail === "number" || typeof detail === "boolean") {
return redactClientLogText(detail);
}
if (typeof detail === "object") {
return {
name: redactClientLogText(detail.name || detail.error || "ClientError"),
message: redactClientLogText(detail.message || detail.msg || detail.error_description || ""),
code: redactClientLogText(detail.code || detail.status || detail.statusCode || "")
};
}
return redactClientLogText(detail);
}
function clientLogShouldRedact() {
return Boolean(
(typeof strictProductionModeEnabled === "function" && strictProductionModeEnabled())
|| (typeof appConfig === "object" && appConfig?.mode === "supabase")
);
}
function logClientWarning(message, ...details) {
const safeMessage = redactClientLogText(message);
const safeDetails = clientLogShouldRedact() ? details.map(safeClientLogDetail) : details;
console.warn(safeMessage, ...safeDetails);
}
function inertElement(name = "pruned") {
const classList = {
add() {},
remove() {},
toggle() { return false; },
contains() { return false; }
};
const element = {
name,
value: "",
checked: false,
disabled: false,
hidden: true,
textContent: "",
innerHTML: "",
className: "",
dataset: {},
style: {},
classList,
children: [],
files: [],
options: [],
selectedOptions: [],
addEventListener() {},
removeEventListener() {},
append() {},
prepend() {},
replaceChildren() {},
reset() {},
focus() {},
click() {},
setAttribute() {},
removeAttribute() {},
getAttribute() { return null; },
matches() { return false; },
closest() { return null; },
querySelector() { return inertElement(`${name}:child`); },
querySelectorAll() { return []; }
};
return element;
}
const els = {
roleEntry: document.querySelector("#roleEntry"),
workspace: document.querySelector("#workspace"),
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"),
passengerWorkspaceNav: document.querySelector("#passengerWorkspaceNav"),
passengerPaymentForm: document.querySelector("#passengerPaymentForm"),
passengerPaymentProvider: document.querySelector("#passengerPaymentProvider"),
passengerBankName: document.querySelector("#passengerBankName"),
passengerAccountHolder: document.querySelector("#passengerAccountHolder"),
passengerAccountLast4: document.querySelector("#passengerAccountLast4"),
passengerPaymentReference: document.querySelector("#passengerPaymentReference"),
passengerPaymentStatus: document.querySelector("#passengerPaymentStatus"),
startPassengerPaymentSetup: document.querySelector("#startPassengerPaymentSetup"),
passengerLocationForm: document.querySelector("#passengerLocationForm"),
passengerActiveCountry: document.querySelector("#passengerActiveCountry"),
passengerActiveCity: document.querySelector("#passengerActiveCity"),
passengerLocationStatus: document.querySelector("#passengerLocationStatus"),
businessAccountForm: document.querySelector("#businessAccountForm"),
businessName: document.querySelector("#businessName"),
businessBillingEmail: document.querySelector("#businessBillingEmail"),
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"),
passengerAccountUse: document.querySelector("#passengerAccountUse"),
passengerInitialBusinessFields: document.querySelector("#passengerInitialBusinessFields"),
passengerInitialBusinessName: document.querySelector("#passengerInitialBusinessName"),
passengerInitialBusinessBillingEmail: document.querySelector("#passengerInitialBusinessBillingEmail"),
passengerCountry: document.querySelector("#passengerCountry"),
passengerCity: document.querySelector("#passengerCity"),
passengerStatus: document.querySelector("#passengerStatus"),
passengerSaveButton: document.querySelector("#passengerSaveButton"),
rideRequestForm: document.querySelector("#rideRequestForm"),
passengerRideGate: document.querySelector("#passengerRideGate"),
pickupArea: document.querySelector("#pickupArea"),
pickupDescription: document.querySelector("#pickupDescription"),
pickupSuggestions: document.querySelector("#pickupSuggestions"),
pickupPlaceStatus: document.querySelector("#pickupPlaceStatus"),
useCurrentPickup: document.querySelector("#useCurrentPickup"),
capturePickupGps: document.querySelector("#capturePickupGps"),
clearPickupGps: document.querySelector("#clearPickupGps"),
pickupGpsStatus: document.querySelector("#pickupGpsStatus"),
destinationArea: document.querySelector("#destinationArea"),
destination: document.querySelector("#destination"),
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"),
fareReviewPanel: document.querySelector("#fareReviewPanel"),
paymentPreference: document.querySelector("#paymentPreference"),
riderSignInForm: document.querySelector("#riderSignInForm"),
riderSignInEmail: document.querySelector("#riderSignInEmail"),
riderSignInPassword: document.querySelector("#riderSignInPassword"),
riderSignInOtpPanel: document.querySelector("#riderSignInOtpPanel"),
riderSignInPhone: document.querySelector("#riderSignInPhone"),
riderSignInCode: document.querySelector("#riderSignInCode"),
sendRiderSignInCode: document.querySelector("#sendRiderSignInCode"),
verifyRiderSignIn: document.querySelector("#verifyRiderSignIn"),
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"),
riderInspectionDocument: document.querySelector("#riderInspectionDocument"),
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"),
adminStatus: inertElement("adminStatus"),
seedDemo: inertElement("seedDemo"),
clearDemo: inertElement("clearDemo"),
chatPanel: document.querySelector("#chatPanel"),
chatStatus: document.querySelector("#chatStatus"),
rideActionPanel: document.querySelector("#rideActionPanel"),
chatThread: document.querySelector("#chatThread"),
chatForm: document.querySelector("#chatForm"),
chatInput: document.querySelector("#chatInput"),
safetyReportForm: document.querySelector("#safetyReportForm"),
safetyReportCategory: document.querySelector("#safetyReportCategory"),
safetyReportSeverity: document.querySelector("#safetyReportSeverity"),
safetyReportDetails: document.querySelector("#safetyReportDetails"),
safetyReportStatus: document.querySelector("#safetyReportStatus"),
rideRatingForm: document.querySelector("#rideRatingForm"),
rideRatingScore: document.querySelector("#rideRatingScore"),
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();
const allowed = ["sedan", "suv", "hatchback", "minivan", "wagon", "pickup", "coupe", "convertible", "luxury"];
return allowed.includes(normalized) ? normalized : "sedan";
}
function carBodyTypeLabel(value) {
const normalized = normalizeCarBodyType(value);
const labels = {
sedan: "Sedan",
suv: "SUV",
hatchback: "Hatchback",
minivan: "Minivan",
wagon: "Wagon",
pickup: "Pickup",
coupe: "Coupe",
convertible: "Convertible",
luxury: "Luxury"
};
return labels[normalized] ?? "Sedan";
}
function 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 updateAccountPhoneVerificationControls() {
const relaxed = smsVerificationRelaxedForTesting();
[
els.passengerVerificationCode?.closest(".verification-row"),
els.riderVerificationCode?.closest(".verification-row")
].filter(Boolean).forEach((row) => {
row.hidden = relaxed;
});
}
function setButtonBusy(button, busy) {
if (!button) return;
button.disabled = busy;
button.setAttribute("aria-busy", String(busy));
}
function formatDateTime(value) {
if (!value) return "Not scheduled";
return new Intl.DateTimeFormat("en", {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit"
}).format(new Date(value));
}
function countryNames() {
const first = defaultLaunchCountry();
return [
first,
...Object.keys(countryCities).filter((country) => country !== first)
];
}
function cityNames(country = defaultLaunchCountry()) {
return Object.keys(countries[country] ?? {});
}
function locationSubdivisionLabel(country = defaultLaunchCountry()) {
return country === "United States" ? "state" : "city";
}
function areas(country = defaultLaunchCountry(), city = defaultLaunchCity(country)) {
return countries[country]?.[city] ?? [];
}
function findArea(country, city, name) {
return areas(country, city).find((area) => area.name === name) ?? areas(country, city)[0];
}
function areaDistanceUnits(firstArea, secondArea) {
if (!firstArea || !secondArea) return null;
return Math.hypot(firstArea.x - secondArea.x, firstArea.y - secondArea.y);
}
function citySpanKm(country, city) {
return cityDistanceSpanKm[country]?.[city] ?? defaultCitySpanKm;
}
function estimatedAreaDistanceKm(country, city, firstArea, secondArea) {
const distance = areaDistanceUnits(firstArea, secondArea);
if (distance == null) return null;
return (distance / 100) * citySpanKm(country, city);
}
function formatDistanceKm(value) {
if (value == null) return "distance not estimated";
if (value < 0.2) return "same pickup area";
if (value < 1) return `${Math.round(value * 1000)} m away`;
return `${value.toFixed(value < 10 ? 1 : 0)} km away`;
}
function formatDistanceMiles(value) {
if (value == null || !Number.isFinite(Number(value))) return "distance not estimated";
if (value < 0.2) return "same pickup area";
if (value < 1) return `${Math.round(value * 5280)} ft away`;
return `${value.toFixed(value < 10 ? 1 : 0)} mi away`;
}
function pickupEtaMinutes(distanceKm, rider = currentRiderRecord()) {
if (distanceKm == null || !Number.isFinite(Number(distanceKm))) return null;
const speedKmh = riderPickupEtaSpeedKmh[rider?.vehicle] ?? riderPickupEtaSpeedKmh.car;
return Math.max(2, Math.ceil((Number(distanceKm) * riderPickupEtaRoadFactor * 60) / speedKmh));
}
function formatPickupEta(minutes) {
if (minutes == null) return "pickup ETA not estimated";
if (minutes < 60) return `about ${minutes} min pickup`;
const hours = Math.floor(minutes / 60);
const remainder = minutes % 60;
return remainder ? `about ${hours} hr ${remainder} min pickup` : `about ${hours} hr pickup`;
}
function populateLocationFields() {
const countriesList = countryNames();
const passengerCountry = countriesList.includes(state.passenger?.country) ? state.passenger.country : defaultLaunchCountry();
populateSelect(els.passengerCountry, countriesList, passengerCountry);
populateSelect(els.passengerActiveCountry, countriesList, passengerCountry);
const passengerCity = state.passenger?.city ?? cityNames(passengerCountry)[0];
populateSelect(els.passengerCity, cityNames(passengerCountry), passengerCity);
populateSelect(els.passengerActiveCity, cityNames(passengerCountry), passengerCity);
populateSelect(els.pickupArea, areas(passengerCountry, passengerCity).map((area) => area.name), areas(passengerCountry, passengerCity)[0]?.name);
populateSelect(els.destinationArea, areas(passengerCountry, passengerCity).map((area) => area.name), areas(passengerCountry, passengerCity)[1]?.name ?? areas(passengerCountry, passengerCity)[0]?.name);
populateSelectOptions(els.vehiclePreference, carTypePreferenceOptions, normalizeCarTypePreference(els.vehiclePreference?.value));
updateRidePaymentOptions(passengerCountry);
updateFareGuidance();
const riderCountry = countriesList.includes(state.rider?.country) ? state.rider.country : passengerCountry;
const riderCity = state.rider?.city ?? cityNames(riderCountry)[0];
populateSelect(els.riderCountry, countriesList, riderCountry);
populateSelect(els.riderCity, cityNames(riderCountry), riderCity);
populateSelect(els.riderArea, areas(riderCountry, riderCity).map((area) => area.name), state.rider?.area ?? areas(riderCountry, riderCity)[0]?.name);
populateSelect(els.riderActiveCountry, countriesList, riderCountry);
populateSelect(els.riderActiveCity, cityNames(riderCountry), riderCity);
populateSelect(els.riderActiveArea, areas(riderCountry, riderCity).map((area) => area.name), state.rider?.area ?? areas(riderCountry, riderCity)[0]?.name);
populateRiderDailyRegionOptions(riderCountry, riderCity);
populateVehicleCatalogFields(state.rider);
}
function hydrateForms() {
els.languageSelect.value = state.language;
updateAccountPhoneVerificationControls();
updatePassengerInitialBusinessFields();
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;
const passengerSubdivision = locationSubdivisionLabel(state.passenger.country);
const passengerPhoneStatus = smsVerificationRelaxedForTesting()
? "Phone verification is relaxed for this staging pilot."
: "Phone verified.";
els.passengerStatus.textContent = `${state.passenger.name} is ready to request rides in ${state.passenger.city}. ${passengerPhoneStatus}`;
els.passengerLocationStatus.textContent = `Ride requests publish in ${state.passenger.city} ${passengerSubdivision}, ${state.passenger.country}.`;
const passengerPayment = paymentAccountFor("passenger", state.passenger.id);
if (passengerPayment) {
els.passengerPaymentProvider.value = passengerPayment.provider;
els.passengerBankName.value = passengerPayment.institutionName ?? "";
els.passengerAccountHolder.value = passengerPayment.accountHolder ?? "";
els.passengerAccountLast4.value = passengerPayment.accountLast4 ?? "";
els.passengerPaymentReference.value = passengerPayment.reference ?? "";
els.passengerPaymentStatus.textContent = paymentAccountSummary("passenger", state.passenger);
}
}
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,
vehicleInspectionDocumentPath: documents.vehicleInspection,
status: row.application_status ?? "profile only",
approvedAt: row.reviewed_at ?? null,
trialEndsAt: row.trial_ends_at ?? null,
subscriptionPaidUntil: row.paid_until ?? null,
rating: "new"
};
}
function 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);
}
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;
logClientWarning("Passenger location RPC is not installed yet. Falling back to direct profile update.", error);
}
}
assertClientFallbackAllowed("Passenger location update", "supabase-location-update-rpc.sql");
lastLocationUpdateSource = "direct location update fallback";
await updateProfileLocationInSupabase(profileId, country, city);
}
async function updateRiderCurrentAreaInSupabase(riderId, country, city, area) {
if (!hasSupabaseRuntime() || !riderId) return;
if (!locationUpdateRpcUnavailable.rider) {
try {
await callSupabaseRpc(
"rider_update_current_area",
{
p_country: country,
p_city: city,
p_area: area
},
"Updating rider area",
supabaseProfileSaveTimeoutMs
);
lastLocationUpdateSource = "location update RPC";
return;
} catch (error) {
if (!adminDirectoryRpcMissing(error)) throw error;
locationUpdateRpcUnavailable.rider = true;
logClientWarning("Rider location RPC is not installed yet. Falling back to direct profile and rider location updates.", error);
}
}
assertClientFallbackAllowed("Rider location update", "supabase-location-update-rpc.sql");
lastLocationUpdateSource = "direct location update fallback";
await updateProfileLocationInSupabase(riderId, country, city);
await updateRiderApplicationLocationInSupabase(riderId, area);
}
async function updateRiderLiveGpsWithRpc(rider) {
const currentGps = riderCurrentGps(rider);
if (!currentGps) return false;
await callSupabaseRpc(
"rider_update_live_gps",
{
p_lat: currentGps.latitude,
p_lng: currentGps.longitude,
p_accuracy_meters: currentGps.accuracyMeters ?? null,
p_captured_at: currentGps.capturedAt ?? null
},
"Updating rider live GPS",
optionalSupabaseRequestTimeoutMs
);
lastLocationUpdateSource = "location update RPC";
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;
logClientWarning("Rider clear live GPS RPC is not installed yet. Falling back to direct rider location upsert.", error);
}
}
assertClientFallbackAllowed("Rider live GPS clearing", "supabase-location-update-rpc.sql");
await updateRiderLocationPresenceInSupabase(clearRiderLiveGpsFields(rider));
}
async function expireRiderLiveGpsIfNeeded() {
const rider = currentRiderRecord();
if (!riderLiveGpsNeedsClearing(rider)) return false;
const 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;
logClientWarning("Rider live GPS RPC is not installed yet. Falling back to direct rider location upsert.", error);
}
}
const location = gpsPointToDatabase(currentGps);
const payload = {
rider_id: rider.id,
city: rider.city,
area_label: rider.area,
is_online: 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) {
logClientWarning("Rider live location was not updated.", error);
}
}
function reportSupabaseStep(onStage, message) {
if (typeof onStage === "function") onStage(message);
}
function pause(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isMissingAuthSessionError(error) {
return Boolean(error?.message && /auth session missing|session.*missing/i.test(error.message));
}
function isMissingJwtUserError(error) {
return Boolean(error?.message && /user from sub claim in jwt does not exist/i.test(error.message));
}
function isInvalidLoginCredentialsError(error) {
return Boolean(error?.message && /invalid login credentials/i.test(error.message));
}
function isAlreadyRegisteredError(error) {
return Boolean(error?.message && /already|registered|exists/i.test(error.message));
}
function authUserEmail(user) {
return user?.email?.toLowerCase?.() ?? "";
}
function authUserMatchesVerifiedPhone(user, profile) {
return Boolean(user?.phone && profile?.phone && phoneMatches(user.phone, profile.phone));
}
async function attachEmailPasswordToVerifiedPhoneUser(user, profile, onStage) {
if (!authUserMatchesVerifiedPhone(user, profile)) return user;
const updates = {};
if (profile.email && authUserEmail(user) !== profile.email) updates.email = profile.email;
if (profile.password) updates.password = profile.password;
if (!Object.keys(updates).length || !supabaseClient) return user;
reportSupabaseStep(onStage, "Linking email and password to the verified phone account...");
const { data, error } = await withSupabaseTimeout(
supabaseClient.auth.updateUser(updates),
"Linking email/password to the verified phone account",
supabaseProfileSaveTimeoutMs
);
if (error) {
logClientWarning("Phone was verified, but email/password setup still needs attention.", error);
reportSupabaseStep(onStage, `Phone verified. Email/password sign-in still needs attention: ${error.message}`);
return {
...user,
emailSetupPending: true,
emailSetupError: error.message
};
}
const updatedUser = data?.user ?? user;
return {
...updatedUser,
emailSetupPending: Boolean(updates.email && authUserEmail(updatedUser) !== profile.email)
};
}
function clearStoredSupabaseAuthSession() {
try {
Object.keys(localStorage)
.filter((key) => /^sb-.+-auth-token$/.test(key))
.forEach((key) => localStorage.removeItem(key));
} catch (error) {
logClientWarning("Stored Supabase session could not be cleared.", error);
}
}
async function clearStaleSupabaseSession() {
clearStoredSupabaseAuthSession();
try {
await withSupabaseTimeout(
supabaseClient.auth.signOut({ scope: "local" }),
"Clearing the stale Supabase session",
optionalSupabaseRequestTimeoutMs
);
} catch (error) {
logClientWarning("Stale Supabase session could not be signed out.", error);
}
}
async function getSupabaseUser() {
if (supabaseRestSession?.user && !supabaseClient) return supabaseRestSession.user;
if (!isSupabaseMode()) return null;
if (supabaseClient.auth.getSession) {
const { data: sessionData, error: sessionError } = await withSupabaseTimeout(
supabaseClient.auth.getSession(),
"Reading the saved Supabase session",
optionalSupabaseRequestTimeoutMs
);
if (sessionError && !isMissingAuthSessionError(sessionError)) throw sessionError;
if (sessionData?.session?.user) return sessionData.session.user;
}
const { data, error } = await withSupabaseTimeout(
supabaseClient.auth.getUser(),
"Checking the current Supabase session"
);
if (isMissingAuthSessionError(error)) return null;
if (isMissingJwtUserError(error)) {
await clearStaleSupabaseSession();
return null;
}
if (error) throw error;
return data?.user ?? null;
}
async function loadSupabaseProfileForUser(user, select = "*", timeoutMessage = "Loading the Waka profile") {
if (!user?.id) return null;
if (supabaseClient) {
const { data, error } = await withSupabaseTimeout(
supabaseClient
.from("profiles")
.select(select)
.eq("id", user.id)
.maybeSingle(),
timeoutMessage,
supabaseProfileSaveTimeoutMs
);
if (error) throw error;
return data;
}
return selectProfileRest(user.id, select, supabaseRestSession?.access_token);
}
async function savePhoneVerificationEvent(userId, phone, provider = "supabase-otp") {
if (!isSupabaseMode() || !userId) return;
try {
const { error } = await withSupabaseTimeout(
supabaseClient.from("phone_verification_events").insert({
user_id: userId,
phone,
provider
}),
"Saving the phone verification audit event",
optionalSupabaseRequestTimeoutMs
);
if (error) logClientWarning("Phone verification audit event was not saved.", error);
} catch (error) {
logClientWarning("Phone verification audit event was skipped.", error);
}
}
function profileOnboardingRpcBody(profile, profilePhotoPath = null) {
return {
p_role: profile.role,
p_full_name: profile.name,
p_email: profile.email,
p_phone: profile.phone,
p_phone_verified_at: profile.phoneVerifiedAt,
p_national_id_number: profile.nationalId,
p_date_of_birth: profile.dateOfBirth,
p_preferred_language: profile.preferredLanguage,
p_country: profile.country,
p_city: profile.city,
p_profile_photo_path: profilePhotoPath,
p_phone_verification_provider: profile.phoneVerificationProvider ?? "supabase-otp"
};
}
async function upsertProfileWithOnboardingRpc(profile, user, onStage, didRefreshSession = false) {
reportSupabaseStep(onStage, didRefreshSession ? "Retrying profile onboarding RPC..." : "Syncing profile details through onboarding RPC...");
try {
await callSupabaseRpc(
"upsert_own_profile",
profileOnboardingRpcBody(profile, profile.profilePhotoPath ?? null),
"Saving the Waka profile",
supabaseProfileSaveTimeoutMs
);
lastProfileOnboardingSource = "profile onboarding RPC";
return user;
} catch (error) {
if (isMissingJwtUserError(error) && !didRefreshSession) {
reportSupabaseStep(onStage, "Supabase session mismatch found. Refreshing sign-in...");
const refreshedUser = await refreshSupabaseSignIn(profile, onStage);
await pause(800);
return upsertProfileWithOnboardingRpc(profile, refreshedUser, onStage, true);
}
throw error;
}
}
async function saveProfilePhotoPathWithRpc(profilePhotoPath) {
await callSupabaseRpc(
"save_profile_photo_path",
{ p_profile_photo_path: profilePhotoPath },
"Saving the profile photo path",
supabaseProfileSaveTimeoutMs
);
lastProfileOnboardingSource = "profile onboarding RPC";
}
async function submitRiderApplicationWithRpc(rider, documentPath) {
await callSupabaseRpc(
"submit_rider_application",
{
p_vehicle: rider.vehicle,
p_operating_area: rider.area,
p_credential_number: rider.credential,
p_vehicle_registration: rider.registration,
p_car_make: rider.carMake,
p_car_model: rider.carModel,
p_car_body_type: normalizeCarBodyType(rider.carBodyType),
p_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, options = {}) {
reportSupabaseStep(onStage, "Checking current Supabase session...");
const existingUser = await getSupabaseUser();
if (authUserMatchesVerifiedPhone(existingUser, profile)) {
if (options.preventExistingAccount) {
throw new Error("An account already exists with this phone number. Sign in with the existing account instead of creating a duplicate.");
}
return attachEmailPasswordToVerifiedPhoneUser(existingUser, profile, onStage);
}
if (authUserEmail(existingUser) === profile.email) {
if (options.preventExistingAccount) {
throw new Error("An account already exists with this email address. Sign in with the existing account instead of creating a duplicate.");
}
return existingUser;
}
if (existingUser) {
reportSupabaseStep(onStage, "Switching Supabase user...");
await clearStaleSupabaseSession();
}
if (!profile.email || !profile.password) {
throw new Error("Enter an email and password to create the Supabase account.");
}
const credentials = { email: profile.email, password: profile.password };
reportSupabaseStep(onStage, "Checking for existing Supabase account...");
const signInFirst = await withSupabaseTimeout(
supabaseClient.auth.signInWithPassword(credentials),
"Checking for an existing Supabase account"
);
if (!signInFirst.error && signInFirst.data?.user) {
if (options.preventExistingAccount) {
await clearStaleSupabaseSession();
throw new Error("An account already exists with this email address. Sign in with the existing account instead of creating a duplicate.");
}
return signInFirst.data.user;
}
if (signInFirst.error && !isInvalidLoginCredentialsError(signInFirst.error)) {
throw signInFirst.error;
}
reportSupabaseStep(onStage, "Creating Supabase auth account...");
const signUpResult = await withSupabaseTimeout(
supabaseClient.auth.signUp(credentials),
"Creating the Supabase auth account"
);
if (isAlreadyRegisteredError(signUpResult.error) && options.preventExistingAccount) {
throw new Error("An account already exists with this email address. Sign in with the existing account instead of creating a duplicate.");
}
if (signUpResult.error && !isAlreadyRegisteredError(signUpResult.error)) {
throw signUpResult.error;
}
if (signUpResult.data?.session?.user) return signUpResult.data.session.user;
reportSupabaseStep(onStage, "Signing in to Supabase...");
const signInResult = await withSupabaseTimeout(
supabaseClient.auth.signInWithPassword(credentials),
"Signing in to Supabase"
);
if (signInResult.error) {
if (isInvalidLoginCredentialsError(signInResult.error) && isAlreadyRegisteredError(signUpResult.error)) {
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;
logClientWarning("Profile onboarding RPC is not installed yet. Falling back to direct profile upsert.", error);
}
}
if (!syncedUser) {
reportSupabaseStep(onStage, "Profile onboarding RPC unavailable; using policy-protected profile save...");
syncedUser = await upsertProfilePayload(payload, profile, user, onStage);
savePhoneVerificationEvent(syncedUser.id, profile.phone, profile.phoneVerificationProvider ?? "supabase-otp");
}
queueProfilePhotoUpload(syncedUser.id, profile.role, profilePhotoInput(profile.role)?.files[0] ?? null);
return syncedUser;
}
async function saveProfileToSupabase(profile, onStage, options = {}) {
if (!isSupabaseMode()) return null;
let user = await ensureSupabaseAuthUser(profile, onStage, options);
if (!user) throw new Error("Supabase account could not be created or signed in.");
reportSupabaseStep(onStage, options.waitForProfile
? `Creating Waka ${profile.role} profile before reporting success...`
: "Account created. Finishing setup in the background...");
const profileSyncPromise = syncProfileDetailsToSupabase(profile, user, onStage)
.then((syncedUser) => {
reportSupabaseStep(onStage, `${profile.role === "rider" ? "Rider" : "Passenger"} account created and profile synced.`);
return syncedUser;
})
.catch((error) => {
reportSupabaseStep(onStage, `${profile.role === "rider" ? "Rider" : "Passenger"} account created. Profile sync still needs attention: ${error.message}`);
logClientWarning("Profile sync was not completed.", error);
if (options.waitForProfile) throw error;
return user;
});
if (options.waitForProfile) {
user = await profileSyncPromise;
}
return { ...user, profilePhotoPath: profile.profilePhotoPath ?? null, profileSyncPromise };
}
function queueProfilePhotoUpload(userId, type, file) {
if (!isSupabaseMode() || !file) return;
uploadProfilePhoto(userId, type, file)
.then(async (profilePhotoPath) => {
if (!profilePhotoPath) return;
if (!profileOnboardingRpcUnavailable.photo) {
try {
await saveProfilePhotoPathWithRpc(profilePhotoPath);
return;
} catch (error) {
if (!adminDirectoryRpcMissing(error)) throw error;
profileOnboardingRpcUnavailable.photo = true;
logClientWarning("Profile photo RPC is not installed yet. Falling back to direct profile update.", error);
}
}
assertClientFallbackAllowed("Profile photo path save", "supabase-profile-onboarding-rpc.sql");
lastProfileOnboardingSource = "direct profile photo update fallback";
const { error } = await withSupabaseTimeout(
supabaseClient.from("profiles").update({ profile_photo_path: profilePhotoPath }).eq("id", userId),
"Saving the profile photo path"
);
if (error) throw error;
})
.catch((error) => {
logClientWarning("Profile photo upload was skipped.", error);
});
}
function profilePhotoInput(type) {
return type === "rider" ? els.riderPhoto : els.passengerPhoto;
}
async function uploadProfilePhoto(userId, type, file) {
if (!isSupabaseMode() || !file) return null;
const safeName = file.name.replace(/[^a-z0-9._-]/gi, "-").toLowerCase();
const path = `${userId}/${type}-${Date.now()}-${safeName}`;
const { error } = await withSupabaseTimeout(
supabaseClient.storage
.from(appConfig.buckets.profilePhotos)
.upload(path, file, { upsert: false }),
"Uploading the profile photo"
);
if (error) throw error;
return path;
}
async function uploadRiderDocument(userId, documentType, file) {
if (!isSupabaseMode() || !file) return null;
const safeName = file.name.replace(/[^a-z0-9._-]/gi, "-").toLowerCase();
const path = `${userId}/${documentType}-${Date.now()}-${safeName}`;
const { error } = await withSupabaseTimeout(
supabaseClient.storage
.from(appConfig.buckets.riderDocuments)
.upload(path, file, { upsert: false }),
`Uploading the ${riderDocumentLabels[documentType]}`
);
if (error) throw error;
return path;
}
async function uploadRiderDocuments(userId) {
const files = selectedRiderDocumentFiles();
const entries = await Promise.all(Object.entries(files).map(async ([documentType, file]) => {
return [documentType, await uploadRiderDocument(userId, documentType, file)];
}));
return Object.fromEntries(entries);
}
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;
logClientWarning("Rider application RPC is not installed yet. Falling back to direct application write.", error);
}
}
assertClientFallbackAllowed("Rider application submission", "supabase-profile-onboarding-rpc.sql");
const { data: existingApplication, error: lookupError } = await withSupabaseTimeout(
supabaseClient
.from("rider_applications")
.select("id")
.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;
}
async function profileContactAvailability(email, phone, excludeUserId = null) {
if (!hasSupabaseRuntime()) return { emailAvailable: true, phoneAvailable: true };
const rows = await callSupabaseRpcResult(
"profile_contact_available",
{
p_email: email,
p_phone: phone,
p_exclude_user_id: excludeUserId
},
"Checking whether this email and phone are available",
optionalSupabaseRequestTimeoutMs
);
const result = Array.isArray(rows) ? rows[0] : rows;
return {
emailAvailable: result?.email_available !== false,
phoneAvailable: result?.phone_available !== false
};
}
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;
}
function passengerAccountErrorMessage(error) {
const message = String(error?.message || error || "");
if (/profiles_email|email|already exists with this email/i.test(message)) {
return "An account already exists with this email address. Sign in with that account instead of creating a duplicate.";
}
if (/profiles_phone|phone|already exists with this phone/i.test(message)) {
return "An account already exists with this phone number. Sign in with that account instead of creating a duplicate.";
}
if (/duplicate key|unique constraint/i.test(message)) {
return "This email or phone number is already used by another Waka account. Sign in with the existing account instead of creating a duplicate.";
}
return message;
}
async function sendVerificationCode(type) {
const phoneInput = type === "passenger" ? els.passengerPhone : els.riderPhone;
const status = type === "passenger" ? els.passengerStatus : els.riderStatus;
const phone = phoneInput.value.trim();
if (phone.length < 8) {
setTranslatedStatus(status, "validPhoneRequired");
return;
}
const cooldownSeconds = phoneOtpCooldownSeconds(type, phone);
if (cooldownSeconds > 0) {
setTranslatedStatus(status, "phoneOtpCooldown", { seconds: cooldownSeconds });
return;
}
if (smsVerificationRelaxedForTesting()) {
markSmsRelaxedPhoneVerified(type, phone, status);
return;
}
if (usesManualPhoneVerification()) {
markManualPhoneVerified(type, phone, status);
return;
}
if (isSupabaseMode()) {
startPhoneOtpCooldown(type, phone);
setTranslatedStatus(status, "sendingVerificationCode");
const { error } = await supabaseClient.auth.signInWithOtp({ phone });
if (error) {
if (error.status !== 429 && !/rate limit|too many/i.test(error.message)) clearPhoneOtpCooldown(type, phone);
status.textContent = phoneOtpErrorMessage(error);
return;
}
state.verification[type] = { phone, phoneDigits: phoneDigits(phone), verifiedPhone: null, provider: "supabase-otp" };
saveState();
setTranslatedStatus(status, "verificationCodeSent", { phone });
return;
}
const code = makeVerificationCode();
state.verification[type] = { phone, phoneDigits: phoneDigits(phone), code, verifiedPhone: null };
saveState();
setTranslatedStatus(status, "demoCode", { code, phone });
}
async function verifyPhone(type) {
const phoneInput = type === "passenger" ? els.passengerPhone : els.riderPhone;
const codeInput = type === "passenger" ? els.passengerVerificationCode : els.riderVerificationCode;
const status = type === "passenger" ? els.passengerStatus : els.riderStatus;
const verification = state.verification[type];
const phone = phoneInput.value.trim();
if (!verification || !phoneMatches(verification.verifiedPhone ?? verification.phone, phone)) {
setTranslatedStatus(status, "freshVerificationCodeRequired");
return false;
}
if (smsVerificationRelaxedForTesting()) {
return markSmsRelaxedPhoneVerified(type, phone, status);
}
if (usesManualPhoneVerification()) {
return markManualPhoneVerified(type, phone, status);
}
if (isSupabaseMode()) {
setTranslatedStatus(status, "verifyingPhoneNumber");
const { data, error } = await supabaseClient.auth.verifyOtp({
phone,
token: codeInput.value.trim(),
type: "sms"
});
if (error) {
status.textContent = error.message;
return false;
}
state.verification[type] = {
...verification,
phone,
phoneDigits: phoneDigits(phone),
verifiedPhone: phone,
verifiedAt: new Date().toISOString(),
userId: data.user?.id ?? null,
provider: "supabase-otp"
};
state.sessions[type] = {
phone,
userId: data.user?.id ?? null,
signedInAt: new Date().toISOString()
};
saveState();
setTranslatedStatus(status, "phoneNumberVerified");
return true;
}
if (codeInput.value.trim() !== verification.code) {
setTranslatedStatus(status, "verificationCodeIncorrect");
return false;
}
state.verification[type] = {
...verification,
phone,
phoneDigits: phoneDigits(phone),
verifiedPhone: phone,
verifiedAt: new Date().toISOString()
};
state.sessions[type] = {
phone,
userId: null,
signedInAt: new Date().toISOString()
};
saveState();
setTranslatedStatus(status, "phoneNumberVerified");
return true;
}
function hasVerifiedPhone(type, phone) {
const account = type === "passenger" ? state.passenger : state.rider;
if (account?.phone && account.phoneVerified && phoneMatches(account.phone, phone)) return true;
const verification = state.verification[type];
if (!verification?.verifiedAt) return false;
return phoneMatches(verification.verifiedPhone ?? verification.phone, phone);
}
function phoneVerificationCodeInput(type) {
return type === "passenger" ? els.passengerVerificationCode : els.riderVerificationCode;
}
function phoneVerificationStatusKey(type) {
return type === "passenger" ? "passengerPhoneBeforeSave" : "riderPhoneBeforeReview";
}
function supabaseUserPhoneVerifiedAt(user) {
return user?.phone_confirmed_at ?? user?.confirmed_at ?? user?.last_sign_in_at ?? null;
}
async function markPhoneVerifiedFromSupabaseSession(type, phone, status) {
if (!isSupabaseMode()) return false;
let user = null;
try {
user = await getSupabaseUser();
} catch (error) {
logClientWarning("Current Supabase phone session could not be checked.", error);
return false;
}
if (!user?.phone || !phoneMatches(user.phone, phone)) return false;
const verifiedAt = supabaseUserPhoneVerifiedAt(user) ?? new Date().toISOString();
state.verification[type] = {
...(state.verification[type] ?? {}),
phone,
phoneDigits: phoneDigits(phone),
verifiedPhone: user.phone,
verifiedAt,
userId: user.id ?? null,
provider: "supabase-otp"
};
state.sessions[type] = {
...(state.sessions[type] ?? {}),
phone,
userId: user.id ?? null,
signedInAt: new Date().toISOString()
};
saveState();
setTranslatedStatus(status, "phoneNumberVerified");
return true;
}
async function ensureVerifiedPhoneForAccount(type, phone, status) {
if (hasVerifiedPhone(type, phone)) return true;
if (smsVerificationRelaxedForTesting()) {
return markSmsRelaxedPhoneVerified(type, phone, status);
}
if (usesManualPhoneVerification()) {
return markManualPhoneVerified(type, phone, status);
}
const verification = state.verification[type];
const codeInput = phoneVerificationCodeInput(type);
if (codeInput?.value.trim() && verification && phoneMatches(verification.verifiedPhone ?? verification.phone, phone)) {
if (await verifyPhone(type)) return true;
return false;
}
if (await markPhoneVerifiedFromSupabaseSession(type, phone, status)) return true;
setTranslatedStatus(status, phoneVerificationStatusKey(type));
return false;
}
function hasSignedIn(type) {
return Boolean(state.sessions[type]);
}
function 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 refreshPaymentAccountsFromSupabase(type);
await loadMarketplaceFromSupabase().catch((error) => {
logClientWarning("Marketplace refresh after sign-in was skipped.", error);
});
if (type === "passenger" && paymentAccountReady("passenger", state.passenger)) {
clearPendingPaymentSetup();
state.passengerPage = "request";
saveState();
}
if (typeof handlePaymentSetupReturnFromLocation === "function") {
await handlePaymentSetupReturnFromLocation();
}
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;
try {
data = await loadSupabaseProfileForUser(user, "*", `Loading the ${type} profile`);
} catch {
return;
}
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;
let taxDocumentRows = [];
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();
const { data: taxDocumentData } = await supabaseClient
.from("rider_tax_documents")
.select("*")
.eq("rider_id", user.id)
.order("tax_year", { ascending: false });
application = applicationData;
subscription = subscriptionData;
taxIdentityReference = taxIdentityData;
taxDocumentRows = taxDocumentData ?? [];
} 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;
taxDocumentRows = await supabaseRestRequest(`/rest/v1/rider_tax_documents?rider_id=eq.${user.id}&select=*&order=tax_year.desc`, {
accessToken: supabaseRestSession?.access_token
}).catch(() => []);
}
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,
vehicleInspectionDocumentPath: documents.vehicleInspection,
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)
);
}
state.taxDocuments = [
...state.taxDocuments.filter((item) => item.riderId !== user.id),
...taxDocumentRows.map((document) => mapTaxDocumentFromDatabase(document))
];
}
saveState();
populateLocationFields();
hydrateForms();
switchTab(type);
renderAll();
}
async function restoreSignedInRoleFromSupabaseSession() {
if (!hasSupabaseRuntime()) return false;
let user = null;
try {
user = await getSupabaseUser();
} catch (error) {
logClientWarning("Stored Supabase session could not be restored.", error);
return false;
}
if (!user) return false;
let profile = null;
try {
profile = await loadSupabaseProfileForUser(user, "*", "Restoring the signed-in Waka profile");
} catch (error) {
logClientWarning("Signed-in Waka profile could not be restored.", error);
return false;
}
const role = profile?.role;
if (!["passenger", "rider"].includes(role) || !availableWorkspaceTab(role)) return false;
applySignedInProfile(role, profile, user);
state.accountMode[role] = "signin";
state.activeTab = role;
saveState();
await hydrateProfileFromSupabase(role);
await refreshPaymentAccountsFromSupabase(role);
if (role === "passenger" && paymentAccountReady("passenger", state.passenger)) {
clearPendingPaymentSetup();
state.passengerPage = "request";
saveState();
}
await loadMarketplaceFromSupabase().catch((error) => {
logClientWarning("Marketplace refresh after session restore was skipped.", error);
});
renderAll();
return true;
}
// 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 "Enter pickup and destination addresses to see the estimated fare before publishing.";
const stops = guidance.stopCount ? `, ${guidance.stopCount} stop${guidance.stopCount === 1 ? "" : "s"}` : "";
const prefix = guidance.source === "place-preview" ? "Quick estimate" : "Estimated fare";
const suffix = guidance.source === "place-preview"
? " Google Routes will refine this automatically."
: " Final fare is negotiable.";
return `${prefix}: $${guidance.min}-$${guidance.max} for ${formatDistanceMiles(guidance.distanceMiles)} and about ${guidance.minutes} minutes${stops}.${suffix}`;
}
function routeGuidancePendingMessage() {
return routeEstimatesEnabled()
? "Enter a destination and either use current location or enter a pickup address to estimate the fare before publishing."
: fareGuidanceMessage(null);
}
function routeGuidanceUnavailableMessage() {
return routeEstimateFallbackAllowedForTesting()
? "Google route distance is unavailable right now. For staging, Waka will publish with the fare you enter."
: "Google route distance is unavailable right now. Please try again in a moment.";
}
function routeGuidanceInputKey(country, city, pickupAreaName, destinationAreaName, pickupDescription, destination, stops = []) {
const pickupPlace = pickupPlaceForRoute(pickupDescription);
const destinationPlace = destinationPlaceForRoute(destination);
const origin = routeOriginForEstimate(country, city, pickupAreaName, pickupDescription, pendingPickupGps);
const pickupGps = origin.source === "browser-gps" && validGpsCoordinate(Number(origin.latitude), Number(origin.longitude))
? { latitude: Number(origin.latitude), longitude: Number(origin.longitude) }
: null;
const gpsKey = pickupGps ? `${pickupGps.latitude.toFixed(4)},${pickupGps.longitude.toFixed(4)}` : "";
return JSON.stringify({
country,
city,
pickupAreaName,
destinationAreaName,
pickupDescription: String(pickupDescription ?? "").trim(),
destination: String(destination ?? "").trim(),
pickupPlaceId: pickupPlace?.placeId ?? null,
destinationPlaceId: destinationPlace?.placeId ?? null,
gpsKey,
stops: normalizeRideStops(stops)
});
}
function fareGuidancePreviewInputs() {
const pickupDescription = els.pickupDescription?.value.trim() ?? "";
const destination = els.destination?.value.trim() ?? "";
const country = selectedPassengerCountry();
const city = state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(country);
return {
country,
city,
pickupAreaName: els.pickupArea?.value ?? "",
destinationAreaName: els.destinationArea?.value ?? "",
pickupDescription,
destination,
stops: els.rideStops?.value ?? ""
};
}
function routeEstimateErrorMessage(error) {
const message = String(error?.message || "").trim();
if (!message) return routeGuidanceUnavailableMessage();
if (/edge function returned a non-2xx status code/i.test(message)) return routeGuidanceUnavailableMessage();
return message;
}
function clearFareGuidancePreview() {
window.clearTimeout(fareGuidanceTimer);
fareGuidanceInFlightKey = "";
lastRouteFareGuidance = null;
lastRouteFareGuidanceKey = "";
}
function immediateCoordinateFareGuidance(country, city, pickupAreaName, destinationAreaName, pickupDescription, destination, stops = []) {
const origin = routeOriginForEstimate(country, city, pickupAreaName, pickupDescription, pendingPickupGps);
if (routeOriginNeedsConfirmedAddressForPricing(origin)) return null;
const destinationPlace = destinationPlaceForRoute(destination);
const originPoint = validGpsCoordinate(Number(origin?.latitude), Number(origin?.longitude))
? { latitude: Number(origin.latitude), longitude: Number(origin.longitude) }
: null;
const destinationPoint = validGpsCoordinate(Number(destinationPlace?.latitude), Number(destinationPlace?.longitude))
? { latitude: Number(destinationPlace.latitude), longitude: Number(destinationPlace.longitude) }
: null;
if (!originPoint || !destinationPoint) return null;
const straightKm = gpsDistanceKmBetween(originPoint, destinationPoint);
if (!Number.isFinite(straightKm) || straightKm <= 0) return null;
const distanceMiles = Math.max(0.6, straightKm * riderPickupEtaRoadFactor * kmToMiles);
const minutes = Math.max(3, Math.ceil(distanceMiles * 2.1));
return fareGuidanceFromDistance(distanceMiles, minutes, stops, {
source: "place-preview",
provider: "local-place-preview"
});
}
function scheduleFareGuidancePreview() {
if (!els.fareGuidance) return null;
const inputs = fareGuidancePreviewInputs();
const origin = routeOriginForEstimate(
inputs.country,
inputs.city,
inputs.pickupAreaName,
inputs.pickupDescription,
pendingPickupGps
);
if (!inputs.destination || !routeOriginIsSpecific(origin)) {
clearFareGuidancePreview();
els.fareGuidance.textContent = routeGuidancePendingMessage();
return null;
}
const key = routeGuidanceInputKey(
inputs.country,
inputs.city,
inputs.pickupAreaName,
inputs.destinationAreaName,
inputs.pickupDescription,
inputs.destination,
inputs.stops
);
if (lastRouteFareGuidance && key === lastRouteFareGuidanceKey) {
els.fareGuidance.textContent = fareGuidanceMessage(lastRouteFareGuidance);
return lastRouteFareGuidance;
}
const immediateGuidance = immediateCoordinateFareGuidance(
inputs.country,
inputs.city,
inputs.pickupAreaName,
inputs.destinationAreaName,
inputs.pickupDescription,
inputs.destination,
inputs.stops
);
els.fareGuidance.textContent = immediateGuidance
? `${fareGuidanceMessage(immediateGuidance)} Checking Google Routes now...`
: "Checking Google route distance and suggested fare...";
if (key === fareGuidanceInFlightKey) return immediateGuidance;
const requestId = ++fareGuidanceRequestId;
window.clearTimeout(fareGuidanceTimer);
fareGuidanceInFlightKey = key;
fareGuidanceTimer = window.setTimeout(async () => {
try {
const guidance = await accurateFareGuidanceForRide(
inputs.country,
inputs.city,
inputs.pickupAreaName,
inputs.destinationAreaName,
inputs.destination,
pendingPickupGps,
inputs.stops,
inputs.pickupDescription
);
if (requestId !== fareGuidanceRequestId) return;
fareGuidanceInFlightKey = "";
const finalGuidance = guidance ?? immediateGuidance;
lastRouteFareGuidance = finalGuidance;
lastRouteFareGuidanceKey = key;
if (els.fareGuidance) {
els.fareGuidance.textContent = finalGuidance
? fareGuidanceMessage(finalGuidance)
: routeEstimateErrorMessage(lastRouteEstimateError);
}
} catch (error) {
if (requestId !== fareGuidanceRequestId) return;
fareGuidanceInFlightKey = "";
lastRouteFareGuidance = immediateGuidance;
lastRouteFareGuidanceKey = immediateGuidance ? key : "";
if (els.fareGuidance) {
els.fareGuidance.textContent = immediateGuidance
? `${fareGuidanceMessage(immediateGuidance)} Google Routes is unavailable right now, so this quick estimate is shown.`
: routeEstimateErrorMessage(error);
}
}
}, 250);
return immediateGuidance;
}
function updateFareGuidance() {
if (!els.fareGuidance) return null;
if (routeEstimatesEnabled()) {
return scheduleFareGuidancePreview();
}
if (!els.destination?.value.trim()) {
els.fareGuidance.textContent = fareGuidanceMessage(null);
return null;
}
const country = selectedPassengerCountry();
const city = state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(country);
const guidance = fareGuidanceForRide(country, city, els.pickupArea?.value, els.destinationArea?.value, pendingPickupGps, 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 routeEstimateFallbackAllowedForTesting() {
return !accurateRouteEstimateRequired()
&& /\b(staging|pilot|test|preview)\b/i.test(String(appConfig.projectName || ""));
}
function routeOriginNeedsConfirmedAddressForPricing(origin) {
return false;
}
function currentLocationNeedsAddressMessage() {
return "Current location was captured. Waka will use the GPS point for route pricing.";
}
function normalizedRouteEstimateSourceForDatabase(source) {
return String(source || "zone").toLowerCase() === "google-routes" ? "google-routes" : "zone";
}
function normalizedRouteEstimateProviderForDatabase(source, provider) {
return normalizedRouteEstimateSourceForDatabase(source) === "google-routes"
? String(provider || "google-routes").trim() || "google-routes"
: "zone";
}
function routeGuidanceConfirmedForPublish(guidance) {
if (!routeEstimatesEnabled() || !accurateRouteEstimateRequired()) return true;
return normalizedRouteEstimateSourceForDatabase(guidance?.source) === "google-routes";
}
function routeEstimateFunctionName() {
return String(appConfig.routeEstimateFunctionName || "route-estimate").trim() || "route-estimate";
}
function typedRouteAddress(text, city, country) {
return compactLocationQuery([text, city, country]);
}
function destinationRouteAddress(destination, destinationAreaName, city, country) {
const destinationText = String(destination ?? "").trim();
return destinationText.length >= 6
? typedRouteAddress(destinationText, city, country)
: compactLocationQuery([destinationText, destinationAreaName, city, country]);
}
function placesAutocompleteEnabled() {
return String(appConfig.placesAutocompleteProvider || "none").toLowerCase() === "google-places";
}
function placesAutocompleteFunctionName() {
return String(appConfig.placesAutocompleteFunctionName || "place-autocomplete").trim() || "place-autocomplete";
}
function autoPickupGpsEnabled() {
return configFlagEnabled(appConfig.autoPickupGpsEnabled);
}
function autoRiderGpsEnabled() {
return configFlagEnabled(appConfig.autoRiderGpsEnabled);
}
function 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 placeMatchesInput(place, inputValue) {
if (!place) return false;
const input = String(inputValue ?? "").trim().toLowerCase();
if (!input) return false;
return [place.displayName, place.formattedAddress].some((value) => String(value ?? "").trim().toLowerCase() === input);
}
function destinationPlaceMatchesInput(place, destination) {
return placeMatchesInput(place, destination);
}
function pickupPlaceMatchesInput(place, pickup) {
return placeMatchesInput(place, pickup);
}
function pickupPlaceForRoute(pickup = els.pickupDescription?.value) {
const place = normalizedPlaceSelection(selectedPickupPlace);
return pickupPlaceMatchesInput(place, pickup) ? place : null;
}
function currentPickupLocationLabel(point = pendingPickupGps) {
const gps = normalizeGpsPoint(point);
return gps ? "Current location" : "";
}
function pickupUsesCurrentLocationText(value) {
return /^current location\b/i.test(String(value ?? "").trim());
}
function destinationPlaceForRoute(destination = els.destination?.value) {
const place = normalizedPlaceSelection(selectedDestinationPlace);
return destinationPlaceMatchesInput(place, destination) ? place : null;
}
function routeOriginForEstimate(country, city, pickupAreaName, pickupDescription, pickupGps = pendingPickupGps) {
const pickupText = String(pickupDescription ?? "").trim();
const selectedPlace = pickupPlaceForRoute(pickupText);
const selectedGps = normalizeGpsPoint(selectedCurrentPickupGps);
const gps = normalizeGpsPoint(selectedGps && (!pickupText || pickupUsesCurrentLocationText(pickupText)) ? selectedGps : pickupGps);
if (selectedPlace) {
return {
address: selectedPlace.formattedAddress || selectedPlace.displayName || compactLocationQuery([pickupText, pickupAreaName, city, country]),
placeId: selectedPlace.placeId ?? null,
formattedAddress: selectedPlace.formattedAddress ?? null,
latitude: selectedPlace.latitude ?? null,
longitude: selectedPlace.longitude ?? null,
area: pickupAreaName,
city,
country,
source: "google-places"
};
}
if (gps && pickupUsesCurrentLocationText(pickupText)) {
return {
latitude: gps.latitude,
longitude: gps.longitude,
accuracyMeters: gps.accuracyMeters ?? null,
capturedAt: gps.capturedAt ?? null,
address: pickupText || compactLocationQuery([pickupAreaName, city, country]),
placeId: null,
formattedAddress: null,
area: pickupAreaName,
city,
country,
source: "browser-gps"
};
}
const typedAddress = pickupText.length >= 6
? typedRouteAddress(pickupText, city, country)
: compactLocationQuery([pickupText, pickupAreaName, city, country]);
if (pickupText.length >= 6) {
return {
address: typedAddress,
placeId: null,
formattedAddress: null,
latitude: null,
longitude: null,
area: pickupAreaName,
city,
country,
source: "typed-address"
};
}
if (gps) {
return {
latitude: gps.latitude,
longitude: gps.longitude,
accuracyMeters: gps.accuracyMeters ?? null,
capturedAt: gps.capturedAt ?? null,
address: typedAddress,
placeId: null,
formattedAddress: null,
area: pickupAreaName,
city,
country,
source: "browser-gps"
};
}
return {
address: typedAddress,
placeId: null,
formattedAddress: null,
latitude: null,
longitude: null,
area: pickupAreaName,
city,
country,
source: "typed-address"
};
}
function routeOriginIsSpecific(origin) {
return Boolean(origin?.placeId)
|| validGpsCoordinate(Number(origin?.latitude), Number(origin?.longitude))
|| String(origin?.address ?? "").trim().length >= 6;
}
function requestPickupGpsFromRouteOrigin(origin, fallbackGps = pendingPickupGps) {
if (validGpsCoordinate(Number(origin?.latitude), Number(origin?.longitude))) {
const fallback = normalizeGpsPoint(fallbackGps);
return normalizeGpsPoint({
latitude: Number(origin.latitude),
longitude: Number(origin.longitude),
accuracyMeters: origin.accuracyMeters ?? fallback?.accuracyMeters ?? null,
capturedAt: origin.capturedAt ?? fallback?.capturedAt ?? new Date().toISOString()
});
}
return origin?.source === "browser-gps" ? normalizeGpsPoint(fallbackGps) : null;
}
function routeEstimateRequestBody(country, city, pickupGps, destinationAreaName, destination, stops = [], pickupDescription = els.pickupDescription?.value, pickupAreaName = els.pickupArea?.value) {
const origin = routeOriginForEstimate(country, city, pickupAreaName, pickupDescription, pickupGps);
const selectedPlace = destinationPlaceForRoute(destination);
return {
origin,
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();
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 = [], pickupDescription = els.pickupDescription?.value) {
const fallback = routeEstimatesEnabled() ? null : fareGuidanceForRide(country, city, pickupAreaName, destinationAreaName, pickupGps, stops);
if (!routeEstimatesEnabled()) return fallback;
const body = routeEstimateRequestBody(country, city, pickupGps, destinationAreaName, destination, stops, pickupDescription, pickupAreaName);
if (!routeOriginIsSpecific(body.origin)) {
if (accurateRouteEstimateRequired()) throw new Error("Pickup address or pickup GPS is required before accurate route pricing.");
return null;
}
if (routeOriginNeedsConfirmedAddressForPricing(body.origin)) {
throw new Error(currentLocationNeedsAddressMessage());
}
if (!body.destination.address) {
if (accurateRouteEstimateRequired()) throw new Error("Destination address is required before accurate route pricing.");
return null;
}
try {
lastRouteEstimateError = null;
const estimate = await fetchRouteEstimateFromEdge(body);
const guidance = fareGuidanceFromRouteEstimate(estimate, stops);
if (!guidance) throw new Error("Google Routes did not return a usable driving distance.");
lastRouteEstimateError = null;
return guidance;
} catch (error) {
logClientWarning("Accurate route estimate failed.", error);
lastRouteEstimateError = error;
if (accurateRouteEstimateRequired() && !routeEstimateFallbackAllowedForTesting()) {
throw new Error("Driving distance could not be confirmed. Please try again in a moment.");
}
return null;
}
}
function validGpsCoordinate(latitude, longitude) {
return Number.isFinite(latitude)
&& Number.isFinite(longitude)
&& latitude >= -90
&& latitude <= 90
&& longitude >= -180
&& longitude <= 180;
}
function normalizeGpsPoint(value) {
if (!value) return null;
const latitude = Number(value.latitude ?? value.lat);
const longitude = Number(value.longitude ?? value.lng);
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 passengerPickupGpsReadyLabel(value) {
return normalizeGpsPoint(value) ? "Pickup GPS is ready." : "Pickup GPS is not ready.";
}
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. Keep this page open so Waka can refresh it automatically.";
}
if (pickupGps.accuracyMeters > passengerPickupGpsMaxAccuracyMeters) {
return "Pickup GPS needs a clearer signal. Waka is refreshing it automatically; move closer to the pickup point if possible.";
}
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 requestReopenedAfterRiderCancellation(request) {
return Boolean(request
&& request.status === "open"
&& request.cancelledBy
&& request.cancelledBy !== request.passengerId
&& request.releasedAt);
}
function rideStatusLabel(request) {
if (requestReopenedAfterRiderCancellation(request)) return "Reopened";
return {
open: "Open",
matched: "Matched",
arrived: "Rider arrived",
in_progress: "Ride in progress",
completed: "Completed",
cancelled: "Cancelled"
}[request?.status] ?? request?.status ?? "Unknown";
}
function reopenedRequestChip(request) {
return requestReopenedAfterRiderCancellation(request)
? "Rider cancelled; reposted to nearby riders"
: null;
}
function proximityChip(request, rider = currentRiderRecord()) {
if (activeRole() !== "rider") return null;
if (selectedRiderIdForRequest(request) === rider?.id) return `Matched to you: ${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 passengerHasTrackableRide() {
return activeRole() === "passenger"
&& hasSignedIn("passenger")
&& state.requests.some((request) => requestBelongsToPassenger(request)
&& selectedRiderIdForRequest(request)
&& ["matched", "arrived", "in_progress"].includes(request.status));
}
function stopPassengerApproachAutoRefresh() {
if (passengerApproachRefreshTimer == null) return;
window.clearInterval(passengerApproachRefreshTimer);
passengerApproachRefreshTimer = null;
}
function ensurePassengerApproachAutoRefresh() {
if (!passengerHasTrackableRide()) {
stopPassengerApproachAutoRefresh();
return;
}
if (passengerApproachRefreshTimer != null) return;
passengerApproachRefreshTimer = window.setInterval(() => {
if (!passengerHasTrackableRide()) {
stopPassengerApproachAutoRefresh();
return;
}
if (document.hidden || marketRefreshInFlight) return;
void refreshMarketplace({ silent: true });
}, passengerApproachRefreshIntervalMs);
}
function riderShouldAutoRefreshMarketplace() {
return activeRole() === "rider"
&& hasSignedIn("rider")
&& riderCanSeeRequests(currentRiderRecord());
}
function stopRiderMarketplaceAutoRefresh() {
if (riderMarketplaceRefreshTimer == null) return;
window.clearInterval(riderMarketplaceRefreshTimer);
riderMarketplaceRefreshTimer = null;
}
function ensureRiderMarketplaceAutoRefresh() {
if (!riderShouldAutoRefreshMarketplace()) {
stopRiderMarketplaceAutoRefresh();
return;
}
if (riderMarketplaceRefreshTimer != null) return;
riderMarketplaceRefreshTimer = window.setInterval(() => {
if (!riderShouldAutoRefreshMarketplace()) {
stopRiderMarketplaceAutoRefresh();
return;
}
if (document.hidden || marketRefreshInFlight) return;
void refreshMarketplace({ silent: true });
}, riderMarketplaceRefreshIntervalMs);
}
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 adminDirectoryRpcMissing(error) {
return /schema cache|Could not find the function|function .* does not exist|404/i.test(error.message ?? String(error));
}
function riderMarketplaceRpcBody(rider) {
return {
p_pickup_areas: pickupAreasWithinRiderRadius(rider),
p_limit: riderMarketplacePageSize,
p_offset: 0
};
}
async function fetchRiderMarketplaceRpcRows(rider) {
const body = riderMarketplaceRpcBody(rider);
if (!supabaseClient) {
return 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;
logClientWarning("Passenger active ride approach RPC is not installed yet. Falling back to offer distance snapshots.", error);
return false;
}
}
async function fetchActiveRideContactRows() {
if (!supabaseClient) {
return 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;
logClientWarning("Active ride contact RPC is not installed yet. Text chat still works after rider selection.", error);
return false;
}
}
function emptyMarketplaceTableResult() {
return {
data: [],
warning: null,
count: 0,
limit: 0,
offset: 0
};
}
function uniqueMarketplaceIds(values = []) {
return [...new Set(values.filter(Boolean))];
}
function currentMarketplaceUserIds() {
return uniqueMarketplaceIds([state.passenger?.id, state.rider?.id]);
}
function marketplaceTableOptions(table, filters = []) {
return {
...runtimeTableLoadOptions(table, marketplaceSyncLoadLimits),
filters: filters.filter(Boolean)
};
}
function marketplaceEqFilter(column, value) {
return value ? { column, value } : null;
}
function marketplaceInFilter(column, values = []) {
const scopedValues = uniqueMarketplaceIds(values);
return scopedValues.length ? { column, operator: "in", value: scopedValues } : null;
}
async function selectScopedMarketplaceTable(table, orderColumn, filters = []) {
const scopedFilters = filters.filter(Boolean);
if (!scopedFilters.length) return emptyMarketplaceTableResult();
return selectRuntimeTable(table, "*", orderColumn, marketplaceTableOptions(table, scopedFilters));
}
function mergeMarketplaceTableResults(results = []) {
const rowsById = new Map();
const warnings = [];
results.forEach((result) => {
if (result?.warning) warnings.push(result.warning);
(result?.data ?? []).forEach((row) => {
rowsById.set(row.id ?? JSON.stringify(row), row);
});
});
const rows = [...rowsById.values()];
return {
data: rows,
warning: warnings[0] ?? null,
count: rows.length,
limit: null,
offset: 0
};
}
async function selectAnyUserScopedMarketplaceTable(table, orderColumn, columns = []) {
const userIds = currentMarketplaceUserIds();
if (!userIds.length || !columns.length) return emptyMarketplaceTableResult();
const results = await Promise.all(columns.map((column) => (
selectScopedMarketplaceTable(table, orderColumn, [marketplaceInFilter(column, userIds)])
)));
return mergeMarketplaceTableResults(results);
}
async function 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;
logClientWarning(
areaProximityRpcMissing(error)
? "Server-side area proximity helper is not installed yet. Falling back to local distance estimates."
: "GPS/PostGIS rider marketplace RPC is not installed yet. Falling back to the GPS-gated area-distance marketplace RPC.",
error
);
}
}
if (riderCanSeeRequests(rider) && !riderMarketplaceRpcUnavailable) {
try {
const rows = await fetchRiderMarketplaceRpcRows(rider);
lastMarketplaceSyncSource = "rider_marketplace_requests RPC";
return {
data: 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;
logClientWarning(
areaProximityRpcMissing(error)
? "Server-side area proximity helper is not installed yet. Falling back to capped table reads."
: "Rider marketplace RPC is not installed yet. Falling back to capped table reads.",
error
);
}
}
lastMarketplaceSyncSource = "capped table reads";
if (riderCanSeeRequests(rider)) {
assertClientFallbackAllowed("Rider marketplace table read", "supabase-rider-marketplace-rpc.sql");
return selectScopedMarketplaceTable("ride_requests", "created_at", [
marketplaceEqFilter("status", "open"),
marketplaceEqFilter("country", rider.country),
marketplaceEqFilter("city", rider.city),
marketplaceEqFilter("vehicle_preference", rider.vehicle),
marketplaceInFilter("destination_area", riderDailyDestinationRegions(rider))
]);
}
if (state.passenger?.id) {
return selectScopedMarketplaceTable("ride_requests", "created_at", [
marketplaceEqFilter("passenger_id", state.passenger.id)
]);
}
return emptyMarketplaceTableResult();
}
async function loadMarketplaceFromSupabase() {
if (!hasSupabaseRuntime() || (!hasSignedIn("passenger") && !hasSignedIn("rider"))) return;
const userIds = currentMarketplaceUserIds();
const passengerIds = uniqueMarketplaceIds([state.passenger?.id]);
const riderIds = uniqueMarketplaceIds([state.rider?.id]);
const [requestsResult, notificationsResult, paymentAccountsResult, businessAccountsResult, riderDayPreferencesResult, rideRatingsResult, rideSettlementsResult, rideTipsResult] = await Promise.all([
selectMarketplaceRequests(),
selectScopedMarketplaceTable("admin_notifications", "created_at", [marketplaceInFilter("recipient_id", userIds)]),
selectScopedMarketplaceTable("payment_accounts", "updated_at", [marketplaceInFilter("user_id", userIds)]),
selectScopedMarketplaceTable("business_accounts", "updated_at", [marketplaceInFilter("owner_id", passengerIds)]),
selectScopedMarketplaceTable("rider_day_preferences", "updated_at", [marketplaceInFilter("rider_id", riderIds)]),
selectAnyUserScopedMarketplaceTable("ride_ratings", "created_at", ["reviewer_id", "rated_user_id"]),
selectAnyUserScopedMarketplaceTable("ride_payment_settlements", "created_at", ["passenger_id", "rider_id"]),
selectAnyUserScopedMarketplaceTable("ride_tips", "created_at", ["passenger_id", "rider_id"])
]);
const requestIds = uniqueMarketplaceIds((requestsResult.data ?? []).map((request) => request.id));
const [offersResult, chatsResult] = await Promise.all([
selectScopedMarketplaceTable("ride_offers", "created_at", [marketplaceInFilter("ride_request_id", requestIds)]),
selectScopedMarketplaceTable("ride_chats", "created_at", [marketplaceInFilter("ride_request_id", requestIds)])
]);
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 businessAccountIds = uniqueMarketplaceIds(businessAccounts.map((account) => account.id));
const businessSubscriptionsResult = await selectScopedMarketplaceTable("business_subscriptions", "updated_at", [
marketplaceInFilter("business_account_id", businessAccountIds)
]);
const businessSubscriptions = (businessSubscriptionsResult.data ?? []).map(mapBusinessSubscriptionFromDatabase);
const riderDayPreferences = (riderDayPreferencesResult.data ?? []).map((preference) => mapRiderDayPreferenceFromDatabase(preference));
const rideRatings = (rideRatingsResult.data ?? []).map((rating) => mapRideRatingFromDatabase(rating));
const rideSettlements = (rideSettlementsResult.data ?? []).map((settlement) => mapRideSettlementFromDatabase(settlement));
const rideTips = (rideTipsResult.data ?? []).map((tip) => mapRideTipFromDatabase(tip));
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
);
});
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 pendingPaymentSetupStorageKey = "waka-pending-payment-setup-v1";
const pendingPaymentSetupMaxAgeMs = 24 * 60 * 60 * 1000;
let pendingPaymentSetupPollTimer = null;
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;
}
async function refreshPaymentAccountsFromSupabase(role) {
if (!hasSupabaseRuntime()) return false;
const roles = role ? [role] : ["passenger", "rider"];
let refreshed = false;
for (const accountRole of roles) {
const account = accountRole === "passenger" ? state.passenger : currentRiderRecord();
const userId = account?.id;
if (!userId || !hasSignedIn(accountRole)) continue;
const rows = supabaseClient
? await supabaseClient
.from("payment_accounts")
.select("*")
.eq("user_id", userId)
.eq("role", accountRole)
.order("updated_at", { ascending: false })
.then(({ data, error }) => {
if (error) throw error;
return data ?? [];
})
: await supabaseRestRequest(`/rest/v1/payment_accounts?select=*&user_id=eq.${encodeURIComponent(userId)}&role=eq.${encodeURIComponent(accountRole)}&order=updated_at.desc`, {
accessToken: supabaseRestSession?.access_token
});
rows.map((row) => mapPaymentAccountFromDatabase(row, new Map([[userId, { full_name: account?.name, email: account?.email }]])))
.forEach((paymentAccount) => {
state.paymentAccounts = upsertById(
state.paymentAccounts.filter((item) => !(item.role === paymentAccount.role && item.userId === paymentAccount.userId)),
paymentAccount
);
refreshed = true;
});
}
if (refreshed) saveState();
return refreshed;
}
function paymentAccountReady(role, account) {
const userId = account?.id;
return Boolean(paymentAccountFor(role, userId)?.status === "linked");
}
function paymentAccountSummary(role, account) {
const paymentAccount = paymentAccountFor(role, account?.id);
if (!account) return "Sign in before payment setup.";
if (!paymentAccount) return "Stripe payment setup required. Waka does not collect card or bank credentials.";
const ending = paymentAccount.accountLast4 ? ` ending ${paymentAccount.accountLast4}` : "";
return `${paymentAccount.provider} ${paymentAccount.accountType}${ending} is ${paymentAccount.status}.`;
}
function paymentSetupRelaxedForTesting() {
return configFlagEnabled(appConfig.relaxPaymentSetupForTesting)
&& /\b(staging|pilot|test|preview)\b/i.test(String(appConfig.projectName || ""));
}
function paymentSetupConfirmFunctionName() {
return String(appConfig.paymentSetupConfirmFunctionName || "payment-method-setup-confirm").trim() || "payment-method-setup-confirm";
}
function paymentSetupReturnParams() {
const params = new URLSearchParams(window.location.search);
const payment = String(params.get("payment") || "").toLowerCase();
if (!payment) return null;
const path = window.location.pathname.toLowerCase();
const role = path.includes("rider") ? "rider" : "passenger";
return {
payment,
sessionId: String(params.get("session_id") || "").trim(),
role
};
}
function normalizePaymentSetupRole(role) {
return role === "rider" ? "rider" : "passenger";
}
function readPendingPaymentSetup() {
try {
const pending = JSON.parse(localStorage.getItem(pendingPaymentSetupStorageKey));
if (!pending || typeof pending !== "object") return null;
const sessionId = String(pending.sessionId || "").trim();
const role = normalizePaymentSetupRole(pending.role);
const createdAtMs = new Date(pending.createdAt || 0).getTime();
if (!sessionId || !Number.isFinite(createdAtMs) || Date.now() - createdAtMs > pendingPaymentSetupMaxAgeMs) {
localStorage.removeItem(pendingPaymentSetupStorageKey);
return null;
}
return {
payment: "success",
sessionId,
role,
pending: true
};
} catch {
try {
localStorage.removeItem(pendingPaymentSetupStorageKey);
} catch {
// Storage can be unavailable; the URL return can still finish setup.
}
return null;
}
}
function rememberPendingPaymentSetup(role, sessionId) {
const normalizedSessionId = String(sessionId || "").trim();
if (!normalizedSessionId) return;
try {
localStorage.setItem(pendingPaymentSetupStorageKey, JSON.stringify({
role: normalizePaymentSetupRole(role),
sessionId: normalizedSessionId,
createdAt: new Date().toISOString()
}));
} catch {
// If storage is blocked, Waka can still confirm while the return URL is present.
}
}
function clearPendingPaymentSetup(sessionId = "") {
try {
if (!sessionId) {
localStorage.removeItem(pendingPaymentSetupStorageKey);
stopPendingPaymentSetupPolling();
return;
}
const pending = JSON.parse(localStorage.getItem(pendingPaymentSetupStorageKey));
if (!pending || String(pending.sessionId || "") === sessionId) {
localStorage.removeItem(pendingPaymentSetupStorageKey);
stopPendingPaymentSetupPolling();
}
} catch {
// Nothing else to clear.
}
}
function stopPendingPaymentSetupPolling() {
if (!pendingPaymentSetupPollTimer) return;
window.clearInterval(pendingPaymentSetupPollTimer);
pendingPaymentSetupPollTimer = null;
}
function paymentSetupStillInProgressError(error) {
return /\bnot complete yet\b|\bdid not save a payment method\b|\bsetup intent\b/i.test(String(error?.message || error || ""));
}
function schedulePendingPaymentSetupConfirmation({ immediate = false } = {}) {
if (!readPendingPaymentSetup()) {
stopPendingPaymentSetupPolling();
return;
}
if (immediate) {
window.setTimeout(() => {
void handlePaymentSetupReturnFromLocation({ fromPendingCheck: true });
}, 0);
}
if (pendingPaymentSetupPollTimer) return;
let attempts = 0;
pendingPaymentSetupPollTimer = window.setInterval(() => {
attempts += 1;
if (!readPendingPaymentSetup() || attempts > 60) {
stopPendingPaymentSetupPolling();
return;
}
void handlePaymentSetupReturnFromLocation({ fromPendingCheck: true });
}, 5000);
}
function paymentStatusElement(role) {
return role === "rider" ? els.riderPaymentStatus : els.passengerPaymentStatus;
}
function clearPaymentSetupReturnParams() {
const params = new URLSearchParams(window.location.search);
params.delete("payment");
params.delete("session_id");
const nextQuery = params.toString();
const nextUrl = `${window.location.pathname}${nextQuery ? `?${nextQuery}` : ""}${window.location.hash || ""}`;
window.history.replaceState({}, "", nextUrl);
}
async function ensureSignedInForPaymentReturn(role) {
if (hasSignedIn(role)) return true;
const user = await getSupabaseUser().catch(() => null);
if (!user) return false;
const profile = supabaseClient
? await supabaseClient
.from("profiles")
.select("*")
.eq("id", user.id)
.maybeSingle()
.then(({ data, error }) => {
if (error) throw error;
return data;
})
: await selectProfileRest(user.id, "*", supabaseRestSession?.access_token);
if (!profile || profile.role !== role) return false;
applySignedInProfile(role, profile, user);
state.activeTab = role;
if (role === "passenger") state.passengerPage = "payment";
saveState();
return true;
}
async function confirmPaymentMethodSetup(sessionId, role) {
const token = await currentSupabaseAccessToken();
if (!token) throw new Error("Sign in to finish linking the Stripe payment method.");
const response = await withSupabaseTimeout(
fetch(`${appConfig.supabaseUrl}/functions/v1/${paymentSetupConfirmFunctionName()}`, {
method: "POST",
headers: {
apikey: appConfig.supabaseAnonKey,
authorization: `Bearer ${token}`,
"content-type": "application/json"
},
body: JSON.stringify({ sessionId, role })
}),
"Confirming Stripe payment setup",
supabaseProfileSaveTimeoutMs
);
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload?.error || "Stripe payment setup could not be confirmed.");
return payload.paymentAccount;
}
async function handlePaymentSetupReturnFromLocation({ fromPendingCheck = false } = {}) {
const locationReturn = paymentSetupReturnParams();
if (locationReturn?.payment === "success" && locationReturn.sessionId) {
rememberPendingPaymentSetup(locationReturn.role, locationReturn.sessionId);
}
const paymentReturn = locationReturn ?? readPendingPaymentSetup();
if (!paymentReturn) return false;
const { payment, sessionId, role } = paymentReturn;
const status = paymentStatusElement(role);
const signedInAccount = role === "passenger" ? state.passenger : currentRiderRecord();
if (!locationReturn && payment === "success" && paymentAccountReady(role, signedInAccount)) {
clearPendingPaymentSetup(sessionId);
if (status) status.textContent = paymentAccountSummary(role, signedInAccount);
if (role === "passenger") state.passengerPage = "request";
saveState();
renderAll();
return true;
}
state.activeTab = role;
if (role === "passenger") state.passengerPage = "payment";
if (payment === "cancelled") {
if (status) status.textContent = "Stripe card setup was cancelled. No passenger payment method was linked.";
clearPendingPaymentSetup(sessionId);
clearPaymentSetupReturnParams();
saveState();
return true;
}
if (payment !== "success") return false;
if (!await ensureSignedInForPaymentReturn(role)) {
state.accountMode[role] = "signin";
if (status) status.textContent = "Sign in to finish linking the Stripe payment method.";
if (locationReturn) clearPaymentSetupReturnParams();
saveState();
if (!fromPendingCheck) renderAll();
return true;
}
if (!sessionId) {
if (status) status.textContent = "Stripe returned without a setup session id. Open Stripe setup again so Waka can link the saved card immediately.";
saveState();
return true;
}
try {
if (status) status.textContent = "Confirming saved Stripe payment method...";
const row = await confirmPaymentMethodSetup(sessionId, role);
const account = role === "passenger" ? state.passenger : currentRiderRecord();
const savedAccount = mapPaymentAccountFromDatabase(row, new Map([[account?.id, { full_name: account?.name }]]));
state.paymentAccounts = upsertById(
state.paymentAccounts.filter((item) => !(item.role === role && item.userId === account?.id)),
savedAccount
);
await refreshPaymentAccountsFromSupabase(role);
await loadMarketplaceFromSupabase().catch((marketplaceError) => {
logClientWarning("Marketplace refresh after Stripe setup was skipped.", marketplaceError);
});
clearPendingPaymentSetup(sessionId);
clearPaymentSetupReturnParams();
if (role === "passenger") state.passengerPage = "request";
saveState();
renderAll();
const refreshedStatus = paymentStatusElement(role);
if (refreshedStatus) refreshedStatus.textContent = paymentAccountSummary(role, account);
if (role === "passenger" && els.passengerRideGate) {
els.passengerRideGate.textContent = "Passenger payment method is ready. You can request rides.";
}
return true;
} catch (error) {
await refreshPaymentAccountsFromSupabase(role).catch(() => false);
const account = role === "passenger" ? state.passenger : currentRiderRecord();
if (paymentAccountReady(role, account)) {
clearPendingPaymentSetup(sessionId);
if (role === "passenger") state.passengerPage = "request";
saveState();
renderAll();
const refreshedStatus = paymentStatusElement(role);
if (refreshedStatus) refreshedStatus.textContent = paymentAccountSummary(role, account);
return true;
}
if (paymentSetupStillInProgressError(error)) {
if (status) status.textContent = "Finish Stripe card setup in the secure tab, then return here. Waka will link the saved card automatically.";
schedulePendingPaymentSetupConfirmation();
saveState();
return true;
}
if (status) status.textContent = `Stripe payment setup could not be linked yet: ${error.message}`;
saveState();
return true;
}
}
function selectedSubscriptionPlanKey() {
const value = els.subscriptionPlan?.value || "monthly";
return riderSubscriptionPlans[value] ? value : "monthly";
}
function 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;
logClientWarning("Subscription payment submit RPC is not installed yet. Falling back to direct table insert.", error);
}
}
assertClientFallbackAllowed("Subscription payment reference submission", "supabase-subscription-payment-requests.sql");
lastSubscriptionPaymentSource = "direct payment request insert fallback";
if (!supabaseClient) {
const data = await withSupabaseTimeout(
supabaseRestRequest("/rest/v1/subscription_payment_requests", {
method: "POST",
body: payload,
headers: { Prefer: "return=representation" }
}),
"Submitting the subscription payment reference",
supabaseProfileSaveTimeoutMs
);
return mapPaymentRequestFromDatabase(Array.isArray(data) ? data[0] : data, riderMap);
}
const { data, error } = await withSupabaseTimeout(
supabaseClient
.from("subscription_payment_requests")
.insert(payload)
.select("*")
.single(),
"Submitting the subscription payment reference",
supabaseProfileSaveTimeoutMs
);
if (error) throw error;
return mapPaymentRequestFromDatabase(data, riderMap);
}
async function savePaymentAccountToSupabase(account) {
if (!hasSupabaseRuntime()) return account;
const body = {
p_role: account.role,
p_provider: account.provider,
p_account_type: account.accountType,
p_account_holder: account.accountHolder,
p_account_last4: account.accountLast4,
p_institution_name: account.institutionName,
p_provider_reference: account.reference
};
if (!paymentAccountRpcUnavailable) {
try {
const data = supabaseClient
? await withSupabaseTimeout(
supabaseClient.rpc("save_payment_account_setup", body),
"Saving the payment account",
optionalSupabaseRequestTimeoutMs
)
: await withSupabaseTimeout(
supabaseRestRequest("/rest/v1/rpc/save_payment_account_setup", {
method: "POST",
body
}),
"Saving the payment account",
optionalSupabaseRequestTimeoutMs
);
if (supabaseClient && data.error) throw data.error;
lastPaymentAccountSource = "payment account RPC";
const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data;
return row?.id ? mapPaymentAccountFromDatabase(row, new Map([[account.userId, { full_name: account.userName }]])) : account;
} catch (error) {
if (!adminDirectoryRpcMissing(error)) throw error;
paymentAccountRpcUnavailable = true;
logClientWarning("Payment account RPC is not installed yet. Falling back to direct payment account upsert.", error);
}
}
assertClientFallbackAllowed("Payment account setup", "supabase-payment-accounts.sql");
lastPaymentAccountSource = "direct payment account upsert fallback";
const payload = {
user_id: account.userId,
role: account.role,
provider: account.provider,
account_type: account.accountType,
account_holder: account.accountHolder,
account_last4: account.accountLast4,
institution_name: account.institutionName,
provider_reference: account.reference,
status: "linked",
updated_at: new Date().toISOString()
};
if (supabaseClient) {
const { data, error } = await withSupabaseTimeout(
supabaseClient
.from("payment_accounts")
.upsert(payload, { onConflict: "user_id,role" })
.select("*")
.single(),
"Saving the payment account",
optionalSupabaseRequestTimeoutMs
);
if (error) throw error;
return mapPaymentAccountFromDatabase(data, new Map([[account.userId, { full_name: account.userName }]]));
}
const data = await withSupabaseTimeout(
supabaseRestRequest("/rest/v1/payment_accounts?on_conflict=user_id,role", {
method: "POST",
body: payload,
headers: { Prefer: "resolution=merge-duplicates,return=representation" }
}),
"Saving the payment account",
optionalSupabaseRequestTimeoutMs
);
return mapPaymentAccountFromDatabase(Array.isArray(data) ? data[0] : data, new Map([[account.userId, { full_name: account.userName }]]));
}
async function saveBusinessAccountToSupabase(account) {
if (!hasSupabaseRuntime()) return account;
const body = {
p_business_name: account.businessName,
p_billing_email: account.billingEmail
};
if (!businessAccountRpcUnavailable) {
try {
const data = supabaseClient
? await withSupabaseTimeout(
supabaseClient.rpc("create_business_account", body),
"Creating the business account",
supabaseProfileSaveTimeoutMs
)
: await withSupabaseTimeout(
supabaseRestRequest("/rest/v1/rpc/create_business_account", {
method: "POST",
body,
headers: { Prefer: "return=representation" }
}),
"Creating the business account",
supabaseProfileSaveTimeoutMs
);
if (supabaseClient && data.error) throw data.error;
lastBusinessAccountSource = "business account RPC";
const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data;
return row?.id ? mapBusinessAccountFromDatabase(row, new Map([[account.ownerId, { full_name: account.ownerName }]])) : account;
} catch (error) {
if (!adminDirectoryRpcMissing(error)) throw error;
businessAccountRpcUnavailable = true;
logClientWarning("Business account 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;
logClientWarning("Rider day regions RPC is not installed yet. Falling back to direct day-region upsert.", error);
}
}
assertClientFallbackAllowed("Rider day-region setup", "supabase-rider-day-regions.sql");
lastRiderDayRegionsSource = "direct rider day-region upsert fallback";
const payload = {
rider_id: preference.riderId,
service_date: preference.serviceDate,
country: preference.country,
city: preference.city,
origin_area: preference.originArea,
destination_regions: preference.regions,
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 startPaymentMethodSetup(type) {
if (!hasSupabaseRuntime()) {
throw new Error("Stripe payment setup requires the Supabase staging or production runtime.");
}
const body = { role: type };
const token = await currentSupabaseAccessToken();
if (!token) throw new Error("Sign in before opening Stripe setup.");
const response = await withSupabaseTimeout(
fetch(`${appConfig.supabaseUrl}/functions/v1/payment-method-setup-start`, {
method: "POST",
headers: {
apikey: appConfig.supabaseAnonKey,
authorization: `Bearer ${token}`,
"content-type": "application/json"
},
body: JSON.stringify(body)
}),
"Starting Stripe payment setup",
supabaseProfileSaveTimeoutMs
);
const responsePayload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(responsePayload?.error || "Stripe payment setup Edge Function failed.");
if (!responsePayload?.url) throw new Error("Stripe did not return a hosted setup URL.");
return responsePayload;
}
async function startPassengerPaymentSetup(event) {
event.preventDefault();
if (!state.passenger || !hasSignedIn("passenger")) {
els.passengerPaymentStatus.textContent = "Sign in as a passenger before opening Stripe setup.";
return;
}
let setupWindow = null;
try {
setupWindow = window.open("", "wakaStripeCardSetup");
if (setupWindow) {
setupWindow.document.title = "Opening Stripe";
setupWindow.document.body.innerHTML = 'Opening secure Stripe setup Keep Waka open in the original tab. This window will continue to Stripe.
';
}
} catch {
setupWindow = null;
}
try {
setButtonBusy(els.startPassengerPaymentSetup, true);
els.passengerPaymentStatus.textContent = "Opening secure Stripe card setup...";
const checkout = await startPaymentMethodSetup("passenger");
rememberPendingPaymentSetup("passenger", checkout.providerReference);
schedulePendingPaymentSetupConfirmation();
if (setupWindow && !setupWindow.closed) {
els.passengerPaymentStatus.textContent = "Stripe card setup opened in a separate tab. Finish there, then return here; Waka will link the saved card automatically.";
setupWindow.location.href = checkout.url;
setupWindow.focus();
} else {
els.passengerPaymentStatus.textContent = "Popup was blocked. Redirecting this tab to secure Stripe card setup...";
window.location.assign(checkout.url);
}
} catch (error) {
if (setupWindow && !setupWindow.closed) setupWindow.close();
if (paymentSetupRelaxedForTesting()) {
try {
els.passengerPaymentStatus.textContent = `Stripe setup could not open: ${error.message}. Staging is linking a test payment method...`;
const account = state.passenger;
const stagingAccount = {
id: paymentAccountFor("passenger", account.id)?.id ?? makeId("payacct"),
userId: account.id,
userName: account.name,
role: "passenger",
provider: "stripe-test",
accountType: "test_card",
accountHolder: account.name,
accountLast4: "4242",
institutionName: "Stripe test mode",
reference: `staging-test-${account.id}`,
status: "linked",
createdAt: paymentAccountFor("passenger", account.id)?.createdAt ?? new Date().toISOString(),
updatedAt: new Date().toISOString()
};
let savedAccount = stagingAccount;
let localOnly = false;
try {
savedAccount = await savePaymentAccountToSupabase(stagingAccount);
} catch (saveError) {
localOnly = true;
logClientWarning("Staging test payment method could not be saved to Supabase; keeping it local for pilot testing.", saveError);
}
state.paymentAccounts = upsertById(
state.paymentAccounts.filter((item) => !(item.role === "passenger" && item.userId === account.id)),
savedAccount
);
state.passengerPage = "payment";
saveState();
renderAll();
if (els.passengerPaymentStatus) {
els.passengerPaymentStatus.textContent = localOnly
? `Staging test payment method is linked on this device because Stripe setup returned: ${error.message}`
: `Staging test payment method is linked because Stripe setup returned: ${error.message}`;
}
if (els.passengerRideGate) {
els.passengerRideGate.textContent = "Passenger payment method is ready for staging. Pickup GPS updates automatically.";
}
if (els.passengerSessionSummary) {
els.passengerSessionSummary.textContent = `${account.email ?? account.phone} - ${account.city}, ${account.country}. You can request rides.`;
}
return;
} catch (fallbackError) {
els.passengerPaymentStatus.textContent = `Stripe setup failed, and the staging test setup could not be linked: ${fallbackError.message}`;
return;
}
}
els.passengerPaymentStatus.textContent = `Could not open Stripe setup: ${error.message}`;
} finally {
setButtonBusy(els.startPassengerPaymentSetup, false);
}
}
async function startBusinessSubscriptionCheckout(accountId) {
const account = passengerBusinessAccounts().find((item) => item.id === accountId);
if (!account) {
els.businessAccountStatus.textContent = "Business account was not found.";
return;
}
try {
els.businessAccountStatus.textContent = `Opening 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: normalizedRouteEstimateSourceForDatabase(request.routeEstimateSource),
route_estimate_provider: normalizedRouteEstimateProviderForDatabase(request.routeEstimateSource, request.routeEstimateProvider),
route_estimate_cached: Boolean(request.routeEstimateCached),
route_estimate_key: request.routeEstimateKey ?? null,
route_estimate_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: normalizedRouteEstimateSourceForDatabase(request.routeEstimateSource),
p_route_estimate_provider: normalizedRouteEstimateProviderForDatabase(request.routeEstimateSource, request.routeEstimateProvider),
p_route_estimate_cached: Boolean(request.routeEstimateCached),
p_route_estimate_key: request.routeEstimateKey ?? null,
p_route_estimate_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;
logClientWarning("Ride request RPC is not installed yet. Falling back to direct table insert.", error);
}
}
assertClientFallbackAllowed("Ride request publishing", "supabase-ride-request-rpc.sql");
lastRidePostSource = "direct ride request insert fallback";
if (!supabaseClient) {
const data = await withSupabaseTimeout(
supabaseRestRequest("/rest/v1/ride_requests", {
method: "POST",
body: requestPayloadForSupabase(request),
headers: { Prefer: "return=representation" }
}),
"Publishing the ride request",
supabaseProfileSaveTimeoutMs
);
return mapRideRequestFromDatabase(Array.isArray(data) ? data[0] : data);
}
const { data, error } = await withSupabaseTimeout(
supabaseClient
.from("ride_requests")
.insert(requestPayloadForSupabase(request))
.select("*")
.single(),
"Publishing the ride request",
supabaseProfileSaveTimeoutMs
);
if (error) throw error;
return mapRideRequestFromDatabase(data);
}
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;
logClientWarning("Passenger fare RPC is not installed yet. Fare updates need server-side request checks.", error);
throw new Error("Run supabase-marketplace-actions-rpc.sql before fare updates in Supabase mode.");
}
}
if (marketplaceActionRpcUnavailable.fare) {
throw new Error("Run supabase-marketplace-actions-rpc.sql before fare updates in Supabase mode.");
}
}
async function saveOfferToSupabase(offer) {
if (!hasSupabaseRuntime()) return offer;
if (!marketplaceActionRpcUnavailable.offer) {
try {
const body = {
p_ride_request_id: offer.requestId,
p_fare_xaf: offer.fare,
p_type: offer.type,
p_public_note: offer.note || null
};
const data = supabaseClient
? await withSupabaseTimeout(
supabaseClient.rpc("rider_save_offer", body),
"Saving the rider offer",
supabaseProfileSaveTimeoutMs
)
: await withSupabaseTimeout(
supabaseRestRequest("/rest/v1/rpc/rider_save_offer", {
method: "POST",
body
}),
"Saving the rider offer",
supabaseProfileSaveTimeoutMs
);
if (supabaseClient && data.error) throw data.error;
lastMarketplaceActionSource = "marketplace action RPC";
const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data;
if (row?.id) return mapOfferFromDatabase(row);
throw new Error("Rider offer RPC did not return a saved offer.");
} catch (error) {
if (areaProximityRpcMissing(error)) areaProximityRpcUnavailable = true;
if (!adminDirectoryRpcMissing(error)) throw error;
marketplaceActionRpcUnavailable.offer = true;
logClientWarning(
areaProximityRpcMissing(error)
? "Server-side area proximity helper is not installed yet. Rider offers need server-side proximity checks."
: "Rider offer RPC is not installed yet. Offer submission needs server-side proximity checks.",
error
);
throw new Error(areaProximityRpcMissing(error)
? "Run supabase-area-proximity.sql and supabase-marketplace-actions-rpc.sql before rider offers in Supabase mode."
: "Run supabase-marketplace-actions-rpc.sql before rider offers in Supabase mode.");
}
}
if (marketplaceActionRpcUnavailable.offer) {
throw new Error("Run supabase-marketplace-actions-rpc.sql before rider offers in Supabase mode.");
}
}
async function chooseOfferInSupabase(request, offer) {
if (!hasSupabaseRuntime()) return;
if (!marketplaceActionRpcUnavailable.selection) {
try {
const body = { p_offer_id: offer.id };
const data = supabaseClient
? await withSupabaseTimeout(
supabaseClient.rpc("passenger_select_rider_offer", body),
"Choosing the rider offer",
supabaseProfileSaveTimeoutMs
)
: await withSupabaseTimeout(
supabaseRestRequest("/rest/v1/rpc/passenger_select_rider_offer", {
method: "POST",
body
}),
"Choosing the rider offer",
supabaseProfileSaveTimeoutMs
);
if (supabaseClient && data.error) throw data.error;
lastMarketplaceActionSource = "marketplace action RPC";
const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data;
if (row?.id) return mapRideRequestFromDatabase(row, new Map(), new Map([[offer.id, offer]]));
return null;
} catch (error) {
if (!adminDirectoryRpcMissing(error)) throw error;
marketplaceActionRpcUnavailable.selection = true;
logClientWarning("Passenger rider-selection RPC is not installed yet. Rider selection needs server-side proximity checks.", error);
throw new Error("Run supabase-marketplace-actions-rpc.sql before choosing riders in Supabase mode.");
}
}
if (marketplaceActionRpcUnavailable.selection) {
throw new Error("Run supabase-marketplace-actions-rpc.sql before choosing riders in Supabase mode.");
}
}
function currentActorIdForChat() {
if (activeRole() === "passenger") return state.sessions.passenger?.userId ?? state.passenger?.id;
if (activeRole() === "rider") return state.sessions.rider?.userId ?? state.rider?.id;
return state.adminSession?.userId ?? null;
}
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;
logClientWarning("Ride chat RPC is not installed yet. Falling back to direct chat insert.", error);
}
}
const payload = {
ride_request_id: message.requestId,
sender_id: senderId,
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) {
logClientWarning("Chat message was not synced to Supabase.", error);
}
}
function contactRelayFunctionName() {
return String(appConfig.contactRelayFunctionName || "ride-contact-relay").trim() || "ride-contact-relay";
}
async function relayRideChatMessageToPhone(message) {
if (!hasSupabaseRuntime() || message.sender === "system") return;
const token = await currentSupabaseAccessToken();
if (!token) return;
try {
const response = await withSupabaseTimeout(
fetch(`${appConfig.supabaseUrl}/functions/v1/${contactRelayFunctionName()}`, {
method: "POST",
headers: {
apikey: appConfig.supabaseAnonKey,
authorization: `Bearer ${token}`,
"content-type": "application/json"
},
body: JSON.stringify({
channel: "sms",
rideRequestId: message.requestId,
message: message.text
})
}),
"Sending Waka SMS relay",
optionalSupabaseRequestTimeoutMs
);
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload?.error || "Waka SMS relay failed.");
if (els.chatStatus) els.chatStatus.textContent = "Open - SMS relay sent";
} catch (error) {
if (els.chatStatus) {
els.chatStatus.textContent = /not configured|missing|provider/i.test(String(error.message))
? "Open - in-app sent; SMS relay not configured"
: "Open - in-app sent; SMS relay failed";
}
logClientWarning("Waka SMS relay was not sent.", error);
}
}
async function saveSafetyReportToSupabase(report) {
if (!hasSupabaseRuntime()) return report;
const payload = {
ride_request_id: report.requestId,
reporter_id: report.reporterId,
reporter_role: report.reporterRole,
reported_user_id: report.reportedUserId,
category: report.category,
severity: report.severity,
details: report.details,
status: "open"
};
const preserveDisplayFields = (savedReport) => ({
...savedReport,
reporterName: report.reporterName ?? savedReport.reporterName,
reportedUserName: report.reportedUserName ?? savedReport.reportedUserName,
routeSummary: report.routeSummary ?? savedReport.routeSummary
});
if (!safetyReportRpcUnavailable.submit) {
try {
const rpcBody = {
p_ride_request_id: report.requestId,
p_reported_user_id: report.reportedUserId,
p_category: report.category,
p_severity: report.severity,
p_details: report.details
};
const data = supabaseClient
? await withSupabaseTimeout(
supabaseClient.rpc("submit_safety_report", rpcBody),
"Submitting the safety report",
optionalSupabaseRequestTimeoutMs
)
: await withSupabaseTimeout(
supabaseRestRequest("/rest/v1/rpc/submit_safety_report", {
method: "POST",
body: rpcBody
}),
"Submitting the safety report",
optionalSupabaseRequestTimeoutMs
);
if (supabaseClient && data.error) throw data.error;
lastSafetyReportSource = "safety report RPC";
const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data;
if (row?.id) return preserveDisplayFields(mapSafetyReportFromDatabase(row));
} catch (error) {
if (!adminDirectoryRpcMissing(error)) throw error;
safetyReportRpcUnavailable.submit = true;
logClientWarning("Safety report submit RPC is not installed yet. Falling back to direct table insert.", error);
}
}
assertClientFallbackAllowed("Safety report submission", "supabase-safety-reports.sql");
lastSafetyReportSource = "direct safety report insert fallback";
if (!supabaseClient) {
const data = await withSupabaseTimeout(
supabaseRestRequest("/rest/v1/safety_reports", {
method: "POST",
body: payload,
headers: { Prefer: "return=representation" }
}),
"Submitting the safety report",
optionalSupabaseRequestTimeoutMs
);
return preserveDisplayFields(mapSafetyReportFromDatabase(Array.isArray(data) ? data[0] : data));
}
const { data, error } = await withSupabaseTimeout(
supabaseClient
.from("safety_reports")
.insert(payload)
.select("*")
.single(),
"Submitting the safety report",
optionalSupabaseRequestTimeoutMs
);
if (error) throw error;
return preserveDisplayFields(mapSafetyReportFromDatabase(data));
}
async function 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)) {
logClientWarning("Ride rating RPC is not installed yet. Ratings need server-side completed-ride and counterparty checks.", error);
throw new Error("Run supabase-ride-ratings.sql before ride ratings in Supabase mode.");
}
throw error;
}
}
async function saveRideTipToSupabase(tip) {
if (!hasSupabaseRuntime()) return tip;
const rpcBody = {
p_ride_request_id: tip.requestId,
p_tip_amount_cents: dollarsToCents(tip.amount)
};
try {
const data = supabaseClient
? await withSupabaseTimeout(
supabaseClient.rpc("passenger_tip_rider", rpcBody),
"Submitting the rider tip",
optionalSupabaseRequestTimeoutMs
)
: await withSupabaseTimeout(
supabaseRestRequest("/rest/v1/rpc/passenger_tip_rider", {
method: "POST",
body: rpcBody
}),
"Submitting the rider tip",
optionalSupabaseRequestTimeoutMs
);
if (supabaseClient && data.error) throw data.error;
const row = Array.isArray(data.data ?? data) ? (data.data ?? data)[0] : data.data ?? data;
if (row?.id) return mapRideTipFromDatabase(row);
throw new Error("Passenger tip RPC did not return a saved tip.");
} catch (error) {
if (adminDirectoryRpcMissing(error)) {
logClientWarning("Ride tip RPC is not installed yet. Tips need server-side completion and payout checks.", error);
throw new Error("Run supabase-ride-lifecycle.sql before passenger tips in Supabase mode.");
}
throw error;
}
}
async function updateRideDestinationInSupabase(request, nextDestination, guidance, nextStops = request.rideStops) {
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(nextStops),
p_estimated_distance_miles: guidance?.distanceMiles ?? request.estimatedDistanceMiles ?? null,
p_estimated_travel_minutes: guidance?.minutes ?? request.estimatedTravelMinutes ?? null,
p_route_estimate_source: normalizedRouteEstimateSourceForDatabase(guidance?.source ?? request.routeEstimateSource),
p_route_estimate_provider: normalizedRouteEstimateProviderForDatabase(guidance?.source ?? request.routeEstimateSource, guidance?.provider ?? request.routeEstimateProvider),
p_route_estimate_cached: Boolean(guidance?.cached ?? request.routeEstimateCached),
p_route_estimate_key: guidance?.routeKey ?? request.routeEstimateKey ?? null,
p_route_estimate_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 stopsInput = form.querySelector(".stops-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;
}
const nextStops = normalizeRideStops(stopsInput?.value ?? request.rideStops);
let guidance = fareGuidanceForRide(
request.country,
request.city,
request.pickupArea,
request.destinationArea,
requestPickupGps(request),
nextStops
);
try {
if (routeEstimatesEnabled()) {
status.textContent = "Checking updated driving distance...";
guidance = await accurateFareGuidanceForRide(
request.country,
request.city,
request.pickupArea,
request.destinationArea,
nextDestination,
requestPickupGps(request),
nextStops
);
}
status.textContent = "Updating destination...";
const saved = await updateRideDestinationInSupabase(request, nextDestination, guidance, nextStops);
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 ?? nextStops),
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}${nextStops.length ? ` with ${nextStops.length} stop${nextStops.length === 1 ? "" : "s"}` : ""}.`);
saveState();
renderAll();
} catch (error) {
status.textContent = `Destination update failed: ${/edge function|non-2xx|failed|timeout|network|unavailable/i.test(String(error.message)) ? "route distance is temporarily unavailable. Try again in a moment." : error.message}`;
}
}
function canCancelBeforeStart(request) {
if (!request || !preStartCancellationStatuses.includes(request.status)) return false;
if (activeRole() === "passenger") return requestBelongsToPassenger(request);
return activeRole() === "rider" && 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 requestReopenedAfterRiderCancellation(request)
? "The matched rider cancelled before pickup. This request is open again and visible to nearby riders."
: "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. In-app chat can relay SMS through Waka once the phone provider is configured; masked calling needs an active Waka relay number.";
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);
if (type === "accepted") {
state.requests = state.requests.map((item) => {
if (item.id !== request.id) return item;
return {
...item,
status: "matched",
selectedOfferId: savedOffer.id,
agreedFare: savedOffer.fare,
selectedRiderId: savedOffer.riderId,
selectedRiderName: firstNameOnly(rider?.name, "Rider"),
riderConfirmationStatus: isScheduledRequest(item) ? "not_requested" : null,
riderConfirmationRequestedAt: null,
riderConfirmedAt: null,
releasedAt: null,
matchedAt: new Date().toISOString()
};
});
state.selectedRequestId = request.id;
pushSystemChat(
request.id,
`Rider accepted the passenger fare of ${formatMoney(savedOffer.fare)}. Ride matched automatically.`
);
}
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);
void relayRideChatMessageToPhone(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);
if (!signedIn) panel.hidden = true;
if (signedIn && type !== "passenger") panel.hidden = false;
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);
if (!passengerSignedIn) {
els.businessAccountForm.hidden = true;
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 = automaticRideBillingAccountId() ?? els.rideBillingAccount.value;
els.rideBillingAccount.innerHTML = "";
const personal = document.createElement("option");
personal.value = "";
personal.textContent = "Personal ride";
els.rideBillingAccount.append(personal);
passengerBusinessAccounts().forEach((account) => {
const option = document.createElement("option");
option.value = account.id;
option.textContent = businessAccountCanRequest(account)
? `Business: ${account.businessName}`
: `Business pending: ${account.businessName}`;
option.disabled = !businessAccountCanRequest(account);
option.selected = selected === account.id && !option.disabled;
els.rideBillingAccount.append(option);
});
if (selected && [...els.rideBillingAccount.options].some((option) => option.value === selected && !option.disabled)) {
els.rideBillingAccount.value = selected;
} else {
els.rideBillingAccount.value = "";
}
}
function automaticRideBillingAccountId() {
if (!passengerBusinessWorkspaceEnabled()) return null;
return passengerBusinessAccounts().find((account) => businessAccountCanRequest(account))?.id ?? null;
}
const accountModeSelectedThisSession = {
passenger: false,
rider: false
};
const passengerWorkspacePages = ["request", "trips", "payment", "business", "profile", "notices"];
function passengerBusinessWorkspaceEnabled(passenger = state.passenger) {
return Boolean(passenger?.accountUse === "business" || passengerBusinessAccounts(passenger).length);
}
function availablePassengerWorkspacePages() {
return passengerWorkspacePages.filter((page) => page !== "business" || passengerBusinessWorkspaceEnabled());
}
function passengerWorkspacePage() {
const pages = availablePassengerWorkspacePages();
return pages.includes(state.passengerPage) ? state.passengerPage : "request";
}
function setPassengerWorkspacePage(page) {
if (!availablePassengerWorkspacePages().includes(page)) return;
state.passengerPage = page;
saveState();
renderAll();
}
function updatePassengerInitialBusinessFields() {
if (!els.passengerInitialBusinessFields || !els.passengerAccountUse) return;
const wantsBusiness = els.passengerAccountUse.value === "business";
els.passengerInitialBusinessFields.hidden = !wantsBusiness;
const personalIdentityFields = document.querySelector("#passengerPersonalIdentityFields");
if (personalIdentityFields) personalIdentityFields.hidden = wantsBusiness;
[els.passengerNationalId, els.passengerDob].forEach((input) => {
if (!input) return;
input.required = !wantsBusiness;
if (wantsBusiness) input.value = "";
});
}
function renderPassengerWorkspacePages(passengerSignedIn) {
const page = passengerWorkspacePage();
if (els.passengerWorkspaceNav) {
els.passengerWorkspaceNav.hidden = !passengerSignedIn;
els.passengerWorkspaceNav.querySelectorAll("[data-passenger-page]").forEach((button) => {
const available = availablePassengerWorkspacePages().includes(button.dataset.passengerPage);
button.hidden = !available;
const active = button.dataset.passengerPage === page;
button.classList.toggle("active", active);
button.setAttribute("aria-pressed", String(active));
});
}
document.querySelectorAll("[data-passenger-page-section]").forEach((section) => {
section.hidden = !passengerSignedIn || section.dataset.passengerPageSection !== page;
});
if (!passengerSignedIn) return;
const paymentReady = paymentAccountReady("passenger", state.passenger);
if (els.passengerRideGate) {
els.passengerRideGate.textContent = paymentReady
? "Ready to publish. Pickup GPS updates automatically."
: "Add a passenger payment method under Payment before publishing.";
}
}
function ensureAccountChoiceState(type, signedIn) {
if (signedIn) return;
if (requestedTabFromLocation() !== type) return;
if (accountModeSelectedThisSession[type]) return;
state.accountMode[type] = "choice";
}
function renderAccountWorkspaces() {
const passengerSignedIn = Boolean(state.sessions.passenger && state.passenger);
ensureAccountChoiceState("passenger", passengerSignedIn);
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;
renderPassengerWorkspacePages(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 passenger payment method before requesting rides."}`;
els.passengerPaymentStatus.textContent = paymentAccountSummary("passenger", state.passenger);
}
renderBusinessAccountPanel();
updateBusinessBillingOptions();
renderAccountNotices("passenger");
const riderSignedIn = Boolean(state.sessions.rider && state.rider);
ensureAccountChoiceState("rider", riderSignedIn);
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();
updateAccountPhoneVerificationControls();
}
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;
accountModeSelectedThisSession[type] = true;
state.accountMode[type] = mode === "create" ? "create" : "signin";
saveState();
renderAll();
}
function accountMode(type) {
const mode = state.accountMode?.[type];
return mode === "signin" || mode === "create" ? mode : "choice";
}
function resetAccountChoice(type) {
if (!["passenger", "rider"].includes(type)) return;
accountModeSelectedThisSession[type] = false;
state.accountMode[type] = "choice";
}
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, resetAccountMode = false } = options;
state.activeTab = tab;
if (resetAccountMode) resetAccountChoice(tab);
if (!preserveEntry) state.showRoleEntry = false;
renderEntryExperience();
document.querySelectorAll(".tab-button").forEach((button) => {
button.classList.toggle("active", button.dataset.tab === tab);
});
document.querySelectorAll(".tab-panel").forEach((panel) => {
panel.classList.toggle("active", panel.id === `${tab}-panel`);
});
if (updateUrl) updateWorkspaceHash(tab);
saveState();
renderAll();
}
function 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");
const passengerTripsPage = passengerWorkspacePage() === "trips";
if (els.seedDemo) els.seedDemo.hidden = !demoToolsAllowed();
if (els.clearDemo) els.clearDemo.hidden = !demoToolsAllowed();
els.marketPanel.hidden = (role === "passenger" && (!passengerSignedIn || !passengerTripsPage))
|| (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 = role === "passenger" || !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 your live GPS in ${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.pickupDescription || 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 = [
activeRole() === "rider" ? paymentLabel(item.paymentPreference) : null,
`${offersForRequest(item.id).length} offers`,
rideStatusLabel(item),
reopenedRequestChip(item),
riderApproachChip(item),
proximityChip(item),
pickupGpsQualityChip(item),
confirmationChip(item),
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 riderApproachTrackPercent(model) {
if (!model || !Number.isFinite(Number(model.distanceKm))) return 8;
const distanceKm = Math.max(0, Number(model.distanceKm));
const rangeKm = 5;
return Math.max(8, Math.min(92, Math.round((1 - Math.min(distanceKm, rangeKm) / rangeKm) * 84 + 8)));
}
function renderPassengerApproachTracker(node, request, model) {
if (!selectedRiderIdForRequest(request)) return;
const tracker = document.createElement("div");
tracker.className = "approach-tracker";
const track = document.createElement("div");
track.className = "approach-track";
track.setAttribute("aria-hidden", "true");
const riderDot = document.createElement("span");
riderDot.className = "approach-dot rider-dot";
riderDot.style.left = `${riderApproachTrackPercent(model)}%`;
const pickupDot = document.createElement("span");
pickupDot.className = "approach-dot pickup-dot";
const labelRow = document.createElement("div");
labelRow.className = "approach-labels";
const riderLabel = document.createElement("span");
riderLabel.textContent = selectedRiderFirstNameForRequest(request);
const pickupLabel = document.createElement("span");
pickupLabel.textContent = "Pickup";
labelRow.append(riderLabel, pickupLabel);
const meta = document.createElement("small");
if (model) {
const updated = model.capturedAt ? `Updated ${formatGpsAgeLabel({ capturedAt: model.capturedAt })}.` : "Refreshes automatically.";
const accuracy = model.accuracyMeters != null ? ` Rider GPS accuracy about ${model.accuracyMeters} m.` : "";
meta.textContent = `${formatDistanceKm(model.distanceKm)} away, ${formatPickupEta(model.etaMinutes)}. ${model.isLive ? "Live rider GPS." : model.source + "."} ${updated}${accuracy}`;
} else {
meta.textContent = "Waiting for the rider's live GPS update.";
}
track.append(riderDot, pickupDot);
tracker.append(track, labelRow, meta);
node.append(tracker);
}
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.pickupDescription || 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.pickupDescription || 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);
if (activeRole() === "passenger" && requestBelongsToPassenger(request) && selectedRiderIdForRequest(request)) {
renderPassengerApproachTracker(node, request, riderApproachModel(request));
}
}
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 = `
Destination address
+
Add stop
Update destination
${windowText}
`;
form.querySelector(".add-stop-toggle").addEventListener("click", () => {
const stopField = form.querySelector(".destination-stop-field");
stopField.hidden = !stopField.hidden;
});
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 contacting Waka."
: activeRole() === "rider"
? "Contact Waka opens after the passenger chooses your offer."
: "Contact Waka opens after choosing a rider.";
const target = canReport ? reportTargetForRequest(request) : null;
const reporterRole = activeRole() === "rider" ? "Rider" : "Passenger";
els.safetyReportForm.hidden = !canReport;
els.safetyReportCategory.disabled = !canReport;
els.safetyReportSeverity.disabled = !canReport;
els.safetyReportDetails.disabled = !canReport;
els.safetyReportForm.querySelector("button").disabled = !canReport;
els.safetyReportStatus.textContent = canReport
? `${reporterRole} report about ${target.name}. Admin reviews safety, no-show, route, payment, identity, and behavior concerns. Use this to contact Waka for support too.`
: disabledReason;
}
function renderRideRatingForm(request) {
if (!els.rideRatingForm) return;
const target = ratingTargetForRequest(request);
const canRate = canRateRequest(request);
els.rideRatingForm.hidden = !canRate && !existingRatingForRequest(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 ? "Updating pickup GPS..." : "Updating pickup GPS...";
}
passengerPickupGpsPromise = getCurrentGpsPoint();
pendingPickupGps = await passengerPickupGpsPromise;
const qualityIssue = pickupGpsQualityIssue(pendingPickupGps);
if (els.pickupGpsStatus) {
els.pickupGpsStatus.textContent = qualityIssue
? qualityIssue
: passengerPickupGpsReadyLabel(pendingPickupGps);
}
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;
selectedCurrentPickupGps = null;
if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = "Pickup GPS cleared.";
updateFareGuidance();
}
function stopAutomaticPassengerPickupGps() {
if (passengerPickupGpsWatchId != null && navigator.geolocation?.clearWatch) {
navigator.geolocation.clearWatch(passengerPickupGpsWatchId);
}
passengerPickupGpsWatchId = null;
}
function ensurePassengerPickupGpsAutoCapture() {
if (!passengerPickupAutoReady()) {
stopAutomaticPassengerPickupGps();
return;
}
if (!navigator.geolocation) {
if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = "GPS is not available in this browser.";
return;
}
if (passengerPickupGpsWatchId != null) return;
if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = "Starting automatic pickup GPS...";
passengerPickupGpsWatchId = navigator.geolocation.watchPosition(
(position) => {
const point = gpsPointFromPosition(position);
if (!point) return;
pendingPickupGps = point;
const qualityIssue = pickupGpsQualityIssue(point);
if (els.pickupGpsStatus) {
els.pickupGpsStatus.textContent = qualityIssue
? qualityIssue
: passengerPickupGpsReadyLabel(point);
}
updateFareGuidance();
},
() => {
if (els.pickupGpsStatus) els.pickupGpsStatus.textContent = "GPS permission was denied or the pickup location could not be refreshed automatically.";
},
{
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 5000
}
);
if (!pendingPickupGps || pickupGpsQualityIssue(pendingPickupGps)) {
void capturePassengerPickupGps({ automatic: true });
}
}
function destinationAutocompleteReady() {
return placesAutocompleteEnabled()
&& hasSupabaseRuntime()
&& Boolean(state.passenger)
&& hasSignedIn("passenger");
}
function pickupAutocompleteReady() {
return destinationAutocompleteReady();
}
function pickupSessionToken() {
if (!pickupAutocompleteSessionToken) {
pickupAutocompleteSessionToken = makeId("place-session");
}
return pickupAutocompleteSessionToken;
}
function destinationSessionToken() {
if (!destinationAutocompleteSessionToken) {
destinationAutocompleteSessionToken = makeId("place-session");
}
return destinationAutocompleteSessionToken;
}
function clearPickupPlaceSelection(message = "") {
selectedPickupPlace = null;
if (!pickupUsesCurrentLocationText(els.pickupDescription?.value)) selectedCurrentPickupGps = null;
if (els.pickupPlaceStatus && message) els.pickupPlaceStatus.textContent = message;
}
function clearDestinationPlaceSelection(message = "") {
selectedDestinationPlace = null;
if (els.destinationPlaceStatus && message) els.destinationPlaceStatus.textContent = message;
}
function passengerFacingPlaceErrorMessage(error, fallback) {
const message = String(error?.message || "").trim();
if (!message) return fallback;
if (/edge function returned a non-2xx status code/i.test(message)) return fallback;
return message
.replace(/destination searches/gi, "address searches")
.replace(/destination search/gi, "address search");
}
function hideDestinationSuggestions() {
if (!els.destinationSuggestions) return;
els.destinationSuggestions.hidden = true;
els.destinationSuggestions.replaceChildren();
}
function hidePickupSuggestions() {
if (!els.pickupSuggestions) return;
els.pickupSuggestions.hidden = true;
els.pickupSuggestions.replaceChildren();
}
async function fetchPlaceAutocomplete(body) {
if (!destinationAutocompleteReady()) throw new Error("Destination autocomplete needs passenger sign-in and Supabase.");
const action = String(body?.action || "").toLowerCase();
const cacheKey = action === "autocomplete"
? JSON.stringify({
action,
input: String(body.input || "").trim().toLowerCase(),
country: String(body.country || "").trim().toLowerCase(),
city: String(body.city || "").trim().toLowerCase()
})
: "";
if (cacheKey && placeAutocompleteCache.has(cacheKey)) {
return placeAutocompleteCache.get(cacheKey);
}
if (action === "autocomplete" && Date.now() < placesAutocompleteRateLimitedUntil) {
throw new Error("Address search is temporarily paused for this account. Enter the full address manually.");
}
const functionName = placesAutocompleteFunctionName();
const token = await currentSupabaseAccessToken();
if (!token) throw new Error("Passenger sign-in is required for destination suggestions.");
const response = await withSupabaseTimeout(
fetch(`${appConfig.supabaseUrl}/functions/v1/${functionName}`, {
method: "POST",
headers: {
apikey: appConfig.supabaseAnonKey,
authorization: `Bearer ${token}`,
"content-type": "application/json"
},
body: JSON.stringify(body)
}),
"Fetching destination suggestions",
optionalSupabaseRequestTimeoutMs
);
const payload = await response.json().catch(() => null);
if (!response.ok) {
const message = payload?.error || "Address autocomplete failed.";
if (/too many|limit reached|rate limit/i.test(message)) {
placesAutocompleteRateLimitedUntil = Date.now() + 30 * 1000;
}
throw new Error(message);
}
if (cacheKey) {
placeAutocompleteCache.set(cacheKey, payload);
while (placeAutocompleteCache.size > 80) {
const oldestKey = placeAutocompleteCache.keys().next().value;
if (!oldestKey) break;
placeAutocompleteCache.delete(oldestKey);
}
}
return payload;
}
function destinationPlaceDetailsCacheKey(placeId) {
return String(placeId ?? "").trim();
}
function rememberDestinationPlaceDetails(placeId, payload) {
const key = destinationPlaceDetailsCacheKey(placeId);
if (!key || !payload) return;
if (destinationPlaceDetailsCache.has(key)) destinationPlaceDetailsCache.delete(key);
destinationPlaceDetailsCache.set(key, payload);
while (destinationPlaceDetailsCache.size > placeDetailsCacheLimit) {
const oldestKey = destinationPlaceDetailsCache.keys().next().value;
if (!oldestKey) break;
destinationPlaceDetailsCache.delete(oldestKey);
}
}
async function fetchDestinationPlaceDetails(suggestion) {
const placeId = destinationPlaceDetailsCacheKey(suggestion?.placeId);
if (!placeId) throw new Error("Selected destination did not include a place id.");
const cached = destinationPlaceDetailsCache.get(placeId);
if (cached) return cached;
const payload = await fetchPlaceAutocomplete({
action: "details",
placeId,
sessionToken: destinationSessionToken()
});
rememberDestinationPlaceDetails(placeId, payload);
return payload;
}
async function fetchPickupPlaceDetails(suggestion) {
const placeId = destinationPlaceDetailsCacheKey(suggestion?.placeId);
if (!placeId) throw new Error("Selected pickup did not include a place id.");
const cached = destinationPlaceDetailsCache.get(placeId);
if (cached) return cached;
const payload = await fetchPlaceAutocomplete({
action: "details",
placeId,
sessionToken: pickupSessionToken()
});
rememberDestinationPlaceDetails(placeId, payload);
return payload;
}
async function fetchPickupReverseGeocode(point) {
const gps = normalizeGpsPoint(point);
if (!gps) throw new Error("A valid GPS location is required.");
return fetchPlaceAutocomplete({
action: "reverse-geocode",
latitude: gps.latitude,
longitude: gps.longitude,
country: state.passenger?.country ?? selectedPassengerCountry(),
city: state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(selectedPassengerCountry())
});
}
function renderPickupSuggestions(suggestions = []) {
if (!els.pickupSuggestions) return;
els.pickupSuggestions.replaceChildren();
const cleanSuggestions = suggestions.filter((suggestion) => suggestion?.placeId && suggestion?.text);
if (!cleanSuggestions.length) {
els.pickupSuggestions.hidden = true;
return;
}
for (const suggestion of cleanSuggestions) {
const button = document.createElement("button");
button.type = "button";
button.className = "place-suggestion";
button.setAttribute("role", "option");
const main = document.createElement("span");
main.className = "place-suggestion-main";
main.textContent = suggestion.mainText || suggestion.text;
const secondary = document.createElement("span");
secondary.className = "place-suggestion-secondary";
secondary.textContent = suggestion.secondaryText || "";
button.append(main, secondary);
wirePlaceSuggestionButton(button, () => selectPickupSuggestion(suggestion));
els.pickupSuggestions.append(button);
}
els.pickupSuggestions.hidden = false;
}
function renderDestinationSuggestions(suggestions = []) {
if (!els.destinationSuggestions) return;
els.destinationSuggestions.replaceChildren();
const cleanSuggestions = suggestions.filter((suggestion) => suggestion?.placeId && suggestion?.text);
if (!cleanSuggestions.length) {
els.destinationSuggestions.hidden = true;
return;
}
for (const suggestion of cleanSuggestions) {
const button = document.createElement("button");
button.type = "button";
button.className = "place-suggestion";
button.setAttribute("role", "option");
const main = document.createElement("span");
main.className = "place-suggestion-main";
main.textContent = suggestion.mainText || suggestion.text;
const secondary = document.createElement("span");
secondary.className = "place-suggestion-secondary";
secondary.textContent = suggestion.secondaryText || "";
button.append(main, secondary);
wirePlaceSuggestionButton(button, () => selectDestinationSuggestion(suggestion));
els.destinationSuggestions.append(button);
}
els.destinationSuggestions.hidden = false;
}
function wirePlaceSuggestionButton(button, handler) {
let handled = false;
const choose = (event) => {
event.preventDefault();
event.stopPropagation();
if (handled) return;
handled = true;
handler();
};
button.addEventListener("pointerdown", choose);
button.addEventListener("mousedown", choose);
button.addEventListener("click", choose);
}
function handlePickupInput() {
clearLowFareReview();
if (!pickupPlaceMatchesInput(selectedPickupPlace, els.pickupDescription.value)) {
clearPickupPlaceSelection(pickupAutocompleteReady()
? "Choose a suggested pickup address if it appears, or allow automatic GPS."
: "Pickup will use automatic GPS when available.");
}
updateFareGuidance();
schedulePickupAutocomplete();
}
function handleDestinationInput() {
clearLowFareReview();
if (!destinationPlaceMatchesInput(selectedDestinationPlace, els.destination.value)) {
clearDestinationPlaceSelection(destinationAutocompleteReady()
? "Choose a suggested address if it appears."
: "Destination text will be routed as typed.");
}
updateFareGuidance();
scheduleDestinationAutocomplete();
}
function schedulePickupAutocomplete() {
window.clearTimeout(pickupAutocompleteTimer);
if (!pickupAutocompleteReady()) {
hidePickupSuggestions();
if (els.pickupPlaceStatus && placesAutocompleteEnabled()) {
els.pickupPlaceStatus.textContent = "Sign in as a passenger to use pickup suggestions.";
}
return;
}
const input = els.pickupDescription.value.trim();
if (input.length < 3) {
hidePickupSuggestions();
if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "Type at least 3 characters to search pickup addresses.";
return;
}
const requestId = ++pickupAutocompleteRequestId;
pickupAutocompleteTimer = window.setTimeout(async () => {
try {
if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "Searching pickup addresses...";
const payload = await fetchPlaceAutocomplete({
action: "autocomplete",
input,
sessionToken: pickupSessionToken(),
country: state.passenger?.country ?? selectedPassengerCountry(),
city: state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(selectedPassengerCountry())
});
if (requestId !== pickupAutocompleteRequestId) return;
renderPickupSuggestions(payload?.suggestions ?? []);
if (els.pickupPlaceStatus) {
els.pickupPlaceStatus.textContent = (payload?.suggestions ?? []).length
? "Choose the matching pickup from the suggestions."
: "No pickup suggestion found; Waka can still use GPS or the full typed address.";
}
} catch (error) {
hidePickupSuggestions();
if (els.pickupPlaceStatus) {
els.pickupPlaceStatus.textContent = passengerFacingPlaceErrorMessage(
error,
"Address suggestions are unavailable right now. Enter the full pickup address."
);
}
}
}, 350);
}
function scheduleDestinationAutocomplete() {
window.clearTimeout(destinationAutocompleteTimer);
if (!destinationAutocompleteReady()) {
hideDestinationSuggestions();
if (els.destinationPlaceStatus && placesAutocompleteEnabled()) {
els.destinationPlaceStatus.textContent = "Sign in as a passenger to use place suggestions.";
}
return;
}
const input = els.destination.value.trim();
if (input.length < 3) {
hideDestinationSuggestions();
if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = "Type at least 3 characters to search addresses.";
return;
}
const requestId = ++destinationAutocompleteRequestId;
destinationAutocompleteTimer = window.setTimeout(async () => {
try {
if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = "Searching destination addresses...";
const payload = await fetchPlaceAutocomplete({
action: "autocomplete",
input,
sessionToken: destinationSessionToken(),
country: state.passenger?.country ?? selectedPassengerCountry(),
city: state.passenger?.city ?? els.passengerCity?.value ?? defaultLaunchCity(selectedPassengerCountry())
});
if (requestId !== destinationAutocompleteRequestId) return;
renderDestinationSuggestions(payload?.suggestions ?? []);
if (els.destinationPlaceStatus) {
els.destinationPlaceStatus.textContent = (payload?.suggestions ?? []).length
? "Choose the matching destination from the suggestions."
: "No address suggestion found; continue with the full address.";
}
} catch (error) {
hideDestinationSuggestions();
if (els.destinationPlaceStatus) {
els.destinationPlaceStatus.textContent = passengerFacingPlaceErrorMessage(
error,
"Address suggestions are unavailable right now. Enter the full destination address."
);
}
}
}, 350);
}
async function selectPickupSuggestion(suggestion) {
try {
clearLowFareReview();
selectedCurrentPickupGps = null;
if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "Confirming pickup place...";
const payload = await fetchPickupPlaceDetails(suggestion);
const place = normalizedPlaceSelection({
...payload?.place,
displayName: payload?.place?.displayName || suggestion.mainText || suggestion.text
});
if (!place?.placeId) throw new Error("Selected pickup did not return a place id.");
selectedPickupPlace = place;
els.pickupDescription.value = place.formattedAddress || place.displayName || suggestion.text;
hidePickupSuggestions();
pickupAutocompleteSessionToken = null;
if (els.pickupPlaceStatus) {
els.pickupPlaceStatus.textContent = `Selected: ${place.displayName || place.formattedAddress}`;
}
updateFareGuidance();
} catch (error) {
if (els.pickupPlaceStatus) {
els.pickupPlaceStatus.textContent = passengerFacingPlaceErrorMessage(
error,
"Could not confirm that pickup suggestion. Enter the full pickup address."
);
}
}
}
async function useCurrentPickupLocation() {
clearLowFareReview();
if (!window.isSecureContext) {
if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "Current location requires a secure HTTPS page.";
return;
}
if (!navigator.geolocation) {
if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "This browser does not support current location. Type the pickup address instead.";
return;
}
const existingPoint = pendingPickupGps ? pendingPickupGps : null;
if (existingPoint) {
applyCurrentPickupPoint(existingPoint, "Current location selected. Looking up the nearest pickup address...");
} else {
els.pickupDescription.value = "Capturing current location...";
if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "Capturing your current location. Approve the browser location prompt if it appears.";
}
setButtonBusy(els.useCurrentPickup, true);
const point = existingPoint ?? await capturePassengerPickupGps({ automatic: false });
setButtonBusy(els.useCurrentPickup, false);
if (!point) {
if (els.pickupDescription.value === "Capturing current location...") els.pickupDescription.value = "";
if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "Current location could not be captured. In Chrome, allow Location for this site or type the pickup address.";
return;
}
applyCurrentPickupPoint(point, "Current location selected. Looking up the nearest pickup address...");
try {
const payload = await fetchPickupReverseGeocode(point);
const place = normalizedPlaceSelection(payload?.place);
if (!place?.formattedAddress) throw new Error("No street address was found for this GPS point.");
selectedPickupPlace = {
...place,
placeId: null,
latitude: point.latitude,
longitude: point.longitude
};
els.pickupDescription.value = place.formattedAddress;
if (els.pickupPlaceStatus) {
els.pickupPlaceStatus.textContent = `Using current location: ${place.displayName || place.formattedAddress}. Riders will see the verified pickup point.`;
}
} catch (error) {
if (els.pickupPlaceStatus) {
els.pickupPlaceStatus.textContent = `Current location is selected. Street address lookup did not finish: ${passengerFacingPlaceErrorMessage(error, "address lookup unavailable")}. Fare and publishing will use the GPS point.`;
}
}
updateFareGuidance();
}
function pickupFieldCanUseGpsAutofill() {
const value = els.pickupDescription?.value.trim() ?? "";
return !value || pickupUsesCurrentLocationText(value) || value === "Capturing current location...";
}
function applyCurrentPickupPoint(point, statusText = "") {
const gps = normalizeGpsPoint(point);
if (!gps || !els.pickupDescription) return false;
selectedPickupPlace = null;
selectedCurrentPickupGps = gps;
els.pickupDescription.value = currentPickupLocationLabel(gps);
hidePickupSuggestions();
if (statusText && els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = statusText;
updateFareGuidance();
return true;
}
function activateUseCurrentPickup(event) {
event?.preventDefault?.();
if (useCurrentPickupActivationInFlight) return;
useCurrentPickupActivationInFlight = true;
Promise.resolve(useCurrentPickupLocation()).finally(() => {
window.setTimeout(() => {
useCurrentPickupActivationInFlight = false;
}, 0);
});
}
async function selectDestinationSuggestion(suggestion) {
try {
clearLowFareReview();
if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = "Confirming destination place...";
const payload = await fetchDestinationPlaceDetails(suggestion);
const place = normalizedPlaceSelection({
...payload?.place,
displayName: payload?.place?.displayName || suggestion.mainText || suggestion.text
});
if (!place?.placeId) throw new Error("Selected destination did not return a place id.");
selectedDestinationPlace = place;
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 = passengerFacingPlaceErrorMessage(
error,
"Could not confirm that address suggestion. Enter the full destination address."
);
}
}
}
function currentFareReviewKey(guidance, fareOffer) {
return JSON.stringify({
pickup: els.pickupDescription?.value.trim() ?? "",
destination: els.destination?.value.trim() ?? "",
fareOffer: Number(fareOffer) || 0,
min: guidance?.min ?? null,
max: guidance?.max ?? null,
distanceMiles: guidance?.distanceMiles ? Number(guidance.distanceMiles).toFixed(1) : null,
minutes: guidance?.minutes ?? null
});
}
function clearLowFareReview() {
pendingLowFareOverrideKey = "";
if (!els.fareReviewPanel) return;
els.fareReviewPanel.hidden = true;
els.fareReviewPanel.replaceChildren();
}
function showLowFareReview(guidance, fareOffer) {
if (!els.fareReviewPanel || !guidance) return;
const reviewKey = currentFareReviewKey(guidance, fareOffer);
els.fareReviewPanel.hidden = false;
els.fareReviewPanel.replaceChildren();
const message = document.createElement("div");
message.textContent = `Your $${fareOffer} offer is below the suggested $${guidance.min}-$${guidance.max} range for this route. You can adjust it or publish anyway.`;
const actions = document.createElement("div");
actions.className = "fare-review-actions";
const useMinimum = document.createElement("button");
useMinimum.type = "button";
useMinimum.className = "secondary-action";
useMinimum.textContent = `Use $${guidance.min}`;
useMinimum.addEventListener("click", () => {
els.fareOffer.value = String(guidance.min);
clearLowFareReview();
updateFareGuidance();
els.fareOffer.focus();
});
const publishAnyway = document.createElement("button");
publishAnyway.type = "button";
publishAnyway.className = "ghost-action";
publishAnyway.textContent = "Publish anyway";
publishAnyway.addEventListener("click", () => {
pendingLowFareOverrideKey = reviewKey;
els.rideRequestForm.requestSubmit();
});
actions.append(useMinimum, publishAnyway);
els.fareReviewPanel.append(message, actions);
els.fareReviewPanel.scrollIntoView({ behavior: "smooth", block: "center" });
}
function passengerRidePublishedMessage(request) {
const fare = formatMoney(request.fareOffer, request.country);
return `Ride request published: ${request.pickupDescription || request.pickupArea} to ${requestDestinationText(request)} for ${fare}. Status: Open for nearby riders.`;
}
function addPassengerRideNotice(title, body, requestId) {
if (!state.passenger?.id) return;
state.notifications = upsertById(state.notifications, {
id: makeId("notice"),
recipientId: state.passenger.id,
recipientRole: "passenger",
title,
body,
requestId,
createdAt: new Date().toISOString(),
readAt: null
});
}
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 {
const subdivision = locationSubdivisionLabel(country);
els.passengerLocationStatus.textContent = `Updating passenger ${subdivision}...`;
await updatePassengerCurrentCityInSupabase(state.passenger.supabaseUserId ?? state.passenger.id, country, city);
state.passenger = { ...state.passenger, country, city };
state.passengers = upsertById(state.passengers, state.passenger);
clearSelectedRequestOutsideLocation(country, city);
clearPassengerPickupGps();
saveState();
populateLocationFields();
hydrateForms();
renderAll();
void refreshMarketplace({ silent: true });
els.passengerLocationStatus.textContent = `Ride requests now publish in ${city} ${subdivision}, ${country}.`;
} catch (error) {
els.passengerLocationStatus.textContent = error.message;
}
}
function initialBusinessAccountValues() {
const wantsBusiness = els.passengerAccountUse?.value === "business";
return {
wantsBusiness,
businessName: els.passengerInitialBusinessName?.value.trim() ?? "",
billingEmail: els.passengerInitialBusinessBillingEmail?.value.trim().toLowerCase() ?? ""
};
}
function validBusinessAccountFields(values) {
if (!values.wantsBusiness) return true;
return values.businessName.length >= 2 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.billingEmail);
}
async function createInitialBusinessAccountForPassenger(values) {
if (!values.wantsBusiness || !state.passenger) return null;
const localAccount = {
id: makeId("business"),
ownerId: state.passenger.id,
ownerName: state.passenger.name,
businessName: values.businessName,
billingEmail: values.billingEmail,
status: hasSupabaseRuntime() ? "pending" : "active",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
const savedAccount = await saveBusinessAccountToSupabase(localAccount);
state.businessAccounts = upsertById(state.businessAccounts, savedAccount);
return savedAccount;
}
async function createPassenger(event) {
event.preventDefault();
updatePassengerInitialBusinessFields();
setTranslatedStatus(els.passengerStatus, "checkingPassengerAccount");
const phone = els.passengerPhone.value.trim();
const profilePhotoName = els.passengerPhoto.files[0]?.name ?? state.passenger?.profilePhotoName ?? "";
const businessValues = initialBusinessAccountValues();
const dateOfBirth = businessValues.wantsBusiness ? null : normalizeDateOfBirthInput(els.passengerDob);
const email = els.passengerEmail.value.trim().toLowerCase();
if (!validateAccountForm(els.passengerAccountForm, els.passengerStatus)) return;
if (!validBusinessAccountFields(businessValues)) {
els.passengerStatus.textContent = "Enter a business name and valid billing email, or choose Personal rides.";
return;
}
if (!businessValues.wantsBusiness && !validDateOfBirth(dateOfBirth)) {
setTranslatedStatus(els.passengerStatus, "validDateOfBirthRequired");
return;
}
if (hasSupabaseRuntime()) {
try {
const availability = await profileContactAvailability(email, phone, state.passenger?.id ?? null);
if (!availability.emailAvailable || !availability.phoneAvailable) {
els.passengerStatus.textContent = !availability.emailAvailable
? "An account already exists with this email address. Sign in with that account instead of creating a duplicate."
: "An account already exists with this phone number. Sign in with that account instead of creating a duplicate.";
return;
}
} catch (error) {
logClientWarning("Profile contact availability check was skipped.", error);
}
}
if (!(await ensureVerifiedPhoneForAccount("passenger", phone, els.passengerStatus))) return;
const passenger = {
id: state.passenger?.id ?? makeId("passenger"),
name: els.passengerName.value.trim(),
email,
password: els.passengerPassword.value,
phone,
phoneVerified: true,
phoneVerifiedAt: state.verification.passenger?.verifiedAt ?? state.passenger?.phoneVerifiedAt ?? new Date().toISOString(),
phoneVerificationProvider: state.verification.passenger?.provider ?? "manual-pilot",
accountUse: businessValues.wantsBusiness ? "business" : "personal",
nationalId: businessValues.wantsBusiness ? "" : els.passengerNationalId.value.trim(),
dateOfBirth,
preferredLanguage: state.language,
country: els.passengerCountry.value,
city: els.passengerCity.value,
profilePhotoName,
profilePhotoPath: state.passenger?.profilePhotoPath ?? null,
createdAt: state.passenger?.createdAt ?? new Date().toISOString()
};
try {
setButtonBusy(els.passengerSaveButton, true);
const setPassengerStage = (message) => {
els.passengerStatus.textContent = message;
};
setTranslatedStatus(els.passengerStatus, isSupabaseMode() ? "startingPassengerSupabase" : "savingPassenger");
const user = await saveProfileToSupabase({ ...passenger, role: "passenger" }, setPassengerStage, { waitForProfile: true, preventExistingAccount: 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";
let businessCreated = null;
try {
businessCreated = await createInitialBusinessAccountForPassenger(businessValues);
} catch (error) {
els.passengerStatus.textContent = `Passenger account was created, but the business account was not created: ${error.message}`;
}
state.passengerPage = businessCreated ? "business" : "request";
saveState();
renderAll();
const passengerCreatedKey = user?.emailSetupPending ? "passengerCreatedEmailPending" : "passengerCreated";
setTranslatedStatus(els.passengerStatus, passengerCreatedKey, { name: state.passenger.name });
setTranslatedStatus(els.passengerSessionSummary, passengerCreatedKey, { name: state.passenger.name });
if (businessCreated) els.businessAccountStatus.textContent = businessAccountSummary(businessCreated);
} catch (error) {
setTranslatedStatus(els.passengerStatus, "passengerAccountFailed", { message: passengerAccountErrorMessage(error) });
} finally {
setButtonBusy(els.passengerSaveButton, false);
}
}
async function createRideRequest(event) {
event.preventDefault();
if (!state.passenger) {
translatedAlert("passengerAccountRequired");
return;
}
if (!hasSignedIn("passenger")) {
translatedAlert("passengerSignInRequired");
return;
}
if (!state.passenger.phoneVerified) {
translatedAlert("passengerPhoneRequired");
return;
}
if (!paymentAccountReady("passenger", state.passenger) && hasSupabaseRuntime()) {
await refreshPaymentAccountsFromSupabase("passenger");
await loadMarketplaceFromSupabase().catch((error) => {
logClientWarning("Marketplace refresh before ride publish was skipped.", error);
});
}
if (!paymentAccountReady("passenger", state.passenger)) {
state.passengerPage = "payment";
saveState();
renderAll();
translatedAlert("passengerPaymentRequired");
return;
}
const 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();
const enteredPickupDescription = els.pickupDescription.value.trim();
const pickupDescription = enteredPickupDescription || currentPickupLocationLabel(pendingPickupGps);
const pickupOrigin = routeOriginForEstimate(
state.passenger.country,
state.passenger.city,
els.pickupArea.value,
pickupDescription,
pendingPickupGps
);
if (!routeOriginIsSpecific(pickupOrigin)) {
if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "Use current location or enter a complete pickup address before publishing.";
return;
}
if (!enteredPickupDescription && pickupOrigin.source === "browser-gps" && els.pickupDescription) {
els.pickupDescription.value = pickupDescription;
if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "Using current location as the pickup point.";
}
const requestPickupGps = requestPickupGpsFromRouteOrigin(pickupOrigin, pendingPickupGps);
if (pickupOrigin.source === "browser-gps") {
const pickupGpsIssue = pickupGpsQualityIssue(requestPickupGps);
if (pickupGpsIssue) {
els.pickupGpsStatus.textContent = pickupGpsIssue;
return;
}
}
let guidance = updateFareGuidance();
const previewGuidance = guidance;
if (routeEstimatesEnabled()) {
if (els.fareGuidance) els.fareGuidance.textContent = "Checking accurate driving distance before publishing...";
try {
const accurateGuidance = await accurateFareGuidanceForRide(
state.passenger.country,
state.passenger.city,
els.pickupArea.value,
els.destinationArea.value,
els.destination.value.trim(),
pendingPickupGps,
els.rideStops.value,
pickupDescription
);
guidance = accurateGuidance ?? previewGuidance;
if (els.fareGuidance) {
els.fareGuidance.textContent = guidance ? fareGuidanceMessage(guidance) : routeEstimateErrorMessage(lastRouteEstimateError);
}
} catch (error) {
if (els.fareGuidance) els.fareGuidance.textContent = routeEstimateErrorMessage(error);
translatedAlert("publishRideFailed", { message: error.message });
return;
}
if (!routeGuidanceConfirmedForPublish(guidance)) {
const message = routeEstimateErrorMessage(lastRouteEstimateError);
if (els.fareGuidance) els.fareGuidance.textContent = message;
translatedAlert("publishRideFailed", { message });
return;
}
}
if (guidance && fareOffer < guidance.min) {
const reviewKey = currentFareReviewKey(guidance, fareOffer);
if (pendingLowFareOverrideKey !== reviewKey) {
showLowFareReview(guidance, fareOffer);
return;
}
pendingLowFareOverrideKey = "";
}
const paymentPreference = validPaymentPreferenceForCountry(els.paymentPreference.value || "online_card", state.passenger.country);
els.paymentPreference.value = paymentPreference;
const rideStops = normalizeRideStops(els.rideStops.value);
const destinationPlace = destinationPlaceForRoute(els.destination.value.trim());
const businessAccountId = automaticRideBillingAccountId();
if (els.rideBillingAccount) els.rideBillingAccount.value = businessAccountId ?? "";
const businessAccount = businessAccountId ? passengerBusinessAccounts().find((account) => account.id === businessAccountId) : null;
if (businessAccountId && !businessAccountCanRequest(businessAccount)) {
translatedAlert("publishRideFailed", { message: "Business 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,
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: normalizedRouteEstimateSourceForDatabase(guidance?.source),
routeEstimateProvider: normalizedRouteEstimateProviderForDatabase(guidance?.source, guidance?.provider),
routeEstimateCached: Boolean(guidance?.cached),
routeEstimateKey: guidance?.routeKey ?? null,
routeEstimateCreatedAt: guidance?.estimatedAt ?? null,
paymentPreference,
pickupGps: requestPickupGps,
pickupLatitude: requestPickupGps?.latitude ?? null,
pickupLongitude: requestPickupGps?.longitude ?? null,
pickupGpsAccuracyMeters: requestPickupGps?.accuracyMeters ?? null,
pickupGpsCapturedAt: requestPickupGps?.capturedAt ?? null,
rideTiming,
scheduledAt: scheduledDate?.toISOString() ?? null,
riderConfirmationStatus: null,
riderConfirmationRequestedAt: null,
riderConfirmedAt: null,
releasedAt: null,
status: "open",
selectedOfferId: null,
createdAt: new Date().toISOString()
};
try {
const savedRequest = await saveRideRequestToSupabase(request);
state.requests.unshift(savedRequest);
state.selectedRequestId = savedRequest.id;
state.passengerPage = "trips";
addPassengerRideNotice("Ride request published", passengerRidePublishedMessage(savedRequest), savedRequest.id);
pendingPickupGps = null;
selectedPickupPlace = null;
selectedDestinationPlace = null;
hidePickupSuggestions();
hideDestinationSuggestions();
els.pickupGpsStatus.textContent = "Automatic GPS is starting.";
if (els.pickupPlaceStatus) els.pickupPlaceStatus.textContent = "Start typing a pickup address or allow automatic GPS.";
if (els.destinationPlaceStatus) els.destinationPlaceStatus.textContent = "Start typing a destination address.";
els.rideRequestForm.reset();
populateLocationFields();
saveState();
renderAll();
void refreshMarketplace({ silent: true });
const publishedMessage = passengerRidePublishedMessage(savedRequest);
els.passengerSessionSummary.textContent = publishedMessage;
els.selectedSummary.textContent = publishedMessage;
} catch (error) {
translatedAlert("publishRideFailed", { message: error.message });
}
}
async function signOutRole(type) {
if (type === "passenger") {
stopAutomaticPassengerPickupGps();
stopPassengerApproachAutoRefresh();
pendingPickupGps = null;
selectedPickupPlace = null;
}
if (type === "rider") {
stopAutomaticRiderGps();
const rider = currentRiderRecord();
if (riderCurrentGps(rider)) {
try {
await clearRiderLiveGpsInSupabase(clearRiderLiveGpsFields(rider));
} catch (error) {
logClientWarning("Could not clear rider live GPS before sign-out.", error);
}
}
}
if (isSupabaseMode()) {
await clearStaleSupabaseSession();
}
supabaseRestSession = null;
updateConnectionStatus();
state.sessions[type] = null;
state.accountMode[type] = "choice";
if (type === "passenger") {
state.passenger = null;
state.selectedRequestId = null;
pendingPickupGps = null;
selectedPickupPlace = null;
selectedDestinationPlace = null;
hidePickupSuggestions();
hideDestinationSuggestions();
els.passengerSignInPassword.value = "";
setTranslatedStatus(els.passengerSignInStatus, "signedOut");
}
if (type === "rider") {
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",
vehicleInspection: "Vehicle inspection document"
};
function emptyRiderDocuments() {
return {
driverLicense: "",
vehicleRegistration: "",
insurance: "",
vehicleInspection: ""
};
}
function requiredRiderDocuments() {
return ["driverLicense", "vehicleRegistration", "insurance"];
}
function parseRiderDocuments(value) {
const documents = emptyRiderDocuments();
if (!value) return documents;
if (typeof value === "object") {
return { ...documents, ...value };
}
const text = String(value).trim();
if (!text) return documents;
try {
const parsed = JSON.parse(text);
if (parsed && typeof parsed === "object") return { ...documents, ...parsed };
} catch {
return { ...documents, driverLicense: text };
}
return { ...documents, driverLicense: text };
}
function riderDocuments(rider) {
const documents = {
...emptyRiderDocuments(),
...parseRiderDocuments(rider?.documentName),
...parseRiderDocuments(rider?.documents)
};
if (rider?.driverLicenseDocumentName) documents.driverLicense = rider.driverLicenseDocumentName;
if (rider?.vehicleRegistrationDocumentName) documents.vehicleRegistration = rider.vehicleRegistrationDocumentName;
if (rider?.insuranceDocumentName) documents.insurance = rider.insuranceDocumentName;
if (rider?.vehicleInspectionDocumentName) documents.vehicleInspection = rider.vehicleInspectionDocumentName;
if (rider?.driverLicenseDocumentPath) documents.driverLicense = rider.driverLicenseDocumentPath;
if (rider?.vehicleRegistrationDocumentPath) documents.vehicleRegistration = rider.vehicleRegistrationDocumentPath;
if (rider?.insuranceDocumentPath) documents.insurance = rider.insuranceDocumentPath;
if (rider?.vehicleInspectionDocumentPath) documents.vehicleInspection = rider.vehicleInspectionDocumentPath;
return documents;
}
function selectedRiderDocumentFiles() {
return {
driverLicense: els.riderLicenseDocument.files[0] ?? null,
vehicleRegistration: els.riderRegistrationDocument.files[0] ?? null,
insurance: els.riderInsuranceDocument.files[0] ?? null,
vehicleInspection: els.riderInspectionDocument.files[0] ?? null
};
}
function riderDocumentPayload(documents) {
return JSON.stringify({ ...emptyRiderDocuments(), ...documents });
}
function missingRiderDocumentLabels(documents) {
return requiredRiderDocuments()
.filter((key) => !documents[key])
.map((key) => riderDocumentLabels[key]);
}
function riderDocumentSummary(rider) {
const documents = riderDocuments(rider);
const requiredSummary = requiredRiderDocuments()
.map((key) => `${riderDocumentLabels[key]}: ${documents[key] || "missing"}`);
const optionalSummary = documents.vehicleInspection
? [`${riderDocumentLabels.vehicleInspection}: ${documents.vehicleInspection}`]
: ["Vehicle inspection: optional"];
return [...requiredSummary, ...optionalSummary].join(". ");
}
function 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 live GPS and today's destination regions.";
const regions = riderDailyDestinationRegions(rider);
const destinations = regions.length ? ` Today's destinations: ${regions.join(", ")}.` : " Choose today's destination regions.";
return `Car requests use your live GPS within about ${riderServiceRadius(rider)} km in ${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,
vehicleInspectionDocumentName: documentNames.vehicleInspection,
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");
if (hasSupabaseRuntime()) {
try {
const availability = await profileContactAvailability(rider.email, rider.phone, state.rider?.id ?? null);
if (!availability.emailAvailable || !availability.phoneAvailable) {
els.riderStatus.textContent = !availability.emailAvailable
? "An account already exists with this email address. Sign in with that account instead of creating a duplicate."
: "An account already exists with this phone number. Sign in with that account instead of creating a duplicate.";
return;
}
} catch (error) {
logClientWarning("Profile contact availability check was skipped.", error);
}
}
const user = await saveProfileToSupabase({ ...rider, role: "rider" }, setRiderStage, { waitForProfile: true, preventExistingAccount: 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,
vehicleInspectionDocumentPath: savedDocuments.vehicleInspection,
supabaseUserId: user?.id ?? null
};
state.sessions.rider = {
phone: state.rider.phone,
email: state.rider.email,
userId: state.rider.supabaseUserId,
signedInAt: new Date().toISOString()
};
els.riderPassword.value = "";
els.riderPhoto.value = "";
els.riderLicenseDocument.value = "";
els.riderRegistrationDocument.value = "";
els.riderInsuranceDocument.value = "";
els.riderInspectionDocument.value = "";
state.riders = state.riders.filter((item) => item.id !== rider.id && item.id !== user?.id);
state.riders.unshift(state.rider);
state.accountMode.rider = "signin";
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 ensurePassengerApproachAutoRefresh === "function") ensurePassengerApproachAutoRefresh();
if (typeof ensureRiderMarketplaceAutoRefresh === "function") ensureRiderMarketplaceAutoRefresh();
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, { resetAccountMode: true }));
});
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("[data-passenger-page]").forEach((button) => {
button.addEventListener("click", () => setPassengerWorkspacePage(button.dataset.passengerPage));
});
document.querySelectorAll(".filter-button").forEach((button) => {
button.addEventListener("click", () => {
state.filter = button.dataset.filter;
document.querySelectorAll(".filter-button").forEach((item) => item.classList.toggle("active", item === button));
saveState();
renderAll();
});
});
wireDateOfBirthInput(els.passengerDob);
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.passengerAccountUse?.addEventListener("change", updatePassengerInitialBusinessFields);
els.passengerPaymentForm.addEventListener("submit", startPassengerPaymentSetup);
els.startPassengerPaymentSetup?.addEventListener("click", startPassengerPaymentSetup);
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();
const requestedTab = requestedTabFromLocation();
state.activeTab = availableWorkspaceTab(requestedTab ?? preferredSignedInTab() ?? state.activeTab) ?? defaultRuntimeTab();
populateLocationFields();
hydrateForms();
const showEntryOnBoot = shouldShowRoleEntry();
switchTab(state.activeTab, { updateUrl: !showEntryOnBoot, preserveEntry: showEntryOnBoot, resetAccountMode: Boolean(requestedTab) });
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;
});