Proposal demo · Native iOS + PHP/MySQL + Wave Mobile Money

One payment. One ride. Never two.

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.

apps
0
backend
PHP/MySQL
double charges
0
scroll

01 / Risk

Webhooks arrive twice.
Sometimes ten times.

Payment providers retry. Networks hiccup. Users tap twice. Without a gate, each copy is real money leaving the business.

night shift · 23:41
ERR_01

Double wallet credit

One recharge, credited twice. The driver withdraws money that never came in.

loss0
ERR_02

Ghost dispatch

A driver gets sent before the payment is confirmed. Then the payment fails.

trips unpaid0
ERR_03

Ten drivers, one order

The same notification lands ten times and ten drivers start driving.

dispatches0

02 / Flow

Six gates.
Money moves first.

  1. G1

    Rider taps Pay

    The iOS app asks our server to create the checkout. No keys in the app.

    POST /api/checkout → launch_url
  2. G2

    Wave checkout

    The rider confirms in Wave. The order sits in pending. Nothing is dispatched.

    orders.status = 'pending'
  3. G3

    Signed webhook

    Wave calls the backend. We read the raw body before touching JSON.

    php://input
  4. G4

    Check signature

    Rebuild the HMAC, compare with hash_equals, reject anything old or forged.

    401 if mismatch
  5. G5

    Ask Wave again

    Never trust the message alone. Confirm status, amount, currency and our reference.

    payment_status = succeeded
  6. G6

    Lock, then dispatch

    One transaction, one row lock, one conditional update. Then the driver moves.

    affected_rows = 1 → go

03 / Signature

Try to forge one.
The gate stays shut.

Real HMAC-SHA256, computed in your browser. Change one character in the payload, or replay an old one, and send it.

POST /webhooks/wave
Wave-Signature: t=...,v1=...

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

Ten copies hit the gate
at the same millisecond.

webhooks in
0
marked paid
0
wallet credits
0
drivers sent
0

// press fire. same event id, ten times.

05 / Why only one

Three locks.
Any one is enough.

  • L1

    Unique event id

    A repeat insert hits the key and stops before any logic runs.

  • L2

    Row lock

    SELECT ... FOR UPDATE makes parallel requests wait in line.

  • L3

    Conditional update

    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;
recharge window

06 / Wallet

Recharges get
their own lane.

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.

reftypeamountresult
wr_88a1recharge+5 000credited
wr_88a1recharge+5 000dup ref
ride_10482payment-2 500paid
wr_90c4recharge+1 000failed
wr_90c4recharge0no credit
balance0exact
SC 01

Rider taps Pay.

Order saved as pending. No driver yet.

SC 02

Wave confirms.

Signed webhook in. HMAC checked. The API is asked again.

SC 03

Nine copies wait.

Same event id. The unique key says no. They get a 200 and nothing else.

SC 04

Then the driver moves.

affected_rows = 1. One dispatch, sent once.

SC 05

Delivered. Paid once.

Ledger exact. The admin panel shows every attempt.

Replay

One payment.
Frame by frame.

07 / Your three apps

Same Swift/UIKit apps.
Kept, fixed, not rebuilt.

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

Waiting for payment...
Rider app · pay → wait for server

Driver

PAID ✓

New job

Plateau → Almadies · 6.2 km

wallet

12 000 F

+5 000 recharge · wr_88a1

Driver app · only paid jobs arrive

Admin

ride_10482paid
wr_88a1credited
evt_7f3a x9ignored
ride_10490failed
ride_10491cancelled
wr_90c4failed

Payments · Recharges · Logs

Admin panel · every attempt visible

08 / Plan

Five steps.
One accountable contact.

  1. S1

    Review

    Read the task list and the code. Confirm scope in writing before committing.

  2. S2

    Swift/UIKit fixes

    Build on current Xcode, fix deprecated APIs and dependencies in all three apps.

  3. S3

    Wave + backend

    Checkout, signed webhook, API verify, locks, wallet ledger, admin views.

  4. S4

    Staging tests

    Success, failure, cancel, duplicate and replay cases on the agreed flows.

  5. S5

    TestFlight x3

    Updated builds for Rider, Driver and the third app, ready for your review.

next

Send the task list.
I'll open the gate.

Scope, timeline and price confirmed after review. Written updates only, all through the project chat.

Run the race again