The (individual) route group and (auth) individual-auth/forgot-password screens — the only account type currently reachable in the mobile app (Company/Office are gated, see Mobile — Shared). Covers login, signup, Google/Apple sign-in, OTP, password recovery, the identity switcher, and favorites.
Favorites persist correctly across the property lifecycle
P0
As an individual user, I want to save properties I'm interested in and find them again easily later, so that I don't lose track of listings while I'm comparing options across multiple browsing sessions.
Verify the Favorites system (POST /favorites/:propertyId toggle, GET /favorites, GET /favorites/ids, GET /favorites/check/:propertyId, useFavoritesStore) behaves correctly end to end, so that a saved property reliably shows as saved everywhere in the app and survives logout/login.
AC-01Toggling favorite on Property Detail updates the heart icon instantly (optimistic) and reflects the server state after refresh.
AC-02The Favorites tab (more)/favorites shows exactly the set of properties the user has favorited, in a stable order.
AC-03Un-favoriting from the Favorites list removes it from that list without requiring a manual refresh.
AC-04Favorite state is per-user: switching identities (see mobile-identity-switch in Mobile — Shared) does not leak another identity's favorites.
Test Scenarios
TC-01Favorite a property from Search results, then verify it appears favorited on Property Detail and in the Favorites tab.
TC-02Favorite 15+ properties and confirm the Favorites list paginates/scrolls without duplication or drops.
TC-03Favorite a property, then have it deleted/archived by its owner — confirm the Favorites tab handles the now-missing property gracefully (no crash, sensible empty/removed state).
TC-04Toggle favorite rapidly (double-tap) and confirm no duplicate POSTs desync the heart icon from server state.
Home feed favoriting is broken on three surfaces — Properties cards, the details sheet, and Offices' fake toggle
P0
As an individual user browsing the home feed, I want to tap the heart icon on any listing — whether I'm scrolling the feed or looking at the expanded details sheet — to save it instantly, so that I can bookmark things wherever I encounter them instead of hunting for the one screen where it actually works.
Home-feed favoriting is broken across THREE surfaces, each in a slightly different way — track them as distinct findings, not one vague 'favorites are broken' ticket. On the عقارات (Properties) tab, PropertyCard.tsx's and PropertyListItem.tsx's heart buttons both call onPress={onFavorite} — but apps/mobile/src/features/individual/components/home/sections/HomeProperties.tsx never passes an onFavorite prop to either component, in Card view or List view. The same file's <PropertyDetailsSheet> instance — the 'عرض تفاصيل' bottom sheet opened from any of those cards — has the identical gap: PropertyDetailsSheet.tsx accepts onFavorite/onShare props and wires its own heart/share buttons to them correctly, but HomeProperties.tsx only ever passes property, onClose, and onViewProperty when rendering it. Three tap targets, one missing prop, zero functionality, and none of them show any disabled state to hint that. On the مكاتب عقارية (Offices) tab, HomeOffices.tsx DOES wire onToggleFavorite — but only into a local `favoriteOverrides` useState dictionary with no API call behind it, so the heart visually fills in and then silently reverts the moment the screen unmounts. That Offices variant is the most dangerous of the three because it's the only one that will pass a five-second smoke test.
AC-01AC1 (Properties cards): tapping the heart on any card in Home > عقارات, in both Card and List view, produces zero visible change and the property does not appear in the Favorites tab afterward.
AC-02AC2 (Properties cards): tapping the share icon on the same cards produces zero effect — no native share sheet opens, since onShare is equally unwired.
AC-03AC3 (Details sheet): opening 'عرض تفاصيل' from any Home-feed card and tapping the heart or share icon inside the sheet's header produces zero effect — same root cause (HomeProperties.tsx never passing onFavorite/onShare), different tap target.
AC-04AC4 (Offices): tapping the heart on an office card visibly fills red immediately (local state), but relaunching the app or navigating away and back reverts it to the server's real isFavorite value — because favoriteOverrides never reaches an API call.
AC-05AC5: this is scoped specifically to Home-originated surfaces — Search results' and Property Detail's own (full-screen, not the sheet) heart icons are correctly wired (see mobile-favorites) and must not be lumped into the same fix or the same regression check.
AC-06AC6: whatever fix lands must route all three Home surfaces through the same real POST /favorites/:propertyId (or an equivalent office-favoriting endpoint if one exists) and must correctly trigger the guest-auth-sheet for logged-out users rather than silently no-op or silently fake-toggle for them too.
Test Scenarios
TC-01TC1: As a logged-in user on Home > عقارات > بيع, tap the heart on the first 3 cards in Card view, then switch to List view and repeat on the same 3 cards — confirm neither view produces any visual or state change, and none of the 6 taps result in a new Favorites entry.
TC-02TC2: From that same tab, tap 'عرض تفاصيل' on any card to open the details sheet, then tap its heart and share icons — confirm both are equally inert, and that the sheet's own 'عرض العقار' button still correctly navigates to the full property page (proving onViewProperty IS wired even though onFavorite/onShare aren't).
TC-03TC3: As a logged-in user on Home > مكاتب عقارية, tap the heart on an office card and confirm it visually fills red. Force-quit and relaunch the app, return to the same tab, and confirm the heart has reverted to unfilled — proving the toggle never left the device.
TC-04TC4: As a guest (logged out), repeat TC1–TC3 and confirm whether the guest-auth sheet opens for any of the three surfaces (per mobile-guest-guard's documented behavior for Favorites) — if it opens for none of them, that's further proof these taps aren't routed through the shared guarded-action path at all.
TC-05TC5: Tap the heart on the exact same property from its own full-screen Property Detail page (not the Home card or its sheet) and confirm it DOES work there — screenshot all four side by side, since this contrast is what makes the Home-originated gaps easy to miss in a casual pass and trivial to prove once you know where to look.
Home feed's primary tabs, Sale/Rent/Exchange filters, and Card/List toggle each refetch and render correctly
P1
As an individual user, I want to switch between Properties and Offices, filter Properties by Sale/Rent/Exchange, and toggle between card and list layouts, so that I can browse the way that suits me without the feed ever showing stale or mismatched results from whatever I was looking at a moment ago.
Verify HomeScreen's tab/filter state machine (activeTab: 'properties'|'offices', activePropFilter: 'all'|'sale'|'rent'|'exchange', viewMode: 'card'|'list') correctly re-triggers HomeProperties' fetch effect on every filter change and never leaks stale data from the previous filter into the newly-rendered list — this is exactly the class of state-transition bug that sails through a slow, deliberate manual pass and only shows up under real usage patterns like a user impatiently tapping between tabs.
AC-01AC1: switching between بيع/إيجار/بدل triggers a fresh fetch (visible via the loading skeleton) and the list fully replaces — never appends to — the previous filter's results.
AC-02AC2: switching to مكاتب عقارية swaps to HomeOffices entirely and HomeFilterPills correctly returns null, hiding the Sale/Rent/Exchange pills — confirm no orphaned filter state bleeds back in when returning to عقارات.
AC-03AC3: toggling Card ↔ List view preserves the currently active Sale/Rent/Exchange filter and the already-loaded dataset rather than silently resetting to page 1 of 'all'.
AC-04AC4: infinite scroll (onEndReached, page-based, deduplicated by item.id) continues to respect whichever filter is active after a filter change mid-scroll — no cross-filter duplicate or missing items.
Test Scenarios
TC-01TC1: Rapidly tap بيع → إيجار → بدل → بيع in quick succession and confirm the final rendered list matches بيع results only — not a race-condition mix of whichever fetch happened to resolve last.
TC-02TC2: Scroll in بيع far enough to trigger infinite scroll (page 2+), switch to إيجار, then switch back to بيع — confirm بيع resets cleanly to page 1 rather than resuming from the stale page-2 offset.
TC-03TC3: Switch to مكاتب عقارية, confirm the filter pills disappear, switch back to عقارات, and record whether your previous Sale/Rent/Exchange selection persisted or reset to a default — document whichever it actually does so this isn't re-litigated every pass.
TC-04TC4: Load 20+ items in Card view, toggle to List view mid-scroll, and confirm the same dataset renders immediately in List view with no extra network fetch or flash of the empty state.
List-view property income silently defaults to a hardcoded 150,000 د.ك when the API sends no value
P2
As an individual user comparing properties in list view, I want the 'مدخول العقار' figure to always reflect that specific property's real rental-income data, so that I can actually trust it when deciding which property is the better investment.
Flag that PropertyListItem.tsx declares `propertyIncome = 150_000` as a destructured default parameter — meaning any listing where the API's item.propertyIncome comes back undefined renders 'مدخول العقار 150,000 د.ك' in the exact same confident green styling as a genuine figure, with zero visual distinction between 'this property really yields 150,000' and 'we have no income data for this property at all.' On a financial field specifically, a senior QA pass can't stop at 'a number displays' — the number's provenance is the actual thing under test, and right now the UI has no way to tell a real value from a fabricated placeholder that happens to look completely plausible. Worth noting for the fix: PropertyDetailsSheet.tsx renders the identical field correctly — its `property.propertyIncome !== undefined && property.propertyIncome !== null` guard has no default-parameter fallback poisoning it, so the details sheet is the reference implementation to copy, not a second place that needs the same fix.
AC-01AC1: confirm whether any of the '150,000 د.ك' figures visible in the live feed today are coincidentally real, or are the fallback default masking missing data — this is only answerable via a database cross-check, never from the UI alone.
AC-02AC2: the second card in the بيع frame shows a genuine '0 د.ك' income, proving the field renders real zero values correctly — which means every card showing exactly 150,000 is now suspect and needs the same verification, not a free pass because 'it's not zero so it must be real.'
AC-03AC3: get a product decision on whether missing income data should hide the row entirely (matching the existing `propertyIncome !== undefined && propertyIncome !== null` guard's evident intent, which the default parameter currently defeats by never letting the value reach the guard as undefined) rather than fabricate a specific-looking number.
AC-04AC4: confirm the fix only needs to touch PropertyListItem.tsx — PropertyDetailsSheet.tsx already implements this field correctly and should not be touched or re-tested as if it shared the bug.
Test Scenarios
TC-01TC1: cross-reference 5+ properties currently showing exactly '150,000 د.ك' against their database record — if any has a null/undefined propertyIncome column, that's confirmed evidence real users are being shown fabricated financial data, not a hypothetical.
TC-02TC2: create or find a test property with propertyIncome explicitly set to 0 and confirm it renders '0 د.ك', not the 150,000 fallback — the بيع frame already demonstrates this works for one such card, so use it as the known-good baseline.
TC-03TC3: file this as a data-integrity bug rather than a cosmetic one — it's a specific financial figure a buyer could reasonably factor into a real offer.
Property details sheet never shows '/شهرياً' for rentals — a rent price and a sale price render identically
P1
As an individual user viewing a rental listing's details, I want the price to clearly say it's a monthly rent, so that I never mistake it for the total sale price of the property and misjudge whether I can actually afford it.
Flag that PropertyDetailsSheet.tsx's formattedPrice logic renders the exact same markup — `{property.price.toLocaleString()} د.ك` — regardless of property.type, with no branch for 'RENT' at all, even though the Figma frame for a rental listing explicitly appends '/ شهرياً' (per month) after the price. This isn't a cosmetic omission: showing '150,000 د.ك' with no qualifier on a RENT-type listing is actively misleading — a real user has to infer from context (or the category label alone) whether that figure is a lump sum or a monthly recurring cost, and 150,000 KD/month is an absurd number that could easily be misread as a data-entry error rather than correctly understood as '150,000 KD total, paid monthly' being wrong on either reading. A senior QA pass treats price-unit ambiguity on a financial figure as a functional bug, not a copy nit.
AC-01AC1: confirm property.type is available on PropertyDetailsDto at render time (it is — 'SALE'|'RENT'|'EXCHANGE') and that formattedPrice currently ignores it entirely.
AC-02AC2: a RENT-type listing's details sheet must render a per-month qualifier (matching the Figma frame's '/ شهرياً') distinguishing it from a SALE-type listing's one-time price — get explicit confirmation from design on the exact expected string before filing the fix, don't guess the copy.
AC-03AC3: confirm the equivalent price displays elsewhere in the app (Home-feed cards, Search results, full Property Detail page) either already handle this correctly or have the identical gap — this bug may not be isolated to just the details sheet.
Test Scenarios
TC-01TC1: Open the details sheet for a known RENT-type listing and a known SALE-type listing side by side (screenshot both) and confirm the price row is visually and textually identical between them today — that's the bug, reproduced.
TC-02TC2: Check PropertyCard.tsx and PropertyListItem.tsx (the Home-feed card components) for the same formattedPrice pattern — if they also lack a rent qualifier, this is a shared-pattern bug, not a one-off in the sheet, and should be fixed in one place all three consume rather than patched three times.
TC-03TC3: Once fixed, verify EXCHANGE-type listings are unaffected by whatever branch gets added — they use a different price/comparison model entirely (see mobile-property-details-exchange-fields-unrendered) and shouldn't accidentally inherit a rent suffix.
Exchange listings' defining data (طلب/مع years) exists on the DTO but is rendered nowhere — not the details sheet, not the Home card
P0
As an individual user considering a property exchange, I want to see exactly what build year is being asked for and what year the offered property is from — right on the feed card, and again in full detail if I open it — so that I can judge whether the swap is actually fair before I ever reach out to the other party.
Flag this as the sharpest finding in this whole batch, and it just got sharper: PropertyDetailsDto (apps/mobile/src/types/property.ts) already declares `requestYear?: number` and `requiredYear?: number` — almost certainly the backing fields for the design's 'طلب 2021' / 'مع 2023' rows on Exchange listings — but neither PropertyDetailsSheet.tsx nor PropertyCard.tsx ever reads or renders property.requestYear or property.requiredYear anywhere in their JSX. The Exchange Home-feed frame shows طلب/مع rendered directly on the card itself, in place of beds/baths — but PropertyCard.tsx's specs row only ever renders 'شقة {type} • {area} م²', beds, and baths, with no branch for type === 'EXCHANGE' at all. So this isn't a details-sheet-only gap anymore — it's the same two DTO fields missing from BOTH places a user would ever see them, meaning an Exchange listing's one defining trait is currently invisible everywhere in the app except the raw database.
AC-01AC1: open the details sheet for a real EXCHANGE-type property and confirm today's actual rendered output — expected, per the code read, is either 'السعر غير معلن' or a blank price area, with no طلب/مع rows anywhere, contradicting the Figma frame entirely.
AC-02AC2: view an EXCHANGE-type listing in the Home feed's Card view and confirm the specs row shows beds/baths (or omits them, since Exchange listings may have none) instead of the طلب/مع rows the design specifies — same root gap, second surface.
AC-03AC3: confirm property.requestYear and property.requiredYear are in fact populated by the API for real Exchange listings (not just present on the TypeScript type) — if the backend never populates them either, this is a two-sided gap, not just a frontend rendering miss.
AC-04AC4: this must be filed as a single P0 feature-completeness bug against the Exchange flow covering both surfaces, not two separate 'details sheet polish' and 'card polish' tickets that could ship independently and leave one half fixed.
AC-05AC5: confirm whether rentConditions/additionalExpenses (also unrendered in the details sheet) are meant to show for RENT-type listings specifically, and scope a second, lower-priority ticket for those if so — don't let them get lost once the Exchange fix ships.
Test Scenarios
TC-01TC1: Find or create a real Exchange-type listing with requestYear and requiredYear populated, open its details sheet, and document exactly what renders in place of the price row today.
TC-02TC2: View that same listing in the Home feed's Card view (بدل tab) and document what the specs row shows in place of طلب/مع — compare directly against the Exchange Home-card Figma frame.
TC-03TC3: Compare both actual outputs against the three Exchange Figma frames (two details-sheet variants + the card view) and list every row the design shows that's missing from each — expect at minimum the طلب/مع rows in both places.
TC-04TC4: Query the database for a sample of real Exchange listings and confirm requestYear/requiredYear are actually being saved by the create-listing flow (useListingStore's ExchangePropertyTab, per mobile-add-listing in Mobile — Shared) — if they're null in the DB too, the fix needs to start at listing creation, not just at either rendering surface.
TC-05TC5: Once a fix ships to both surfaces, re-verify formattedPrice's 'السعر غير معلن' fallback no longer fires for Exchange listings that do have requestYear/requiredYear data — both the card and the sheet should show the exchange comparison instead of a price-not-disclosed message that isn't even the relevant gap for this listing type.
Home Card view: badge and heart are swapped left/right, the share icon and income row are missing entirely
P1
As an individual user browsing the home feed in card view, I want the featured badge, favorite and share icons, and income figure to appear exactly where I'd expect and to actually be present, so that the app feels finished and I can compare listings at a glance the way the design intends.
Flag three concrete, code-verifiable mismatches between PropertyCard.tsx (the Home feed's Card view) and its Figma frame, none of which require guessing — they're each a direct line-by-line contradiction: (1) the 'مميز' badge is styled `absolute right-3 top-3` in code but appears on the LEFT edge of the image in the design; the heart icon is styled `absolute left-3 top-3` in code but appears on the RIGHT, paired with a share icon, in the design — the two elements are positioned as exact mirror images of the design. (2) The code's heart-icon container comment literally reads '/* Absolute Top Left: Actions (Heart & Share) */' but only a single heart TouchableOpacity is actually implemented underneath it — there's no share button in Card view at all, despite the comment writer's evident intent and despite List view (PropertyListItem.tsx) having one. (3) PropertyCardProps has no propertyIncome field and the JSX never renders a 'مدخول العقار' row anywhere, even though every card in both Figma frames shows one directly under the specs row, and PropertyListItem.tsx (List view) already implements this exact row correctly — meaning there's a working reference implementation sitting one file away that Card view simply never adopted.
AC-01AC1: the featured badge renders on the image's right edge and the heart icon renders alone on the left edge in the live app today — both are mirrored from where the Figma frame places them.
AC-02AC2: tapping the empty space in Card view's top-left corner where the design's share icon should be does nothing, because no such element exists in the DOM/view tree to tap — this is a missing element, not an unwired handler like the heart icon (see mobile-home-feed-actions-unwired for that separate issue).
AC-03AC3: no property card in Card view shows a 'مدخول العقار' figure under any circumstance, regardless of whether the underlying property has income data — confirm this is a rendering omission, not a data-availability issue, by cross-checking the identical property in List view (which does show it, per mobile-home-property-income-fallback).
AC-04AC4: get explicit confirmation from design/product on which layout is authoritative — since PropertyListItem.tsx (List view) already gets the badge/heart-share positioning and income row right, the fastest correct fix is likely to bring Card view's layout in line with List view's, not to redesign either from scratch.
Test Scenarios
TC-01TC1: Screenshot a Home-feed card in Card view next to its Figma frame at the same zoom level and confirm the badge and heart/share swap is visually obvious, not a subtle pixel-level nit.
TC-02TC2: Confirm the same property viewed in List view DOES show a correctly-positioned income row (per PropertyListItem.tsx), establishing List view as the working reference implementation to copy from rather than building the Card view fix from scratch.
TC-03TC3: After any fix, re-run mobile-home-feed-actions-unwired's Card-view test cases too — adding a share button here means there will be a new, similarly-unwired onShare tap target to verify, not just a visual fix.
TC-04TC4: Confirm badge/heart positioning on a tablet/iPad layout (see mobile-ipad-layout in Mobile — Shared) in case the swap is somehow breakpoint-specific rather than present at every size.
Individual login: remember-me checkbox currently has no effect (known gap)
P1
As an individual user, I want to log in with either my email or phone, optionally stay signed in with 'remember me,' and quickly get back into an account I've already used on this device, so that returning to the app is fast and I never lose access to identities I've already set up.
Verify the Individual Login screen (identifier + password, 'تذكري' remember-me checkbox, 'هل نسيت كلمة المرور؟' link, 'المتابعة بحساب محفوظ' saved-account entry point) behaves correctly end to end, while flagging that the rememberMe value captured in IndividualLoginForm is passed to onLogin but never actually used — session persistence/token lifetime doesn't currently change based on it.
AC-01loginSchema validation (identifier: email-or-phone string, password: 6-char min) blocks submission with inline errors before any network call.
AC-02Toggling 'تذكري' visibly checks/unchecks the box, but confirm — and document — that it has no observable effect on session length or biometric re-prompt behavior today.
AC-03'هل نسيت كلمة المرور؟' navigates to the forgot-password flow (see mobile-forgot-password-inline-error).
AC-04'المتابعة بحساب محفوظ' opens ProfileBottomSheet (the saved-identities list), not a dead button.
Test Scenarios
TC-01Log in with 'تذكري' checked, force-quit the app, relaunch after a day, and confirm whether the session persists any differently than with it unchecked — if there's no difference, that confirms the no-op and should be flagged to product as either wire it up or remove it.
TC-02Submit the login form empty, with only identifier filled, and with only password filled — confirm inline validation catches each case before hitting the network.
TC-03Tap 'المتابعة بحساب محفوظ' with zero saved identities on the device and confirm the bottom sheet handles the empty case gracefully rather than showing a blank sheet.
TC-04Log in with a correct identifier and wrong password 3 times in a row and confirm the error Alert is clear each time (message comes from the backend, not a generic fallback, unless the network genuinely fails).
Individual signup: single phone-or-email identifier field now matches design
P1
As an individual user, I want to create an account with either just my email or just my phone number, whichever I prefer, without being forced to fill in both, so that I can start using the app immediately with the identifier I actually trust the app with.
Verify the Individual Signup screen's single 'رقم الهاتف أو البريد الالكتروني' identifier field (previously two separate email + optional-phone fields, now merged to match both the Figma frame and the Login screen's existing identifier field) against signupSchema (identifier required, matching either the email regex or the Kuwaiti phone regex ^(?:+?965)?[569]\d{7}$, password 8-char min, confirmPassword must match), and confirm the backend's RegisterIndividualDto/registerIndividual now accepts the same single `identifier` field and classifies it server-side via the shared isEmail/isPhone helpers — so this closes out the design/code drift the previous version of this story flagged (design showed one required field; code had two, one optional).
AC-01The signup screen shows exactly one identifier field labeled 'رقم الهاتف أو البريد الالكتروني', with no separate phone field — matching the Figma frame.
AC-02Submitting an email-shaped identifier (e.g. ali@example.com) succeeds and is stored as the account's email.
AC-03Submitting a Kuwaiti-phone-shaped identifier (e.g. 51234567, +96551234567, 96551234567) succeeds and is stored as the account's phone.
AC-04Submitting a value that is neither a valid email nor a valid Kuwaiti phone is rejected inline before any network call, both on the mobile form (signupSchema) and — as a defense-in-depth check — by the backend's IsEmailOrPhone validator if the client-side check were ever bypassed.
AC-05Password under 8 characters, or a confirmPassword mismatch, blocks submission with inline errors.
AC-06Submitting an email-shaped identifier skips the OTP step entirely and logs the user straight in (see mobile-signup-otp-skip-logic) — submitting a phone-shaped identifier routes to the OTP step instead.
Test Scenarios
TC-01Sign up with a valid email identifier + password + confirmPassword — confirm the account is created with that email and the OTP step is skipped (goes straight to success).
TC-02Sign up with a value that fails both the email and Kuwaiti-phone regex (e.g. a random string, or a non-Kuwaiti phone number) and confirm inline rejection, not a failed network call.
TC-03Sign up with a valid Kuwaiti phone in the +965XXXXXXXX, 965XXXXXXXX, and bare XXXXXXXX forms and confirm all three are accepted and route to the OTP step.
TC-04Attempt to sign up twice with the same identifier (once as the only field) and confirm the second attempt is rejected as 'البريد الإلكتروني مسجل بالفعل' / 'رقم الهاتف مسجل بالفعل' rather than silently succeeding or leaving an unusable half-created account.
Signup only sends an OTP for a phone-shaped identifier — no email verification exists either way
P0
As an individual user signing up with just my email, I want to be let into the app right away without waiting on a verification code, so that there's no friction between deciding to join and actually browsing properties.
Verify that IndividualAuthScreen's onSignup only advances to the OTP step (IndividualSignupOtpStep) when the identifier entered is phone-shaped (checked client-side via isPhoneIdentifier, mirroring the backend's own isEmail/isPhone classification of the same value) — if the identifier is email-shaped, the app immediately calls POST /auth/login with the new credentials and shows the success sheet with zero verification of any kind. Also confirm the previously-fixed 'no Identity row after registration' bug stays fixed here specifically: this immediate post-signup POST /auth/login (and the phone path's POST /auth/register/verify-otp, which also internally logs the user in) must succeed on the very first attempt, since registerIndividual now creates the User + IndividualProfile + Identity together in one transaction.
AC-01Signing up with an email-shaped identifier + password results in an immediately-active, logged-in account with no OTP and no email-verification link ever sent.
AC-02Signing up with a phone-shaped identifier sends an SMS OTP via POST /auth/register/verify-otp before the account becomes usable.
AC-03Neither path ever surfaces an 'Identity Unlinked' / 'لا توجد هوية مرتبطة بهذا الحساب' error on the very first login attempt right after signup — this was a real regression previously (registration created the User but not the Identity row auth/login depends on) and must not recur.
AC-04Confirm with product/security whether 'email-only = instantly active, unverified account' is the intended trust model, since it means email ownership is never actually confirmed in that path.
AC-05The Success bottom sheet's copy is consistent for both paths (see mobile-auth-success-copy for a text-mismatch nit already spotted here).
Test Scenarios
TC-01Sign up with a disposable/unowned email identifier — confirm the account is fully usable immediately, with no confirmation email ever required to be clicked, and no error is shown on the automatic post-signup login.
TC-02Sign up with a valid phone identifier, then abandon the flow before entering the OTP — confirm the account is NOT usable yet (i.e. verification is actually gating something in that path).
TC-03Sign up with a phone identifier and complete the OTP step — confirm the resulting login succeeds on the first attempt with no 'Identity Unlinked' error, and that the returned activeIdentity/identities are populated (not empty).
TC-04Attempt to sign up twice in a row with the same identifier and confirm the second attempt is cleanly rejected as already-registered rather than creating a duplicate/ghost account.
Google/Apple sign-in buttons on login and signup — new, not yet configured with real credentials
P1
As an individual user, I want to sign in or create an account with my existing Google or Apple account in one tap, so that I don't have to invent and remember another password just to try the app.
Verify the new SocialAuthButtons component (the 'أو قم بالتسجيل باستخدام' divider + Google icon button + Apple's native AppleAuthenticationButton, rendered on both IndividualLoginForm and IndividualSignupForm) correctly drives POST /auth/google and POST /auth/apple, and — critically for this environment — confirm whether GOOGLE_CLIENT_ID/APPLE_CLIENT_ID are actually configured on the target API environment before treating any failure here as a bug: both services are designed to fail closed with a clear 'غير مُفعّل حالياً' (not currently enabled) error rather than silently succeeding when unconfigured, and the buttons themselves are native modules that do not work inside Expo Go — they require a build produced by `eas build`/`expo prebuild` with the Google/Apple config plugins applied.
AC-01Both buttons render on the Login screen and on step 1 of Signup, below the existing submit button, matching the divider + two-button layout in the mockups shared for this update.
AC-02The Apple button only renders on iOS (Android has no Apple Sign-In) — confirm Android shows the Google button alone with no layout gap or crash.
AC-03Tapping Google opens the native Google account picker; tapping Apple opens the native Apple Sign-In sheet — cancelling either flow returns silently to the auth screen with no error alert (this is a deliberate no-op, not a bug).
AC-04On successful sign-in via either provider, the user is logged in and routed to the identity home screen exactly like a normal email/phone login — there is no separate 'social signup success' screen.
AC-05If GOOGLE_CLIENT_ID/APPLE_CLIENT_ID are not configured on the API, tapping the corresponding button surfaces a clear Arabic error ('تسجيل الدخول عبر Google/Apple غير مُفعّل حالياً') rather than a generic network-error message or a silent failure.
AC-06A brand-new social sign-in (no prior account with that email) results in a fully usable account with an active Identity immediately — same regression class as mobile-signup-otp-skip-logic, verify it doesn't recur here on a different code path.
Test Scenarios
TC-01Environment check first: confirm with the backend team whether GOOGLE_CLIENT_ID/APPLE_CLIENT_ID are set on the environment under test, and whether this build was produced via eas build/expo prebuild (not Expo Go) — record the answer, since it determines whether a tap failure is expected or a real bug.
TC-02On a properly configured build, tap 'Continue with Google', complete the picker with a real Google account, and confirm you land on the identity home screen already logged in.
TC-03Repeat with Apple on iOS, including once with 'Hide My Email' selected, and confirm the account still links correctly (the relay email address is acceptable as the stored email).
TC-04Cancel the Google picker mid-flow and confirm the app returns to the auth screen with no error alert and isLoading resets (button is tappable again immediately).
TC-05Sign in with a Google/Apple account whose email already has a password-based account on Buyootna, and confirm it links to the existing account (same identities, not a duplicate) rather than erroring or creating a second account.
TC-06On Android, confirm only the Google button is visible and the layout doesn't leave an awkward empty space where the Apple button would be on iOS.
OTP verification: 45s resend countdown behaves correctly at the boundary
P1
As an individual user verifying my phone number, I want to see clearly how long I have to wait before I can request a new code, and get an easy way to resend it once that time is up, so that I'm never left wondering whether my code is still valid or the app is just stuck.
Verify the OTP screen's 6-box input and 45-second resend countdown (ForgotPasswordStep2, and the equivalent IndividualSignupOtpStep) match the Figma frame exactly and handle the boundary correctly, so that a user is never stuck unable to resend and never able to spam resends before the timer expires.
AC-01The 6 OTP boxes fill left-to-right as digits are typed, with the active box highlighted and an error state (red border) shown only after a failed verify attempt.
AC-02The countdown starts at 00:45 and counts down to 0, then swaps to an 'إعادة إرسال الرمز' (resend) link.
AC-03Tapping resend before the timer hits 0 is not possible (no resend UI is shown yet), and tapping it after reset the timer back to 45.
AC-04The verify button stays disabled until exactly 6 digits are entered, matching the 'grey when disabled' state shown in the design.
Test Scenarios
TC-01Start the OTP step and confirm the timer reads exactly 00:45 initially, matching the Figma frame.
TC-02Let the timer run to 0 and confirm the resend link appears immediately with no dead gap where neither the timer nor the link is visible.
TC-03Enter an incorrect 6-digit code and confirm the boxes/border show an error state consistent with the design's error styling, with the actual error message visible.
TC-04Enter 5 digits and confirm the verify button remains disabled (grey); enter the 6th and confirm it becomes enabled.
Forgot-password 'account not found' error: design shows inline text, code shows a popup Alert (mismatch)
P2
As an individual user who forgot my password, I want to be told clearly and right there on the screen if the email or phone I entered doesn't match any account, so that I immediately understand what went wrong instead of guessing why nothing happened.
Flag that the Figma design for step 1 of Forgot Password shows an inline error message under the field ('لم نتمكن من العثور على حساب مرتبط بهذه البيانات') for an unrecognized identifier, but ForgotPasswordStep1.tsx has no inline error rendering at all — the actual implementation only surfaces failures via a native Alert.alert('خطأ', ...) popup — so this is a real design/implementation mismatch worth a product decision, not just a QA pass/fail.
AC-01Submitting an unrecognized identifier on the real Forgot Password step 1 screen triggers a native Alert dialog, not the inline red-text error shown in Figma.
AC-02The submit button's disabled/enabled (grey vs. navy) states do match the design exactly — that part of the frame is implemented correctly.
AC-03Decide and document whether the inline-error version should be built to match Figma, or whether the Figma frame should be updated to reflect the Alert-based pattern actually shipped.
Test Scenarios
TC-01Submit step 1 with an email/phone that has no matching account and confirm exactly what UI appears — screenshot it next to the Figma frame to make the mismatch concrete for the ticket.
TC-02Confirm the disabled (grey, 'إرسال رمز التحقق' unclickable) state when the field is empty, and the enabled (navy) state once text is entered — this part should match the design.
TC-03Repeat with a clearly malformed identifier (e.g. random string) to confirm the same Alert-based error path fires, not a different one.
Create-new-password step: design has a 'تخطي' skip option, code does not (mismatch)
P2
As an individual user resetting my password, I want the option to back out of the process without being forced to pick a new password right then, so that I'm not stuck finishing a flow I started by mistake or no longer want to complete.
Flag that the Figma frame for the final 'إنشاء كلمة مرور جديدة' (create new password) step shows a 'تخطي' (skip) link in the top-right corner, but ForgotPasswordStep3.tsx has no skip affordance at all — the user must set a new password to proceed, there's no way to abandon the reset and keep the old password from this screen — so this is a design/implementation mismatch that should be resolved deliberately rather than left ambiguous.
AC-01Confirm the real screen has no 'تخطي' button, contrary to the Figma frame.
AC-02Password (8-char min) and confirmPassword (must match) validation blocks the 'تحديث كلمة المرور' button until both are valid — the disabled/enabled grey-vs-navy states should match the design.
AC-03Decide whether 'skip' should mean 'cancel the whole reset flow and return to login unchanged' — if so, that needs an actual back/close affordance, since currently the ForgotPasswordHeader's back button is the only way out and it steps back to the OTP screen, not out of the flow entirely.
Test Scenarios
TC-01Reach the create-new-password step and confirm there is genuinely no skip control anywhere on screen — check the header row specifically, since that's where Figma places it.
TC-02Enter a 7-character password and confirm the update button stays disabled; enter 8 and confirm it enables (once confirmPassword also matches).
TC-03Use the screen's back button from this step and confirm exactly where it lands (expected: back to the OTP step, per handleBack's step-- logic) — compare that against what 'تخطي' in the design likely intended.
Success screens: account-created button copy differs between design and code (minor)
P2
As an individual user who just created my account, I want a clear, encouraging confirmation that tells me what to do next, so that I feel confident the signup worked and know exactly where to go from there.
Flag a small copy drift: the Figma frame for 'تم إنشاء حسابك بنجاح' (account created) shows a button labeled 'ابدأ الاستكشاف' (start exploring), while the actual SuccessBottomSheet rendered in individual-auth.tsx uses buttonText='ابدأ الآن' (start now) — a minor but real inconsistency worth a one-line fix. Confirm the password-reset success screen ('تم تحديث كلمة المرور بنجاح' / 'تسجيل الدخول') matches its Figma frame exactly.
AC-01Password-reset success sheet: title, description, and button text ('تسجيل الدخول') match the Figma frame exactly — verified, no action needed.
AC-02Account-created success sheet: button currently reads 'ابدأ الآن' where the design specifies 'ابدأ الاستكشاف' — needs either a copy fix in code or a design-doc update, whichever is actually correct per product.
AC-03Both success sheets correctly navigate on button press: password-reset → individual-auth (login), account-created → the identity's home route.
Test Scenarios
TC-01Trigger both success sheets (complete a password reset, and complete a no-phone signup) and read the exact button text on each against the two Figma frames side by side.
TC-02Tap through both buttons and confirm the navigation destinations match what's described above.
TC-03File the copy fix as a trivial ticket rather than a QA blocker — this is cosmetic, not functional.
Profile/identity switcher bottom sheet matches design functionally, with a minor label difference
P2
As an individual user who also manages a company or office account, I want to see all of my identities in one place and switch between them with a tap, so that I don't have to log out and back in every time I need to act under a different role.
Verify ProfileBottomSheet (opened via 'المتابعة بحساب محفوظ' on login, or the profile icon elsewhere) correctly lists all of a user's identities with type badges and lets them switch or add a new one, matching the Figma 'اختر الملف الشخصي' frame functionally, while noting the add-account button reads 'إضافة هوية جديدة' (add new identity) in code versus 'اضافة حساب جديد' (add new account) in the design — a wording nit, not a functional gap.
AC-01Every identity the user owns (individual/company/office) appears in the list with the correct type badge and icon, and the currently-active one is visually marked ('الهوية النشطة').
AC-02Tapping a non-active identity shows a loading spinner on that row, then switches and routes to that identity's home screen.
AC-03Tapping the already-active identity simply closes the sheet with no network call.
AC-04The add-identity button opens the signup flow (onAddAccount → handleModeChange('signup')) rather than a dead end.
Test Scenarios
TC-01As a user with 2+ identities, open the sheet and confirm all are listed with correct labels ('فرد' vs. company/office labels) and the right one is marked active.
TC-02Switch identities and confirm the sheet closes and the app navigates to the correct home route for the new identity type — for a company/office identity, confirm it lands cleanly on ComingSoonGate (see mobile-company-office-gate in Mobile — Shared) rather than something broken.
TC-03Tap the currently-active identity's row and confirm it just closes the sheet without a spinner or network call.
TC-04Tap 'إضافة هوية جديدة' and confirm it opens the signup form step (individual signup only — company/office signup is gated, see Mobile — Shared).