HoopSpark IAP — final review wireframes

For Leong, 2026-09-30. Part A shows the security issue and its fix (decision A). Part B shows the two admin tools being built (decision B, approved 00:00). Nothing here is live yet. Code stays on branch tora/iap-final until merge.

A · Who gets the Sparks: today, the attack, and the fix

Three parties: the HOOP app on the phone, the store (Apple or Google), and the HOOP server.

A1 · Today (normal purchase)

HOOP app (phone)

1User taps RM 4.90
2App writes "for account X" on the purchase. X is taken from the phone.
5App sends the receipt to our server

Apple / Google

3Charges the card
4Signs a receipt that carries "for account X" as the app wrote it

HOOP server

6Checks the store's signature ✓
7Reads "for account X" and gives 500 Sparks to X. It can't tell who wrote X.

A2 · The attack (needs a modified app)

Attacker's modified app

1Writes "for account V" (the victim) on the attacker's own purchase
4Asks Apple / Google for a refund

Apple / Google

2Charges the attacker, signs "for account V"
5Approves the refund, tells our server

HOOP server

3Gives 500 Sparks to V
6Takes 500 back from V. If V already spent the surprise Sparks, it takes the rest from V's balance and reverses some of V's tips to creators; V may appear on the refund-risk list
Result (corrected 2026-09-30): nobody gains. V ends where V started (500 → 1,000 → 500). The only harm is confusion, reversed tips if V spent the surprise Sparks, and V showing up on the admin refund-risk list. A nuisance, not theft — Tora's first description overstated it.

A3 · With the fix (one-time purchase code)

HOOP app (phone)

1User taps RM 4.90
2App asks the server: "give me a purchase code"
4App writes the code (not an account ID) on the purchase
7App sends the receipt to our server

Apple / Google

5Charges the card
6Signs a receipt that carries the code

HOOP server

3Creates a random one-time code and remembers "code → the signed-in account"
8Looks the code up and gives the Sparks to the account that asked for it
If the code is missing or unknown (old app, or forged): the Sparks go to whoever is signed in and sent the receipt
A modified app can't make a code for someone else's account, because only our server creates codes, and each one belongs to the account that asked. The worst a forger can do is buy for themselves. Buyers see nothing different: the code request happens in the background before the store sheet opens.

What changes: one new database table (codes, 30-day expiry), one new server call, the app asks for the code before each purchase (next NOVA build). The case "bought while signed in to another HOOP account" still works: the code remembers the account that was signed in at the time of purchase.

B · Admin tools for customer support

Both live on the existing Admin dashboard, next to the IAP risk section. Only admins can open them; every Spark change needs a reason and is recorded with who did it.

Admin Dashboard
IAP risk
Accounts to check 7
Purchases we refused 1
Unclaimed purchases 1
Customer support · new
User ID, transaction ID or order ID
User
Transaction
Order
Look up
Welcome gift
1,000 Sparks to every new account Manage

B1 · Dashboard: a new "Customer support" card. Paste any ID the customer gives (their HOOP ID, Apple transaction ID, or Google order number "GPA.…").

‹ Customer lookup
Siti
0198cccc-…-0001 Copy
Balance
1,250
Adjust Sparks
Purchases (3)
500 Sparks · RM 4.90
App Store · 29 Sep 21:04 · 2000…789 Copy
Credited
1,600 Sparks · RM 14.90
Google Play · 28 Sep · GPA.33…12 Copy
Refunded
500 Sparks
App Store · 27 Sep · sandbox
Test
Refused (1)
Apple · 2000…555
invalid receipt · 3 tries · 26 Sep
Refused
Spark history (last 50)
+500 Top up 29 Sep
−1,600 Refund 28 Sep
+200 Adjustment 27 Sep
Adjustments (1)
+200 by Leong
"Paid but refused as test purchase, ticket #41" · 27 Sep

B2 · Lookup result: who, balance, every purchase with its status and IDs to copy, refused purchases, recent Spark history, and every manual adjustment with who and why.

Adjust Sparks Siti
Add
Remove
500
Up to 102,000 per adjustment
Paid RM 4.90, receipt refused (sandbox mix-up), ticket #57
Reason · required · shown in the audit log
Linked transaction (optional)
Balance 1,250 → 1,750
Add 500 Sparks
Cancel

B3 · Adjust sheet: add or remove, amount, a required reason, optional linked transaction, and a preview of the new balance. The button stays grey until there's a reason. Removing more than the balance is refused. Pressing twice only counts once.

Confirm  

Add 500 Sparks to Siti?

Balance 1,250 → 1,750. This is recorded with your name and reason, and shows in Siti's Spark history as "Adjustment".

Confirm
Back
Done Added 500. New balance 1,750.

B4 · Confirm step, then the result. The lookup page refreshes and the adjustment appears in both lists.

What the customer sees

In the customer's own Spark history, an adjustment is one line: +500 Adjustment with the date. The reason and the admin's name are admin-only.

Checklist: /iap-final-review · setup guide: /iap-setup-en