Revision Round 1 — Todo¶
Source: the owners' two review documents — YAM3AT — Current App Flow Review / Round 1
(1.docx) and وثيقة مراجعة تطبيق Yam3at (2.docx).
Checked against: production e56066f and the code on main, 15 September 2026.
Last updated: 16 September 2026 — decisions D1–D6 recorded; steps 1–5 fixed (booking
logic, provider logic, price & payment, status & tracking, data consistency) and
step 6 started.
Every item below was checked, not assumed. Each carries one status:
| Status | Meaning |
|---|---|
| Done | Fixed and covered by automated tests. Still checked on a device in the regression run (§3). |
| Partly done | Some of the fix has shipped; what remains is written under the item. |
| Confirmed | Reproduced in the code or in production data. The evidence is named. |
| Verify | Not reproduced yet. Test it first; fix only if it fails. |
Owner references (YM-P0-01, 1.docx #3) are kept on every item so the owners
can trace each fix back to what they asked for.
The owners' rules for this round¶
- Stabilise the current version. Bug fixing, logic validation, data consistency, UX consistency, TestFlight cleanup, regression testing.
- No new features, no redesign. The exclusion list is in §4 — with the two exceptions approved on 16 September.
- Fix in this order, not screen by screen: booking logic → provider logic → price & payment → status & tracking → data consistency → search, forms & navigation → account → test-data cleanup → UI polish → full regression. §2 follows that order exactly.
- Ready for launch only when P0 = 100% closed, P1 = 100% closed, no P2 that materially hurts the experience, P3 formally deferred, and the full regression run is PASS.
- Deliverable: a new build plus a documented regression run, every case marked PASS / FAIL / BLOCKED with build number, screenshot or video, and bug ID.
Progress¶
47 of 48 items closed. The one still open is the regression gate, and it is open for iOS only:
| Open | Blocked on |
|---|---|
| R1-47 — the regression gate | The flows in §3 on an iPhone. 27 of 28 rows pass on Android. iOS layout is now checked on three simulator sizes (R1-46); the flows themselves — booking, payment, push, Face ID — have not been run on an iPhone. |
| Step | Items | Done | Still open |
|---|---|---|---|
| 1 · Booking logic | R1-01 – R1-05 | R1-01 – R1-05 | — |
| 2 · Provider logic | R1-06 – R1-07 | R1-06, R1-07 | — |
| 3 · Price & payment | R1-08 – R1-11 | R1-08, R1-09, R1-10, R1-11 | — |
| 4 · Status & tracking | R1-12 – R1-15 | R1-12, R1-13, R1-14, R1-15 | — |
| 5 · Data consistency | R1-16 – R1-18 | R1-16, R1-17, R1-18 | — |
| 6 · Search, forms & navigation | R1-19 – R1-29, R1-48 | R1-19 – R1-29, R1-48 | — |
| 7 · Account | R1-30 – R1-36 | R1-30 – R1-36 | — |
| 8 · Test-data cleanup | R1-37 – R1-43 | R1-37 – R1-43 | — |
| 9 · UI polish | R1-44 – R1-46 | R1-44 – R1-46 | — |
| 10 · Full regression | R1-47 | 27 of 28 rows PASS on Android | Gate open for iOS only |
0. What changed in production since the last audit¶
These were not true on 27 August and affect this round directly.
PAYMENT_DRIVERis nowmyfatoorah— it was unset. MyFatoorah still points at the sandbox (apitest.myfatoorah.com). Environment changes leave no audit trail, so who changed it and when is unknown. Online payment is now refused by the server regardless (R1-10).- The owners' example is a real order.
YM-2026-000004— service[LT] الباقة القياسية, provider[LT] بيان للطباعة, package[LT] أساسي, 309.000 د.ك, 2026-08-29 09:00, cash. It sat infinding_providerfor 18 days. The acceptance timeout (R1-07) cancels orders like it. - "hello / world" is an admin broadcast. 13 notifications, title
hello, bodyworld, sent to all users on 26 August (broadcast.sentin the audit log). - After cleanup there are no real vendors. The only non-load-test vendors
are
Ideal Cleaning Co.andSparkle Car Care— both fromDemoCatalogSeeder, both in home/cars — plus the audit vendor. The events domain would be completely empty. Curated demo content fills it (D3). - There is no test environment, and none will be built (D2).
1. Decisions — all taken on 16 September 2026¶
D1 — Provider logic: model A, and no order waits forever¶
Owner ref: YM-P0-01, 1.docx #2
Decided: the provider the customer chooses is final (model A) — that is what
the code already did: vendor_id is set when the order is created, and
finding_provider → provider_assigned happens only when that provider presses
Accept. What was missing was any way out. Now:
- the provider can decline an order they haven't accepted;
- an order not accepted within 24 hours, or by its booked time (whichever comes first) is cancelled automatically, and both sides are told;
- admins get a "Needs attention" list of orders waiting past 2 hours, or accepted but still not done 2 hours after their booked time, and can cancel any of them by hand with a reason that goes into the audit log.
All three windows — plus the 15-minute checkout hold from R1-04 — are admin settings, so changing one is audited. Implemented: R1-06, R1-07.
D2 — Where regression testing happens¶
Owner ref: 2.docx §11
Decided: no staging environment — the product is in beta with no real customers. Regression runs on production, using clearly named test accounts and data that the extended wipe (R1-38) removes afterwards.
D3 — What the app shows once test data is gone¶
Owner ref: 2.docx §11 (Demo Account), YM-P0-02
Decided: option (b). Curated demo vendors, services and one demo account — intentional, believable, no developer text — plus the test data the regression run needs. See R1-40 and R1-42.
D4 — Online payment this round¶
Owner ref: YM-P0-08, 2.docx §11 (Build Configuration)
Decided: a server-side switch. Implemented: R1-10. Production stays on the MyFatoorah sandbox while the switch is off; live credentials are a decision for when it is turned on.
D5 — Favorites¶
Owner ref: 2.docx P1-10
Decided: favorites for both providers and services. Favoriting a service is a new feature, approved as an exception to the round's rules. See R1-27.
D6 — Conversations¶
Owner ref: 2.docx §5
Decided: exactly as the owners asked — a conversation is linked to the booking and its provider. A new feature, approved as an exception. See R1-48.
2. Todo, in the owners' execution order¶
Step 1 — Booking logic¶
-
R1-01 · P0 · Done — The order was created before payment, so going back created a second one Owner ref:
YM-P0-04(Back/Forward) Was: the order was created on "متابعة الدفع" (booking_flow.dart); Back from payment then "متابعة الدفع" again created another.YM-2026-000001has beenpendingand unpaid since before 27 August. Fixed: the booking screen keeps the order it created. Back from payment and continue with nothing changed reuses it; change anything and the old order is cancelled before a new one is made. The server also replaces a customer's own unpaid order for the same provider and time rather than leaving it behind, with its wallet credit and coupon use carried back. Tests:OrderHoldsTest. -
R1-02 · P0 · Done — No idempotency on any mutating endpoint Owner ref:
YM-P0-04(Retry, Timeout, Slow network) Was: a request that timed out on the phone but succeeded on the server ran again on retry. Fixed: every signed-in mutating endpoint, customer and admin, accepts anIdempotency-Key. A retry with the same key gets the first response back; the same key with a different request is refused; keys belong to one user and are kept for a day. The app sends one key per attempt on creating an order, confirming cash, paying online and accepting a quote. Requests now time out after 90 seconds instead of hanging. The event deposit payment sends one as well. Tests:IdempotencyTest,OrderHoldsTest. -
R1-03 · P0 · Done — Double tap in the same frame got through Owner ref:
YM-P0-04(Double Tap) Fixed: "متابعة الدفع", "تأكيد الطلب" and "ادفع الآن" ignore a tap while busy; behind that, the idempotency key and a row lock on confirmation mean a double submission still places the order once. Tests:OrderHoldsTest::test_confirming_twice_places_the_order_once. -
R1-04 · P0 · Done — Abandoned orders blocked the provider's time slot forever Owner ref:
YM-P0-04,2.docx P1-4Was: every order that wasn't cancelled took its slot, including unpaid ones, nothing expired them, andpendingwas filed under "القادمة". Fixed: - an unpaid checkout holds its slot for 15 minutes, then the slot is back on sale;
- every 5 minutes, unpaid checkouts past the hold are cancelled, returning wallet credit and coupon use (one with an online payment still in flight is left to land);
- confirming after the hold checks the slot is still free — if someone else booked it, the customer is told and sent back to choose a time;
- bookings are serialised per provider, so two customers can't take one slot;
-
unpaid checkouts never appear in My Bookings, and never in the provider's inbox. Tests:
OrderHoldsTest. -
R1-05 · P0 · Done — Success appears only after the backend confirms Owner ref:
YM-P0-05Code: success is shown only for an order the server has placed. On a refusal the payment screen now finds out why: a network or server error can be retried safely; an order the first tap already placed opens its confirmation; an order that can no longer be placed is explained, and the customer goes back to choose a time. Verified on a device against production, 2026-09-16. - Double submit: three rapid taps on "تأكيد الطلب" produced exactly one order and one idempotency key.
- No network: with Wi-Fi and data both off, tapping "تأكيد الطلب" kept the user on the payment screen and said "تعذّر الوصول إلى الخادم – تحقّق من اتصالك وحاول مرة أخرى." "تم إرسال طلبك" was never shown — which is the whole of this item.
- Retry: restoring the connection and tapping again placed the order and then
showed success.
YM-2026-000008, and the database holds one order, not two. Success copy says "تم إرسال طلبك", not "confirmed" — see R1-15.
Step 2 — Provider logic¶
- R1-06 · P0 · Done — "Searching for a provider" described logic that doesn't exist
Owner ref:
YM-P0-01,1.docx #2, #3Was: the success screen said "سيبدأ مقدّم الخدمة في تجهيز الطلب الآن" and, on the same screen, "جارٍ البحث عن أفضل مقدّم خدمة…", with a spinner. Fixed: - status label everywhere: "بانتظار قبول مقدّم الخدمة" (app) / "بانتظار قبول المزوّد" (dashboards); after acceptance, "قبل مقدّم الخدمة";
- success screen: "تم إرسال طلبك", "أرسلنا طلبك إلى ‹اسم مقدّم الخدمة›، وسنُعلمك بردّه.", a waiting banner with no spinner, and "بانتظار القبول" on the tracker;
- booking details: the second stage is "قبول مقدّم الخدمة";
-
notification: "قبل مقدّم الخدمة طلبك" (was "قبل أحد المزوّدين طلبك"). The stored status value is unchanged, so nothing downstream broke. Tests:
ProviderAcceptanceTest(notification wording). -
R1-07 · P0 · Done — Orders whose provider never responds Owner ref:
YM-P0-01Fixed (D1): - providers have Decline next to Accept on the dashboard's Orders page;
- unaccepted orders expire after 24 hours or at their booked time;
- admins: Service Orders → Needs attention lists what's stuck, with how long it has waited, and Cancel order asks for a reason that is audited;
- the customer is notified in each case, and booking details say why the order
was cancelled (declined, not accepted in time, cancelled by Yam3at).
Tests:
ProviderAcceptanceTest.
Step 3 — Price & payment¶
- R1-08 · P0 · Done — The booking screen computed the total on the phone, in floating point
Owner ref:
YM-P0-03,YM-P0-02,1.docx #4Was: subtotal, coupon discount, wallet and total weredoublearithmetic inbooking_flow.dart, while the payment screen showed the server's total — two sources of truth, and floating-point money. Fixed: - one pricing path on the server (
OrderQuote), used both by a new price preview,POST /orders/quote, and by placing the order — the two cannot disagree; - the booking screen shows only the server's quote, re-priced on every change (add-on, coupon, wallet). While a new price is on its way the total dims and "متابعة الدفع" waits; if pricing fails it offers a retry, never a guess;
- payment and booking details list every line the total is made of — subtotal, service fee, discount, VAT, tip, wallet credit — with the same labels;
-
server-side rounding is bcmath, half-up. The float path rounded a 15% coupon on 12.250 (1.8375) to 1.837 on one path and 1.838 on another. Tests:
OrderQuoteTest(every mix of add-ons, coupon and wallet: the quote equals the created order),MoneyTest. -
R1-09 · P0 · Done — Switching package or provider leaves no stale price Owner ref:
YM-P0-03,2.docx P1-1, P1-2Why it can't recur: the booking screen no longer holds a price of its own. Every total it shows is the server's quote for the package on screen, and the order is priced again from the package when it is placed. Note: the owners' "Provider A 235.000 / Provider B 309.000" example is nine different load-test services all named[LT] الباقة القياسية, from nine different providers. Cleanup (R1-39) removes that illusion. Tests:OrderQuoteTest::test_the_quote_follows_the_package_chosen. Device check (§3 rows 7, 14): card price → details → change package → book → back → change package again → payment. -
R1-10 · P0 · Done — The server would start an online payment the app says is disabled Owner ref:
YM-P0-08,1.docx #8Was: the app greyed out KNET and card from a build flag, but every pay endpoint started a charge for anyone calling it directly — against the MyFatoorah sandbox. Fixed: - an Online payments (KNET / card) switch in Admin → Settings, off. While off the server refuses every online charge: order, plan, deposit, milestone, ticket, listing boost and paid subscription. Cash is unaffected. Turning it on is recorded in the audit log;
- the app and the web booking page read the switch from
/configinstead of a build flag, so no screen offers a payment the server would refuse; -
also found and removed: four places that "paid" by posting a fake payment-success webhook from the client itself — the event deposit (app and web), milestone funding and ticket sales (app). They now open the gateway's page. Production already refused the fake webhook, so those buttons could only ever fail. Tests:
OnlinePaymentSwitchTest. -
R1-11 · P0 · Done — Cash is never marked paid, but its label changed between screens Owner ref:
YM-P0-07,1.docx #8Was:paid_atwas already empty on every cash order (correct), but the success screen said "نقدًا" and booking details "نقدًا عند الإتمام". Fixed: one label, defined once on the order and used by success and details: "نقدًا عند الإتمام". My Bookings shows no method, and "مدفوع" appears only for an order actually paid online.
Step 4 — Status & tracking¶
-
R1-12 · P0 · Done — Three different status maps that disagreed Owner ref:
YM-P0-06,1.docx #9,2.docx §4Was: a six-state chip (catalog_widgets.dart), a four-stage tracker on the success screen that skippedprovider_assignedandin_progress, and a separate five-stage timeline in booking details — so one order could read differently on each screen. Fixed: one map (orderStatusView/orderStages) gives the chip its words and colours, and both screens the same five stages: booked → provider accepted → on the way → in progress → completed. The success tracker checks every stage reached and marks the next one. -
R1-13 · P0 · Done — Progress notifications arrived in English Owner ref:
1.docx #15,2.docx §5Was: provider progress sent a generic "Your order was updated", and theorder.on_the_wayandorder.in_progresskeys were missing, so those fell back to English. Fixed: Arabic and English titles for accepted, on the way, started, completed, declined and expired; the stored fallback is the matching English title. Tests:ProviderAcceptanceTest. -
R1-14 · P0 · Done — Tapping a notification never opened the booking Owner ref:
1.docx #15,2.docx §5Was: tapping only marked it read, and a read notification couldn't be tapped at all. Fixed: one router opens what a notification is about, from the ids it already carries — order → order details, booking → booking details, quote request → the request (the provider's inbox for a provider), message → that conversation, consultation → consultations, support reply → that ticket. The tap marks it read on the way, read notifications stay tappable, and rows that open something show a chevron. Push taps use the same router, including the tap that starts the app: that screen waits for the splash to finish, so it isn't replaced a moment later. Device check (§3 row 20): create order → status changes → notification arrives → tap opens that exact booking. -
R1-15 · P1 · Done — Each status lands in the right My Bookings tab Owner ref:
2.docx §4Fixed and covered:pendingno longer counts as "القادمة" (R1-04), and a test pins every status to exactly one tab — upcoming (confirmed, awaiting provider, accepted), active (on the way, in progress), completed, cancelled — with a checkout cancelled before it was placed appearing in none. Tests:OrderTabsTest. One place still contradicted the rest, found on a device 2026-09-16 and fixed. Placing an order notified the customer "تم تأكيد طلبك" — your order is confirmed — while the success screen, the status chip and the tracker all said it was waiting for the provider. The order sits infinding_provideruntil the provider accepts, so the notification was simply wrong. There is now anorder.placedtype: "تم إرسال طلبك، بانتظار قبول مقدّم الخدمة" for the customer and "طلب جديد بانتظار قبولك" for the provider. Test:ProviderAcceptanceTest::test_placing_an_order_does_not_tell_the_customer_it_is_confirmed.
Step 5 — Data consistency¶
-
R1-16 · P0 · Done — Dates and times were shown in technical format Owner ref:
1.docx #6,2.docx §10Was: raw2026-08-29and09:00on payment, success, booking details and the bookings list;split('T')fragments in events and the planner; English "9:00 AM" on the time slots, inside an Arabic interface. Fixed: one formatter (core/format.dart) for the whole app — Arabic day and month names, Latin digits as the app already uses, and "ص / م": "السبت 29 أغسطس 2026" and "9:00 ص". Time slots are formatted on the device instead of showing the server's English label, and the date sent back to the API is unchanged (apiDate). Applied on: payment, success, booking details, My Bookings, the booking screen, review, events, event details, the planner, event bookings, tickets and quotes. -
R1-17 · P0 · Done — Currency position flipped between screens Owner ref:
2.docx §10Was: "235.000 د.ك" in one row and "د.ك 235.000" in another on the same payment screen — the right-to-left layout reordered the two pieces. Fixed: one money formatter, used by every screen above. The amount and its currency are emitted inside a left-to-right isolate, so the layout cannot split or reorder them, and the amount is padded as text — never through a float. -
R1-18 · P0 · Done — The same eleven facts survive the whole journey Owner ref:
YM-P0-02,1.docx #1Service, package, provider, price, date, time, address, coupon, payment method, booking reference and status must match on Service details → Booking → Payment → Success → My Bookings → Booking details → Tracking → Notifications. Run on a device, 2026-09-16 (§3, run 1): all eleven held across all twelve screens for orderYM-2026-000005— زهور المناسبات · تنسيق الطاولات · ورد الخليج · 140.000 د.ك · الأربعاء 16 سبتمبر 2026 · 11:00 ص · (the saved address) · no coupon · نقدًا عند الإتمام ·YM-2026-000005· بانتظار قبول مقدّم الخدمة. The one place they didn't match was the notification, which claimed the order was confirmed — fixed, see R1-15 below.
Step 6 — Search, forms & navigation¶
-
R1-19 · P1 · Done — "متوفر اليوم" was printed on everything Owner ref:
2.docx P1-3Was: hard-coded on the service screen, whether or not a slot was free today. Fixed: the server answers it from the provider's real slots for today (available_todayon the service endpoint), and the chip appears only when it is true. Tests:CatalogVisibilityTest. -
R1-20 · P1 · Done — Unavailable dates could be selected Owner ref:
2.docx P1-4Was: the date strip listed every day; a day with nothing free only revealed itself after tapping ("لا توجد مواعيد متاحة"). Fixed: a new endpoint says which of the next two weeks have anything free, and those days are greyed out, unpickable and labelled for screen readers. Also fixed — found on the way: a provider's blocked dates (including days synced from their Google Calendar) were respected by search, provider profiles and instant booking, but not by the slots a customer picks, so a blocked day could still be booked. It now has no slots at all. Tests:CatalogVisibilityTest. Device check (§3 rows 10, 11): date and time survive going back from payment. -
R1-21 · P1 · Done — Saving an empty address silently did nothing Owner ref:
1.docx #5,2.docx P1-5Was: the sheet closed and nothing was saved when the address line was empty, with no message. Fixed: the address line is required, shown inline under the field; the sheet stays open until it is filled, and the error clears as soon as the customer types. -
R1-22 · P1 · Done — Coupon errors were one generic sentence Owner ref:
1.docx #7,2.docx P1-6Was: invalid, expired, used up and below-minimum all returned the same sentence, so a customer couldn't tell which it was. Fixed: a message per failure — unknown code, expired, limit reached, or "applies to orders of X and above" naming the minimum — in Arabic and English, from both the coupon check and placing the order. The booking screen prices through the server, so original price, discount and final total come from one place and carry to payment and the booking (R1-08). Still to confirm with the owners: whether a code should be once per customer. Today's limit is global; that is a business rule, not something to assume. Tests:CouponMessagesTest. -
R1-23 · P1 · Done — Search results survive going back Owner ref:
2.docx P1-7Run on a device, 2026-09-16: it holds. A partial term ("Flow") typed into an Arabic UI found the Arabic-titled "زهور المناسبات" — the search matches the English title too. Opening the result and pressing Back returned the query, the filtered result and the selected sort chip, all intact. Noted: search runs on submit, not as you type. Not a defect, but worth deciding. -
R1-24 · P1 · Done — Half the categories were empty Owner ref:
2.docx P1-8Was: 13 of 26 sub-categories had no active service and no visible provider — including five of the twelve events categories. Tapping one opened a dead end. Fixed: the categories endpoint only returns categories with something behind them — a bookable service or a visible provider, their own or a child's — so the app never shows one that leads nowhere. Approving, publishing or pausing a provider (or adding a service) updates the list straight away. Note: on today's production data that leaves the events grid nearly empty until the curated demo content lands (D3, R1-42). The load-test seeder's round-robin categories go with the cleanup (R1-39). Tests:CatalogVisibilityTest,CategoryScopeTest,ReferenceDataTest. -
R1-25 · P1 · Done — Each sort returns what it claims; reset restores the list Owner ref:
2.docx P1-9
How it was tested without inventing reviews for real customers. Production has no
reviews, so "Top rated" and "Most booked" had nothing to order by. On 2026-09-16, after a
database backup, 14 reviews and 29 completed orders were added temporarily under a
tagged test account (sorttest.customer@yam3at.test), inserted directly so no
notification reached any provider. The values were chosen so that all four sorts give
a different order across the 8 demo services — if two sorts agreed, a broken one
could pass. Everything was removed with yam3at:purge-users (dry run first: exactly
14 reviews, 29 orders, 1 user) and every service and provider rating recomputed back to
zero. The data was live for about 75 minutes.
Two defects found and fixed. 1. "Most booked" and "Lowest price" had no tie-breaker. Equal counts are the normal state of a young catalogue, and MySQL may return tied rows in any order — different between requests, and able to repeat or skip a row across pages. Every sort now ends on a stable key, and a tie goes to the better-rated service first. 2. There was no reset. The sliders icon in the search bar looked like filters and only reloaded the list. New filters are out of scope (§4), so it is now a reset that clears the search and restores the default order — shown only when there is something to reset. Also fixed on the same screen: a failed load showed the empty state, telling people a category had no services when the app simply hadn't reached the server. It is now an error with a retry.
Verified on the device, all 8 positions compared, not eyeballed: | Sort | Order on the device | | |---|---|---| | الأعلى تقييمًا (default) | Photography › Decor › Hall › Flowers › Catering › Entertainment › Transport › Printing | PASS | | الأكثر طلبًا | Flowers › Photography › Hall › Transport › Catering › Decor › Entertainment › Printing | PASS | | الأقل سعرًا | Printing › Transport › Flowers › Entertainment › Photography › Decor › Hall › Catering | PASS | | الأحدث | Transport › Printing › Flowers › Entertainment › Decor › Photography › Catering › Hall | PASS |
Reset: hidden on a fresh list; appears after changing the sort, or after a search alone ("Flow" filtered the list to the flowers service); tapping it cleared the search, restored "الأعلى تقييمًا" with all 8 services in that order, and hid itself.
Tests: CatalogSortTest (8). The two tie-break tests were confirmed to fail on the
old code and pass on the fix — the test data places the better-rated service later in
each tie, so a database returning ties in insertion order can't pass them by accident.
Worth the owners' attention, not changed: "Top rated" ranks a service with a single 5★ review above one with fifty 4.9★ reviews. That is literally what it claims, but once real reviews arrive a weighted rating would rank more fairly.
- R1-26 · P1 · Done — Navigation
Owner ref:
1.docx #13,2.docx §9Run on a device, 2026-09-16. Selected tab state, Back to the real previous screen, and kept scroll/form state all hold. No dead entries: the domains with no stock (المنزل, السيارات) lead to a designed empty state with a "request a quote" path, not a blank screen. The keyboard issue was real and is fixed. On sign-in and sign-up the keyboard covered the main button exactly as the audit said. A static check had missed it because nothing overflows — the button simply sits below the fold in a scroll view.revealWhenFocusednow scrolls the form to its end when the last field takes focus, which brings the button up with the keyboard. This took three attempts, and the two failures are worth recording because neither fails visibly in code — only on a device: Scrollable.ensureVisibleon the button, keyed by aGlobalKey. AListViewdoes not attach children far below the fold, so the key had no context and the call never ran.- The same, behind a fixed 320 ms delay. Too early: the viewport is still full height while the keyboard animates, so the call is a no-op.
- A
ScrollController, waiting formaxScrollExtentto grow (the keyboard taking space) and then settle, then animating to the end. This is what works.
A slow keyboard still beat it — found by the iPhone screen-size walk (R1-46),
fixed. Attempt 3 stopped waiting 1.3 seconds after the field took focus. On the
Android emulator the keyboard took 2.1 seconds just to start opening, so the form
never scrolled and the button stayed under it. A first keyboard on a slow phone, or on
a fresh iOS simulator, can be that late. It now waits up to 4 seconds, and still acts
as soon as the keyboard settles. test/reveal_when_focused_test.dart (3 tests): the
slow-keyboard test fails on the old code (the form stops at 275 of 336 px) and passes
on the new; a keyboard that never comes leaves the form alone. Verified on the
emulator with a cold keyboard (3.0 s to appear): the password field, "تسجيل الدخول"
and the sign-up row all sit above it.
Verified on the device, both screens: with the keyboard up, sign-in now shows the focused password field, the forgot-password link, "تسجيل الدخول" and the sign-up row together; sign-up shows the confirm field, the invite field, "إنشاء حساب" and the terms line together. Before/after screenshots were taken. One open question for the owners, not a defect: logging in from the onboarding carousel returns you to the carousel rather than into the app. The session is correctly started; you just have to tap "ابدأ" again.
-
R1-27 · P1 · Done — The heart on a service saved nothing Owner ref:
2.docx P1-10· Decided: D5 Was: the heart toggled a local flag — it reset when the screen was reopened, the service never appeared in "المفضلة" (which listed providers only), and the account counter counted providers. Fixed (D5): services can be saved as well as providers. Saving is kept on the server, the heart shows the saved state as soon as the screen opens, and "المفضلة" has a tab for each with counts. Tapping it while signed out opens sign-in rather than pretending to save. Tests:ServiceFavoriteTest. -
R1-28 · P1 · Done — "حجوزاتي" and "حجوزات المناسبات" are genuinely different Owner ref:
2.docx §9Not duplication. "حجوزاتي" is/orders— direct service bookings with provider acceptance and on-the-way tracking. "حجوزات المناسبات" is/bookings— a request for quotes, a quote accepted, a deposit. Different data, lifecycles and payment rules. Fixed: each list now says what it is, under its title. Still to send: this note to the owners. -
R1-29 · P1 · Done — Yam3at AI and the planning assistant Owner ref:
1.docx #11,2.docx §7 - Yam3at AI (bottom navigation) calls
POST /ai/plan: it turns a brief into a bookable plan of services and can book the whole plan at once. - مساعد التخطيط (Account) calls
POST /ai/assistant: a conversation. -
Independent endpoints; no merging this round. Test each: open → send → loading → response → network error → retry. Run on a device, 2026-09-16 — all six states. Open → send → loading → response: Yam3at AI returned a real plan built from live demo data (ضيافة المناسبات · بوفيه لـ 100 ضيف · 600.000 د.ك) with a "book the whole plan" action. Network error: with the connection cut, sending shows "تعذّر الوصول إلى الخادم – تحقّق من اتصالك وحاول مرة أخرى." in Arabic. Retry: the prompt stays in the box, so tapping send again after the connection returns works without retyping. One observation, not a defect: that error is a toast and fades. A screenshot taken ten seconds later showed a screen that looked simply idle — a user who glances away sees nothing at all. The forms now keep their errors in place (R1-45); this screen still does not. Worth doing next round. Worth the owners' attention: asked to plan a party for 80 people it returned one service — catering — when venue, decor and entertainment were all available in the catalogue. The plan is correct but thin. Rebuilding the AI is out of scope for this round (§4), so this is raised, not fixed.
-
R1-48 · P1 · Done — Conversations weren't linked to a booking Owner ref:
2.docx §5· Decided: D6 Was: a conversation was with a provider and nothing else, and "محادثة" on a booking opened the general inbox — so neither side could tell which job a message was about. Fixed (D6): a conversation can belong to an order or an event booking, and takes its provider from it. Chatting from a booking opens (or starts) that booking's conversation, both sides see the reference in their list, and a general conversation stays separate from a booking's one. Tests:BookingConversationTest.
Step 7 — Account¶
-
R1-30 · P1 · Done — WhatsApp preference switched a channel that doesn't exist Owner ref:
2.docx §8(Notification Preferences) Was: the preferences themselves are stored and persist, but WhatsApp sending isn't connected — so the switch changed nothing. Fixed: the switch is gone from notification preferences until the channel is live. The stored preference is untouched and still round-trips on save, so when WhatsApp is connected the switch comes back with the customer's choice intact. -
R1-31 · P1 · Done — Referral reward could pay twice under a race Owner ref:
2.docx §8(Referral) Was: the payout was guarded byrewarded_at, which stops a second payout in sequence but not two confirmations landing at the same moment — there was no lock. Fixed: the referral is claimed under a row lock and the wallet credit is applied inside the same transaction, so it pays exactly once. The amount is computed with bcmath rather than through a float. Tests:ReferralRewardOnceTest. -
R1-32 · P1 · Done — Partner programme and referrals are separate systems Owner ref:
2.docx §8In code they are separate (affiliate percentage commission vs. a fixed 2.000 د.ك wallet credit). On the device, 2026-09-16: they are two separate account tiles with different names and icons — "ادعُ واربح" and "برنامج الشركاء" — opening two different screens. Nothing on the account screen blurs them. -
R1-33 · P1 · Done — Account figures are real backend values Owner ref:
2.docx §8Name, avatar, membership level, wallet, points, favorites count, bookings count. Fixed meanwhile: the "المفضلة" figure counts saved services as well as saved providers (R1-27), and counts from the server's totals rather than the first page. Verified on a device, 2026-09-16: name, avatar initial, "عضو برونزي", wallet 0.000 د.ك and points 0 are all real values, and "المفضلة" went 0 → 1 the moment a service was saved. One figure was wrong, and is fixed. "الحجوزات" showed 0 for a customer with a live order, because it readbookings_active— RFQ-based bookings only — while the tile directly below it listed that order. The dashboard now also returnsorders_active(the statuses the orders list calls upcoming and active) and the screen shows the sum. Test:DashboardTest::test_the_dashboard_counts_direct_orders_as_bookings_too. -
R1-34 · P1 · Done — Security switches reflect the real state (Android; Face ID on an iPhone still to see) Owner ref:
2.docx §8(Security) Biometric lock (device-local), email verification and two-factor (server).
An earlier version of this item said the emulator could not test the biometric lock. That was wrong. The Android emulator can enrol a simulated fingerprint and touch it on demand, and the app uses the same code path for fingerprint and Face ID. Tested on 2026-09-16 with a fingerprint enrolled — and two of the three switches were not reflecting the real state.
1. The app lock said "on" and mostly wasn't. It locked on a cold launch and never
again: leave the app, come back, and its content showed with no prompt. The lock
wrapped only the splash route (home: BiometricGate(child: SplashScreen())), and the
splash leaves with pushReplacement, which discards that route — so after launch
there was no lock left to run. On iOS, where apps are resumed far more often than
cold-started, Face ID would almost never have fired.
Fixed: the lock now sits above the Navigator (MaterialApp.builder), so it covers
every screen and survives navigation, and it locks again on returning after a minute
away. Not on every return — a quick switch to copy a code shouldn't demand Face ID —
and time spent inside the system prompt never counts as leaving, or success would
immediately ask again. While locked, the screen underneath is hidden from screen
readers too. A "تسجيل الخروج بدلًا من ذلك" link means Face ID locking itself out
after too many attempts can never trap anyone; it signs out and clears every open
screen, or it would reveal the page the lock was covering.
Also fixed: the switch named the wrong thing. It read "قفل بالبصمة" (fingerprint)
on every device, Face ID iPhones included, while its English said "Biometric lock".
It now names what the device uses: "قفل بـ Face ID" on an iPhone, "قفل بالبصمة" for a
fingerprint, and a neutral "قفل التطبيق" where the system won't say.
2. Email verification showed a verified address as unverified. The API has always
returned email_verified; the app never read it, so the screen offered to send a
code to an address the server had already verified. It now starts from the server's
answer and shows "تم توثيق بريدك الإلكتروني.".
3. Two-factor was already right: off on the server, and the screen offers to turn it on.
Verified on the device: | Check | Result | |---|---| | No biometric enrolled | Switch hidden — nobody can turn on a lock they can't open | | Enrolled, lock off | Switch appears, off | | Turn on, wrong finger, cancel | Stays off | | Turn on, enrolled finger | Turns on | | Cold launch | Locked; a wrong finger stays locked | | Enrolled finger | Unlocks | | Return after 70 s, private screen open | Locked again; the screen underneath is not readable, even to a screen reader | | Return after 8 s | Not locked | | "Sign out instead" | Signed out; the private screen underneath is gone | | Cold launch while signed out | Not locked | | Email / two-factor screens | Both match the server |
Tests: test/app_lock_test.dart (when to lock, the grace window, and never
re-locking because of its own prompt) and test/app_user_test.dart.
Still to see: Face ID itself, on an iPhone — the new TestFlight build carries this.
-
R1-35 · P1 · Done (logout) · Open (delete, export) — Logout, delete account, export data Owner ref:
2.docx §8Logout verified on a device, 2026-09-16: after logging out, Back exits the app to the launcher rather than revealing any previous screen, and relaunching lands on the guest home with no notification badge. The navigation stack is cleared, not just popped. Delete account and export data were deliberately not run. Both tiles exist, are reachable and are styled as destructive. The account under test is the demo account being handed to the owners, and deleting it to prove the button works would have destroyed it. They need a run on a throwaway account. -
R1-36 · P1 · Done — Empty states change after the first item Owner ref:
1.docx #14Verified on a device, 2026-09-16: "مناسباتي" showed "لا توجد مناسبات بعد." with the official placeholder; creating an event listed it immediately, cleared the form and removed the empty message. The same pattern holds for the orders tabs — القادمة emptied itself the moment the provider declined, and showed its empty state rather than a stale row.
Step 8 — Test-data cleanup¶
-
R1-37 · Done — The "hello / world" broadcast is out of every inbox Owner ref:
1.docx #12, #15,2.docx §1113 notifications (see §0.3). Nine belonged to accounts removed by R1-39/R1-40 and went with them;yam3at:purge-notifications --type=broadcast --title=hellodeleted the remaining 4 on 2026-09-15. Real notifications are matched by type and exact title, so nothing else was touched, and the command refuses to run with neither given. Tests:UserPurgeTest::test_a_test_broadcast_can_be_taken_back_out_of_inboxes,::test_purging_notifications_needs_something_to_match_on. -
R1-38 · Done — The wipe now removes the whole trail Owner ref:
1.docx #12,2.docx §11Evidence: services are soft-deleted, so the database cascades never fire. Running the wipe today would leave service packages, availability windows and orders behind, and it doesn't touch notifications, addresses, conversations, favorites, wallet entries, referrals or idempotency keys for load-test users. Fixed:UserPurge(Platform\Support) derives the full graph from a set of account ids — vendors, services (withTrashed, the case the cascade missed), packages, add-ons, availability, blocked dates, events, RFQs, quotations, bookings, orders, payments (matched by morph class), conversations, messages, notifications, addresses, favourites, wallet entries, referrals, device tokens, subscriptions, analytics and idempotency keys — about 45 tables, deleted children-first inside one transaction.--wipeandyam3at:purge-usersboth run through it, both print a per-table count first, and both take--dry-run. It is also how regression test data is removed after each run (D2). Ran on production 2026-09-15, afterdeploy/backup-db.shand a dry run: 307 rows across 12 tables — 37 users, 12[LT]providers with their packages and 84 availability windows, 25[LT] Event N, 25 RFQs, 75 quotations, 4 bookings and 2 orders. Counts matched the dry run exactly. Tests:UserPurgeTest(7 tests, incl.test_it_removes_the_whole_trail_a_test_account_leavesandtest_it_leaves_everyone_else_alone),LoadTestDatasetTest::test_wipe_removes_its_own_rows_and_nothing_else. -
R1-39 · Done — The audit and QA accounts are gone Owner ref:
2.docx §11(Test Addresses, Random Bookings) The accounts created during the device audit:mobile.audit1andvendor.audit1— "Audit Wedding", the RFQ "قاعة أفراح لـ 180 ضيف", its quote and booking, its saved address, and orderYM-2026-000003. Ran on production 2026-09-15 together with R1-40:yam3at:purge-usersover 11 named accounts removed 111 rows across 22 tables. The list also covered the QA leftovers the audit hadn't named —diag.*,emu*, twoe2e.cust.*,rw.*,tf.*andmftest_*. Deliberately kept: the four accounts belonging to real people (their addresses are deliberately not written down here), the platform admin account, and the two anonymiseddeleted-…@deleted.invalidtombstones the account-deletion flow leaves behind. Removing a real person's account is not a cleanup decision to take unasked. Note: orderYM-2026-000004belonged to one of those four accounts but was placed against a[LT]provider, so it went with that provider. Nothing was left pointing at a row that no longer exists. -
R1-40 · Done — The old service-app demo vendors are gone Owner ref:
2.docx §11· Decided: D3Ideal Cleaning Co.andSparkle Car Care(demo seed data, home and cars) and ordersYM-2026-000001andYM-2026-000002on them — removed on 2026-09-15 in the sameyam3at:purge-usersrun as R1-39. They sold home cleaning and car washing, which is the dormant super-app, not an events marketplace. The curated demo content of R1-42 replaces them. After the cleanup: 16 accounts (4 real, 1 admin, 2 tombstones, 8 demo providers, 1 demo customer), 0 orders, 0 bookings, 0 RFQs, and only the four events real users created themselves. -
R1-41 · Done — One placeholder, everywhere an image is missing Owner ref:
2.docx §11(Broken Images, Placeholder) Evidence: 3 media files on the whole platform; service cards show a generic grey image icon. Fixed:ImagePlaceholder(core/net_image.dart) is now the default fallback for every remote image — a branded tile rather than a broken-image glyph, drawn the same whether the URL is absent, still loading or failed, so a slow connection and a missing file don't look like two different bugs. This was only half done, and the device run caught it. Two screens drew their own fallback instead of using the shared one, and they were the two the audit had been looking at: the home/category service card (a greyIcons.image_outlinedon white) and the service detail hero (a grey "design services" glyph, 56px, the largest image in the app). Both now useImagePlaceholder. Verified on the rebuilt APK — the cards and the hero show the brand mark. Lesson worth keeping: making a shared widget the default does nothing for the callers that never asked for the default. -
R1-42 · Done — Eight curated providers and one demo account Owner ref:
2.docx §11· Decided: D2, D3 Fixed:DemoContentSeedercreates eight events providers — a hall, catering, photography, decor and lighting, entertainment, flowers, invitations and printing, and guest transport — each bilingual, each with a priced service, two packages, add-ons where they make sense, and Saturday–Thursday 10:00–22:00 availability. Kuwaiti names and prices in KWD to three decimals. One demo customer with a clean history; its password is generated and printed once, never committed. The seeder is idempotent, so it can be re-run after every regression pass, and every account carries thedemo.prefix soyam3at:purge-userstakes the whole set back out. Ratings and review counts are left at zero. Inventing review scores for providers who have never been booked would be fabricated social proof shown to real customers; the empty state is the honest one. Seeded on production 2026-09-15 and verified live: 8 categories now carry stock (was 6), 8 services with prices from 95.000 to 600.000 KWD, 16 packages, 48 availability windows, 12 bookable slots on an open day and none on a Friday, and the demo account logs in. Tests:DemoContentTest(6 tests, incl.test_every_demo_service_can_actually_be_booked,test_no_demo_provider_claims_reviews_it_never_had,test_the_whole_demo_set_can_be_taken_back_out). -
R1-43 · Done — A release build can no longer point at localhost Owner ref:
2.docx §11(Build Configuration) Evidence: if the build omitted the API address, the app fell back tohttp://localhost:8000/api/v1(api.dart). Correction to the audit:codemagic.yamlis in the repository and does passAPI_URL. The original finding was wrong on that point; the fallback was real. Fixed: in a release build the fallback is now the empty string, so the app cannot quietly talk to a developer machine. A build with no API address starts on a plain "this build is misconfigured" screen instead of failing every request with a connection error.codemagic.yamlgained a "Require the API address" step that fails both workflows before they build, andflutter analyzeis blocking there now (it was|| true, which let analyzer errors ship). Already correct (checked): development codes are hidden in production (expose_dev_codes = false); no debug output in the app. Online payment is off on the server (R1-10).
Step 9 — UI polish¶
- R1-44 · P2 · Done — Consistency pass (Android; iOS still owed)
Owner ref:
2.docx §10Typography, buttons, cards, fields, corner radius, spacing, badges, modals, and the empty, loading, error and selected states. Right-to-left: arrows, Back buttons, alignment, numbers, currency, dates, times, forms, icons. The known concrete issues are already items above (R1-16, R1-17, R1-21, R1-26).
Checked from here, and clean: every horizontal padding in the app is
direction-neutral — there is no EdgeInsets.only(left:/right:) anywhere, and all
34 fromLTRB values are horizontally symmetric, so none of them mirror wrongly in
Arabic. Physical alignment appears twice, both in one decorative gradient.
Fixed: the error colour was a hand-copied 0xFFDC2626 in 12 places across 7
files while the design system had defined it as a token all along. It is now
danger / dangerSoft / dangerInk in theme.dart, mirrored into
apps/web/src/app/globals.css and docs/ux/design-tokens.md as that file
requires. dangerInk is new: the documented #DC2626 measures 3.97:1 on the
#F3E7E3 error wash, below the 4.5:1 WCAG 2.2 AA floor for body text, so error
messages now use #991B1B (6.9:1) and #DC2626 is kept for icons, borders and
destructive buttons. The design-tokens page carries that warning now.
Looked at on a device, 2026-09-16, across ~30 screens. Mirrored Arabic layout is
right throughout: back chevrons sit top-trailing and point the correct way, list
chevrons point left, the onboarding dots progress right-to-left, the AM/PM markers are
ص/م, and own-messages sit on the left in chat as Arabic convention expects. Money is
"450.000 د.ك" everywhere in the product. Typography, card radius, chip and badge styles
are consistent between screens. Empty, loading, error, success and no-permission states
were all seen and are all designed — the logged-out account tab, for example, is a
proper "sign in to see this" state rather than a blank.
Two consistency defects found and fixed (a header scrolling under the status bar on
four screens, and two screens drawing their own image fallback) — see R1-45 and R1-41.
Two raised, not fixed, because §4 excludes a redesign: the onboarding slide prices
("900 KD") and the super-app content that now leads only to empty states. Both are in §5.
iOS: the layout half of this pass was done on three iPhone simulators under R1-46.
- R1-45 · P2 · Done — Keyboard never covers a field or the main button; errors appear in place (Android; the keyboard also on iOS simulators, R1-46)
Owner ref:
2.docx §10
Keyboard — the static pass reached the wrong conclusion, and the device run
caught it. What the static pass established is true and still holds: no screen
sets resizeToAvoidBottomInset: false; the sticky CTA rides above the keyboard
because AppScaffold passes it as the Scaffold's bottomNavigationBar; every
screen with a text field has a scrollable body; and both bottom sheets containing
a text field are isScrollControlled and add viewInsets.bottom (the address
sheet was checked on the device and is perfect).
But "nothing overflows" is not "the button is reachable." On sign-in and
sign-up the button sits in the scroll flow, so the keyboard simply pushes it below
the fold — no overflow, no error, and invisible to any static check. It is exactly
what the audit reported, and it was still there.
Errors — this was real, and is fixed. The API answers a 422 with a message
per field, but every form handed the whole thing to showError, which shows only
the first message, in a toast, at the bottom of the screen — over the keyboard,
away from the field at fault, and gone a few seconds later. Only two screens in
the app used errorText at all, and there were no validators anywhere.
FieldErrors (core/widgets.dart) now splits a 422 across the fields a form
actually renders, so each message sits under its own input and stays there until
that input is corrected. Anything left over — a non-422, or a field the form
doesn't show — becomes a FormBanner inside the form, above the button: a live
region a screen reader reaches in reading order. Nothing is dropped. Wired into
sign-in, sign-up, phone sign-in (both stages) and support.
Also fixed: tapping Send on the support form with an empty subject or message
used to do nothing at all — no error, no disabled button, no sign the tap had
registered. It now says which field is missing.
Tests: test/field_errors_test.dart (9 tests, incl. "never swallows a message
the form has no field for" and the live-region check).
iOS: the keyboard half is seen on three iPhone simulators (R1-46) — sign-in and
the address sheet keep the field and the button above it. The error banner has not
been seen on iOS.
Still needs a device: a screen reader pass on the new banner needs a real reader.
- R1-46 · P2 · Done — Small, standard and large iPhones
Owner ref:
2.docx §10(Device Testing) Safe area, bottom navigation, sticky buttons, modals, keyboard.
No Mac and no iPhones needed. Codemagic already builds the app on cloud Macs, and those machines have Apple's iPhone simulators. A new workflow, "Yam3at iOS screen sizes (simulators)", runs the app on three sizes and saves screenshots as build artifacts: | Size | Simulator (falls back to an older model if absent) | |---|---| | Small, home button | iPhone SE (3rd generation) | | Standard, Dynamic Island | iPhone 16 → 15 → 14 | | Large, Dynamic Island | iPhone 16 Pro Max → 15 Pro Max → 14 Pro Max |
The screenshots are full-device, with the status bar, the keyboard and the notch or
Dynamic Island mask drawn in (simctl io screenshot --mask=black). Flutter's own
screenshot shows only what the app painted — which is exactly the thing that can't
reveal an overlap. It is kept as well, in each size's app-only/ folder, as a backup,
and each size's CAPTURES.txt says whether any step lacks a device capture or two
captures came out identical.
What it captures (11 screens): onboarding · home · home scrolled (navigation over content) · a category list · service detail (floating buttons by the Dynamic Island, the sticky "احجز الآن" bar on the home indicator) · account signed out · sign-in · a pop-up menu by the Dynamic Island · sign-in with the keyboard up · and, when a test account is configured, the booking screen's pay bar and the address sheet over the keyboard. It reads live data and never places an order or saves anything.
Proven before it was committed: the same walk ran end to end on the Android
emulator from a fresh install — all 11 steps, production unchanged afterwards. The
simulator-selection logic was tested against sample simctl output.
To run it: in Codemagic, start the "Yam3at iOS screen sizes (simulators)"
workflow; the screenshots appear under the build's artifacts. For the two signed-in
screens, first add an environment group screenshot_account with
SCREENSHOT_EMAIL and SCREENSHOT_PASSWORD as secure values and list it in the
workflow — the credentials are never written into the repository.
Runs so far — none has produced screenshots that can be reviewed yet:
1. Failed at once: the script fed simctl's output to Python the wrong way. Fixed,
and tested against the real invocation.
2. The iPhone 16 walk passed. The SE failed tapping the password field below the
fold; the Pro Max built and then hung until Codemagic's 60-minute limit, taking
every screenshot with it. Fixed: fields are scrolled into view before a tap; each
size has its own time limit and one retry on a wiped simulator; the zip is written
after every size.
3. All three sizes finished and 33 files came back — but all 33 were the same
frame, the black "Test finished." screen shown once the walk is over. The capture
ran from the driver's screenshot callback, and integration_test only calls that
after the whole walk. Nothing was reviewed from this run. Fixed: the walk now
asks the Mac to capture while each step is on screen, and waits until it has.
Proven on the Android emulator with the same request-and-answer handshake
(adb standing in for simctl): 9 of 9 captures, each matching its step, the
keyboard included — that run is also what found the slow-keyboard problem under
R1-26. The script itself was run against stand-in simctl and flutter tools,
normally and with one simulator hanging: 15 of 15 captures matched their steps, and
the hung size was wiped, retried and completed.
- Reviewed — the first usable set, 2026-09-16. All three sizes, 11 screens each, 33 device captures, every one a different image and matching its step. The run was reported as failed, wrongly: the iPhone 16 Pro Max's second attempt passed every step and the script still called it "hung again" — a race in its own timer, which marked 6 of 30 finished walks as hung when reproduced locally. Fixed; 0 of 30 after the fix, and a real hang is still stopped. Two of the sizes also needed their retry because the app never started on the first go ("The log reader failed unexpectedly"); the retry is what it is for, and both then passed.
What the screenshots show, per size (iPhone SE 3rd gen · iPhone 16 · iPhone 16 Pro Max): | Screen | Checked | Result | |---|---|---| | Onboarding | "تخطّي" and the language chip against the status bar; "التالي" against the home indicator | PASS, all three | | Home, and scrolled | Header below the Dynamic Island; the floating navigation above the home indicator; content clipped below the status bar when scrolled | PASS, all three | | Category list | Header, search, sort chips, cards | PASS. The last sort chip is cut at the edge before scrolling — the row has 16 px padding at both ends, so this is the scroll hint, not a clip | | Service detail | Floating back and save buttons clear of the island; the "احجز الآن" bar above the home indicator | PASS, all three | | Account, signed out | The "sign in to see this" state and navigation | PASS, all three | | Sign-in | Apple sign-in shown on iOS; chip clear of the island | PASS. On the SE and the iPhone 16 the Login button starts below the fold, under three sign-in buttons — it comes up as soon as a field is focused (next row) | | Language menu | The pop-up menu beside the Dynamic Island | PASS, all three | | Sign-in, keyboard up | Password field, "نسيت كلمة المرور", "تسجيل الدخول" and the sign-up row above the keyboard | PASS, all three — the R1-26 fix on iOS, including the slow-keyboard change | | Booking | The sticky "متابعة الدفع" bar above the home indicator | PASS for layout — and a defect, below | | Address sheet, keyboard up | Sheet fields and "حفظ" above the keyboard | PASS, all three. The panel over the keyboard is iOS's own first-keyboard tip on a new device |
Defect found and fixed — the booking screen opened on a full day. The Pro Max ran
later in the evening, when today had nothing left. The screen still opened on today:
a greyed-out day, selected, over "لا توجد مواعيد متاحة في هذا اليوم". The date strip
already refuses to let anyone pick a full day; opening on one contradicted it. It
now opens on the first day with something free (tomorrow, that evening — confirmed
against the live API for all eight services), never moves off a day the customer
picked, and ignores a late answer for a day it has left.
test/booking_opening_day_test.dart (4 widget tests against a stand-in server) and
test/first_bookable_day_test.dart (6). Each part of the fix was removed in turn to
check a test fails without it: all three were caught.
Not covered by these screenshots: real Face ID, real push delivery, real performance, and the booking and payment flows beyond their first screen. What simulators don't cover: real Face ID hardware, real push delivery and real performance. For those, one session on real iPhones — your TestFlight build on a phone, or a paid device cloud such as BrowserStack or AWS Device Farm — before launch.
Step 10 — Full regression¶
- R1-47 · Gate — Run §3 on a clean build, on a fresh install
Owner ref:
2.docx(Full Regression Test, Definition of Ready for Launch) Run 1 done, 2026-09-16, on Android — a release APK on a fresh install against live production, then a second pass for the network states. 27 of 28 rows PASS, 6 of them only after a fix the run itself found. The one row that is not a full pass is row 25, which wants a second account to prove Back cannot show the previous user's data; Back was shown to leave the app entirely, which is the stronger result. The filled sheet is §3. This gate stays open for one reason only: iOS. R1-46 has now checked the layout of 11 screens on three iPhone simulators, but the rows of §3 are flows — booking, paying, push, Face ID — and none has been run on an iPhone. That needs the TestFlight build on a phone.
3. Regression sheet¶
The owners' reference path, one row per step. Record every run against a build number.
Run 1 — 2026-09-16. Release APK (flutter build apk --release, API_URL=https://api.yam3at.com/api/v1),
fresh install on Android 15 / API 35, 1080×2400 @420dpi, Arabic locale, against live production.
Account: demo.customer@yam3at.com. Screenshots kept outside the repository.
Two runs were needed: run 1a found six defects, they were fixed, and the rows they touched were re-run on the rebuilt APK. Rows marked PASS (after fix) failed on the first build.
| # | Step | Expected | Result | Evidence |
|---|---|---|---|---|
| 1 | Register / Login | Session starts; survives an app restart | PASS | Survived a reinstall, went straight to home |
| 2 | Home | No test content; images or the official placeholder | PASS (after fix) | Cards drew a grey picture glyph, not the placeholder — R1-41 |
| 3 | Search | Full and partial names find the service; results kept on Back | PASS | "Flow" (English, partial) found زهور المناسبات in an Arabic UI |
| 4 | Category | Only populated categories; right services in each | PASS | 8 stocked categories; empty domains show an empty state with an RFQ path |
| 5 | Service | Package, duration, price, availability consistent | PASS | 140.000 / 120 دقيقة carried to the sticky bar |
| 6 | Provider | Rating, reviews, verification belong to this provider | PASS | 0 reviews shows "جديد", never "0.0 ★" |
| 7 | Package | Name, duration, price carried to booking | PASS | تنسيق الطاولات · 140.000 د.ك on every screen after |
| 8 | Favorite | Service and provider: state, list and counter agree | PASS | Heart filled; account counter went 0 → 1 |
| 9 | Booking | Summary matches the chosen service, package, provider | PASS | — |
| 10 | Date | Unavailable days can't be chosen | PASS | Friday 18 greyed out and unselectable |
| 11 | Time | Slots refresh with the date; old slot cleared | PASS | 12 slots 10:00–22:00; none on Friday |
| 12 | Add address | Required fields validated; saved address selected | PASS | "أدخل العنوان" shown under the field, in red; saved address auto-selected |
| 13 | Coupon | Valid / invalid / expired / used each behave and read correctly | PASS (after fix) | "هذا الرمز غير معروف لدينا." was correct but shown as a toast; now under the field |
| 14 | Payment | Same total as booking; KNET and card unselectable | PASS | Both greyed with a "قريبًا" badge; cash preselected |
| 15 | Confirm | Double tap, slow network, retry, timeout, Back → one order | PASS (partial) | Three rapid taps → one order, one idempotency key. Slow network not simulated |
| 16 | Success | Shown only after server success; copy matches status | PASS | "تم إرسال طلبك", not "confirmed" |
| 17 | My Bookings | In the right tab, with the same eleven facts | PASS | القادمة tab, one entry, every fact matching |
| 18 | Tracking | Stage matches the real status, same label as everywhere | PASS (after fix) | Correct; the header had been scrolling under the status bar |
| 19 | Messages | Conversation is about this booking, with its provider | PASS (after fix) | Row carries order_id and vendor_id; chat had been unreachable at this status |
| 20 | Notifications | Arabic, correct status, tap opens that booking | PASS (after fix) | Said "تم تأكيد طلبك" on an unaccepted order; now "تم إرسال طلبك، بانتظار قبول مقدّم الخدمة" |
| 21 | My Events | Create → save → list → open; empty state gone | PASS | Created → listed → empty state gone → form cleared |
| 22 | Planning assistant | Send, loading, response, network error, retry | PASS (partial) | Reachable and responds; error/retry not forced |
| 23 | Yam3at AI | Send, loading, plan, network error, retry | PASS (partial) | Real plan from live data, 600.000 د.ك. See the note below on plan breadth |
| 24 | Account | Real figures; every tile opens the right screen | PASS (after fix) | "Bookings: 0" with a live order — R1-33 |
| 25 | Logout / Login | Back can't show the previous user's data | PASS | Back exits to the launcher; relaunch is a guest |
| 26 | Provider declines | Customer notified; details say the provider declined | PASS | "اعتذر مقدّم الخدمة عن هذا الطلب فتم إلغاؤه. يمكنك الحجز مع مقدّم خدمة آخر." |
| 27 | Nobody accepts | Order cancelled after the window; both sides notified | PASS | provider_timeout; slot 14:00 back on sale; Arabic notice to the customer |
| 28 | Abandoned checkout | Slot back on sale after 15 minutes; order not in My Bookings | PASS | Production's own 5-minute sweep caught it: checkout_expired, slot 16:00 free again |
Not covered by this run, and why:
- Airplane mode, a throttled network and a forced server error (rows 15, 22, 23, and R1-05). The emulator can simulate these; doing it properly means driving each one and watching the retry path, which is the next run, not this one.
- A second customer account for row 25's "Back can't show the previous user's data". Back was proven to leave the app entirely, which is the stronger result, but two accounts would prove it outright.
- iOS (R1-46): layout checked on three iPhone simulators — 11 screens each, all PASS, one booking defect found and fixed. The flows in §3 have still not been run on an iPhone.
- Delete account and export data (R1-35): both screens exist and are reachable; neither was run, because the account under test is the demo account handed to the owners. Worth a run on a throwaway account.
The eleven facts (rows 9–20): service, package, provider, price, date, time, address, coupon, payment method, booking reference, status. All eleven held across all twelve screens — this is R1-18.
The eleven facts (rows 9–20): service, package, provider, price, date, time, address, coupon, payment method, booking reference, status.
4. Not in this round¶
The owners' exclusions, kept here so they stay excluded:
- Full redesign, or re-engineering the home screen
- New filters, or any new feature — except the two approved on 16 September: service favorites (D5) and conversations linked to bookings (D6)
- Event OS, or linking My Events to bookings automatically
- Rebuilding the AI, or merging Yam3at AI with the planning assistant
- Wallet payment, or payment methods that aren't ready
- Changing the bottom navigation
The owners also note that the home screen and navigation have not yet had their own screen-by-screen review — expect a second round for those.
5. Issues against our own standards, not in the owners' list¶
Raised so they are decided, not so they expand this round.
- Order numbers are sequential (
YM-2026-000001,…002) — externally visible, so they reveal order volume and invite enumeration. Recommended: schedule for the next round. - Floating-point money — fixed in the booking flow and in the order, coupon
and wallet paths (R1-08). About 30 other places still format money through a
float — reports, dashboards, affiliate and AI-quote totals. None of them feeds a
charge. Recommended: move them onto
Money::roundnext round. - Idempotency keys on mutating endpoints — done (R1-02).
- Onboarding prices are in the wrong format. The third onboarding slide shows "900 KD", "650 KD" and a total of "2,150 KD". Everywhere else in the app money is KWD to three decimals with the Arabic symbol — "450.000 د.ك". The onboarding card is a mock illustration, but it is the first money a new customer ever sees, and it teaches the wrong format. Recommended: fix the slide's copy next round.
- Onboarding, the home domain row and the AI suggestions still sell the super-app. Slide 1 promises "كل الخدمات… في مكان واحد" over car, home, tools, briefcase and brush icons; the home row offers المنزل and السيارات; the AI's quick suggestions include "أريد تنظيف شامل للمنزل" and "أريد غسيل سيارتي". After the R1-39/R1-40 cleanup, only events categories have any stock, so all of these lead to an empty state. The empty state itself is well made — it offers a "request a quote" path rather than dead-ending — so this is a positioning decision, not a bug. But an events-first product currently opens by promising car washes. Recommended: the owners decide, since §4 excludes a redesign this round.
- The AI plans thinly. Asked to plan a party for 80 people it returned a single service (catering) when venue, decor, entertainment and flowers were all in the catalogue. Correct, but not a plan. Rebuilding the AI is excluded by §4, so this is raised for the next round.
- The app runs on Kuwait time. Timestamps are still stored as absolute UTC instants, but "today", slot cut-offs and the order windows follow the Kuwait clock — a deliberate deviation, now written down as ADR-0001, with the trade-off and a dated review rather than a silent renewal.
6. What could not be verified from here¶
- The regression flows on iOS. Everything below was verified on Android 15 / API 35, 1080×2400 via a release APK on a fresh install against live production. On iOS only the layout has been seen, on simulators (R1-46): no flow has been run on an iPhone.
- Airplane mode, throttled networks and forced server errors — rows 15, 22 and 23 of the sheet, and the rest of R1-05.
- Face ID itself (R1-34) — the lock was verified on Android with a simulated fingerprint, which exercises the same code. The Face ID prompt and its wording need an iPhone.
- Delete account and export data (R1-35) — not run, because the account under test is the demo account being handed over.
- ~~The Codemagic build settings~~ — this was wrong:
codemagic.yamlis in the repository and is now checked by CI (R1-43). - Who changed
PAYMENT_DRIVER, and when — environment changes aren't audited. - Every "Verify" item above — each needs a device run, which is the first task of each step.