Double wallet credit
One recharge, credited twice. The driver withdraws money that never came in.
Proposal demo · Native iOS + PHP/MySQL + Wave Mobile Money
A paid-first flow for your three iOS apps. The server verifies every payment before a ride or delivery moves, and every duplicate stops at the gate.
01 / Risk
Payment providers retry. Networks hiccup. Users tap twice. Without a gate, each copy is real money leaving the business.
One recharge, credited twice. The driver withdraws money that never came in.
A driver gets sent before the payment is confirmed. Then the payment fails.
The same notification lands ten times and ten drivers start driving.
02 / Flow
The iOS app asks our server to create the checkout. No keys in the app.
POST /api/checkout → launch_url
The rider confirms in Wave. The order sits in pending. Nothing is dispatched.
orders.status = 'pending'
Wave calls the backend. We read the raw body before touching JSON.
php://input
Rebuild the HMAC, compare with hash_equals, reject anything old or forged.
401 if mismatch
Never trust the message alone. Confirm status, amount, currency and our reference.
payment_status = succeeded
One transaction, one row lock, one conditional update. Then the driver moves.
affected_rows = 1 → go
03 / Signature
Real HMAC-SHA256, computed in your browser. Change one character in the payload, or replay an old one, and send it.
waiting
Send the payload to see what the server decides.
// webhook.php $raw = file_get_contents('php://input'); [$t, $v1] = parseSig($_SERVER['HTTP_WAVE_SIGNATURE']); $calc = hash_hmac('sha256', $t . $raw, $secret); if (!hash_equals($calc, $v1) || abs(time() - (int)$t) > 300) { http_response_code(401); exit; }
04 / The race
// press fire. same event id, ten times.
05 / Why only one
A repeat insert hits the key and stops before any logic runs.
SELECT ... FOR UPDATE makes parallel requests wait in line.
Only pending → paid. Zero rows changed means already done.
-- L1: duplicates die here INSERT INTO webhook_events (event_id, payload) VALUES (?, ?); -- UNIQUE(event_id) START TRANSACTION; -- L2: one at a time SELECT status FROM orders WHERE id = ? FOR UPDATE; -- L3: only the first one wins UPDATE orders SET status = 'paid' WHERE id = ? AND status = 'pending'; -- affected_rows = 0 -> stop, no dispatch COMMIT;
06 / Wallet
Wallet top-ups never share a code path with ride payments. Every credit is a ledger row with a unique reference, and the balance moves in the same transaction.
Order saved as pending. No driver yet.
Signed webhook in. HMAC checked. The API is asked again.
Same event id. The unique key says no. They get a 200 and nothing else.
affected_rows = 1. One dispatch, sent once.
Ledger exact. The admin panel shows every attempt.
Replay
07 / Your three apps
Current Xcode and iOS compatibility, dependency fixes, then the payment states wired into the screens that already exist.
Rider
Plateau → Almadies
2 500 F CFA
Driver
New job
Plateau → Almadies · 6.2 km
wallet
12 000 F
+5 000 recharge · wr_88a1
Admin
Payments · Recharges · Logs
08 / Plan
Read the task list and the code. Confirm scope in writing before committing.
Build on current Xcode, fix deprecated APIs and dependencies in all three apps.
Checkout, signed webhook, API verify, locks, wallet ledger, admin views.
Success, failure, cancel, duplicate and replay cases on the agreed flows.
Updated builds for Rider, Driver and the third app, ready for your review.
next
Scope, timeline and price confirmed after review. Written updates only, all through the project chat.
Run the race again