ProjectR Docs
Screen MapMobile app

Loyalty & vouchers

Mobile app: every screen, state and flow in Loyalty & vouchers.

6 screens and overlays, 59 states, 3 flows. Frames show a real screenshot where one was captured and a wireframe card where the state needs data or conditions a device run cannot produce.

Flows

Duit Raya voucher claim

Step 1
Scan Duit Raya QR Code, Scanning (iOS)Scan Duit Raya QR Code, Scanning (Android)
Camera view waits for a Duit Raya QR code
Scan a QR
Step 2
Wireframe

Scan Duit Raya QR Code

Loading

Claiming voucher...

Camera unmounts, Please wait; 10 s timeout.

Not reachable on a device: Needs a network fault, crash, race or code change to show

Claim in progress, up to 10 s
Claim succeeds
Step 3
Wireframe

Scan Duit Raya QR Code

Success

Voucher claimed

Confetti sheet with discount, tap-to-copy code (Copied!) and Valid until date.

Needs data: Capture audit 2026-10-30. ios: iOS simulator has no camera; capture scanner outcomes on supported Android device. android: Requires QR input through camera. Current supported device controls have no QR-image injection; scanner itself captured.

Success sheet shows the voucher
View My Discounts
Step 4
My Discounts, Voucher wallet (iOS)My Discounts, Voucher wallet (Android)
My Discounts lists the new voucher

Branches and edge cases

Gatefrom step 1
Scan Duit Raya QR Code, Sign in to claim vouchers (iOS)Scan Duit Raya QR Code, Sign in to claim vouchers (Android)
Guest opens the scanner
Edgefrom step 1
Wireframe

Scan Duit Raya QR Code

Loading

Checking sign-in...

Black screen with spinner while auth loads.

Not reachable on a device: Needs a network fault, crash, race or code change to show

Checking sign-in
Gatefrom step 1
Scan Duit Raya QR Code, Camera Permission Required (iOS)Scan Duit Raya QR Code, Camera Permission Required (Android)
Camera permission denied
Gatefrom step 1
Scan Duit Raya QR Code, Camera Access Required alert (iOS)Scan Duit Raya QR Code, Camera Access Required alert (Android)
Settings alert for camera
Errorfrom step 2
Wireframe

Scan Duit Raya QR Code

Error

Claim Failed

Server message such as Invalid code, already claimed, campaign not active or expired, rate limited.

Needs data: Capture audit 2026-10-30. ios: iOS simulator has no camera; capture scanner outcomes on supported Android device. android: Requires QR input through camera. Current supported device controls have no QR-image injection; scanner itself captured.

Claim fails
Errorfrom step 2
Wireframe

Scan Duit Raya QR Code

Error

Request timed out

No response within 10 s; Please try again.

Not reachable on a device: Needs a network fault, crash, race or code change to show

Claim times out
Successfrom step 3
Shop (iOS)Shop (Android)
Start shopping
Flow diagram

Earning a stamp and shake reward

Step 1
Order Details, Stamp row: awarded (iOS)Order Details, Stamp row: awarded (Android)
Paid, linked order shows its stamp
Open Loyalty tab
Step 2
Loyalty, Stamp node: special available (iOS)Loyalty, Stamp node: special available (Android)
A special node glows on the stamp board
Tap the node
Step 3
Shake to Win!, Challenge (iOS)Shake to Win!, Challenge (Android)
Shake to Win sheet asks for 5 shakes
Shake the phone
Step 4
Shake to Win!, Shaking (iOS)Shake to Win!, Shaking (Android)
Counter moves as shakes register
Fifth shake
Step 5
Shake to Win!, Result: won (iOS)Shake to Win!, Result: won (Android)
Voucher name and code revealed
View My Discounts
Step 6
My Discounts, Voucher wallet (iOS)My Discounts, Voucher wallet (Android)
Won voucher sits in My Discounts

Branches and edge cases

Edgefrom step 1
Order Details, Stamp row: below minimum (iOS)Order Details, Stamp row: below minimum (Android)
Subtotal below minimum: no stamp
Edgefrom step 2
Loyalty, Stamp node: earned (iOS)Loyalty, Stamp node: earned (Android)
Plain stamp earned
Edgefrom step 2
Loyalty, Stamp node: special completed (iOS)Loyalty, Stamp node: special completed (Android)
Special already completed
Edgefrom step 3
Wireframe

Shake to Win!

Edge

Already claimed

No stored outcome for that position; also fires for reversed rows

Needs data: Capture audit 2026-10-30. ios: Requires a claimed special reward outside the 20-row client history window plus a node the client still treats as available; current test history does not reproduce this mismatch. android: Requires a claimed special reward outside the 20-row client history window plus a node the client still treats as available; current test history does not reproduce this mismatch.

Already claimed
Edgefrom step 4
Shake to Win!, Result: lost (iOS)
Miss: better luck next time
Errorfrom step 4
Wireframe

Shake to Win!

Edge

Reward still syncing

History not loaded yet; no reveal recorded

Not reachable on a device: Needs a network fault, crash, race or code change to show

History still loading
Errorfrom step 4
Wireframe

Shake to Win!

Error

Processing error

Friendly error result and error haptic

Not reachable on a device: Needs a network fault, crash, race or code change to show

Processing error
Flow diagram

Using a voucher

Step 1
My Discounts, Voucher wallet (iOS)My Discounts, Voucher wallet (Android)
My Discounts lists Ready vouchers
Tap a card
Step 2
My Discounts, Code Copied toast (iOS)
Toast confirms the code was copied
Tap Use, go to Shop
Step 3
My Bag, Social proof and voucher hint (iOS)My Bag, Social proof and voucher hint (Android)
Bag shows the voucher pill
Tap the pill
Step 4
Vouchers & Promo, Recommended and expiry tags (iOS)Vouchers & Promo, Recommended and expiry tags (Android)
Vouchers sheet, pick the voucher and Apply

Branches and edge cases

Errorfrom step 1
My Discounts, Voucher Unavailable toast (iOS)My Discounts, Voucher Unavailable toast (Android)
Voucher not active
Errorfrom step 2
Wireframe

My Discounts

Error

Copy Failed toast

Clipboard copy threw

Not reachable on a device: Needs a network fault, crash, race or code change to show

Copy fails
Edgefrom step 1
My Discounts, Expiry warning (iOS)My Discounts, Expiry warning (Android)
Voucher near expiry
Emptyfrom step 1
Wireframe

My Discounts

Empty

No ready discounts

Ready bucket empty; View history

Needs data: Capture audit 2026-10-30. ios: All four fixed-OTP test accounts have active vouchers. Needs an isolated empty-ready-wallet fixture. android: All four fixed-OTP test accounts have active vouchers. Needs an isolated empty-ready-wallet fixture.

No ready discounts
Flow diagram

Screens and states

Scan Duit Raya QR Code

Wireframe

Scan Duit Raya QR Code

Not captured yet

Screenduit-raya-flow

Scan Duit Raya QR Code

QR scanner that claims a Duit Raya voucher.

src/app/duit-raya-flow.tsx

Entry

  • Home app bar camera icon
  • Deep link /duit-raya-flow

Exits

  • Start shopping → Shop
  • View My Discounts → discount pool
  • Maybe later → close
  • Close X → back or Home
  • Try again → scan again

States · 13

Loading
Checking sign-in... · Black screen with spinner while auth loads.src/app/duit-raya-flow.tsx:109
Gate
Sign in to claim vouchers · Guest sees the gift gate. Sign In opens the sheet; Maybe later closes.src/app/duit-raya-flow.tsx:122
Loading
Checking camera access... · Camera permission still resolving.src/app/duit-raya-flow.tsx:309
Gate
Camera Permission Required · Permission denied or undetermined; Continue requests it.src/app/duit-raya-flow.tsx:322
Error
Camera Access Required alert · After a denial an alert explains Settings with Cancel and Open Settings; no button on the screen.src/shared/hooks/utils/use-camera-permission.ts
Edge
Scanning · Full-screen QR camera; the same payload is ignored for 3 s and only one claim runs.src/app/duit-raya-flow.tsx:167
Loading
Claiming voucher... · Camera unmounts, Please wait; 10 s timeout.src/app/duit-raya-flow.tsx:189
Success
Voucher claimed · Confetti sheet with discount, tap-to-copy code (Copied!) and Valid until date.src/app/duit-raya-flow.tsx:658
Edge
Success sheet gesture differs by platform · Not dismissible by gesture on iOS; a drag closes the flow on Android.src/app/duit-raya-flow.tsx:279
Error
Claim Failed · Server message such as Invalid code, already claimed, campaign not active or expired, rate limited.packages/backend/convex/duitRaya/mutations.ts:122
Error
Request timed out · No response within 10 s; Please try again.src/app/duit-raya-flow.tsx:228
Error
Something went wrong · Unexpected throw during claim; Please try again.src/app/duit-raya-flow.tsx:221
Offline
Offline hangs then times out · A claim that succeeded after the timeout is not detected; the voucher may exist in My Discounts.src/app/duit-raya-flow.tsx:189

Captured states

Scan Duit Raya QR Code, Sign in to claim vouchers (iOS)Scan Duit Raya QR Code, Sign in to claim vouchers (Android)
Sign in to claim vouchers
Scan Duit Raya QR Code, Camera Permission Required (iOS)Scan Duit Raya QR Code, Camera Permission Required (Android)
Camera Permission Required
Scan Duit Raya QR Code, Camera Access Required alert (iOS)Scan Duit Raya QR Code, Camera Access Required alert (Android)
Camera Access Required alert
Scan Duit Raya QR Code, Scanning (iOS)Scan Duit Raya QR Code, Scanning (Android)
Scanning

Loyalty

Loyalty (iOS)Loyalty (Android)
Screen(app)/(tabs)/loyalty

Loyalty

Stamp card journey, special positions and stamp activity

src/app/(app)/(tabs)/loyalty/index.tsx

Entry

  • Bottom tab
  • Profile stats
  • Home carousel slides
  • Voucher empty state Start Earning

Exits

  • Special node → Shake sheet
  • Earned node → stamp preview
  • Carousel slide → campaign, product or link
  • Guest Get Started → auth sheet

States · 18

Loading
Loading · Auth loadingsrc/app/(app)/(tabs)/loyalty/index.tsx:25
Gate
Guest view · Not signed in; Get Started buttonsrc/features/loyalty/components/shared/loyalty-guest-view.tsx:52
Loading
Loyalty skeleton · Campaign, progress or history loadingsrc/features/loyalty/components/pages/loyalty-page-redesigned.tsx:222
Empty
No Active Campaign · No active campaign in its date window; Refresh actionsrc/features/loyalty/components/pages/loyalty-page-redesigned.tsx:227
Edge
Campaign Has Ended · Client clock past endsAt while screen opensrc/features/loyalty/components/pages/loyalty-page-redesigned.tsx:280
Success
Stamp journey board · Carousel plus board up to 24 stamps; first campaign onlysrc/features/loyalty/components/stamps/stamp-journey-board.tsx:39
Edge
Stamp node: locked · Not yet earned; not interactivesrc/features/loyalty/utils/stamp-state-resolver.ts:70
Edge
Stamp node: earned · Position reached; tap opens previewsrc/features/loyalty/utils/stamp-state-resolver.ts:95
Edge
Stamp node: special available · Special position earned, reward unrevealed; glowssrc/features/loyalty/utils/stamp-state-resolver.ts:70
Edge
Stamp node: special completed · Special reward already revealedsrc/features/loyalty/utils/stamp-state-resolver.ts:70
Edge
Stamp node: final reward · Last position, earned or notsrc/features/loyalty/utils/stamp-state-resolver.ts:70
Loading
Activity skeleton · History loadingsrc/features/loyalty/components/activity/history/activity-history.tsx:129
Empty
No activity yet · No stamp history rowssrc/features/loyalty/components/activity/history/activity-history.tsx:136
Success
Activity history · Grouped by date and order; Bonus stamps for admin grants; limit 50src/features/loyalty/components/activity/history/order-activity-card.tsx:52
Edge
Cancelled order still shows +N · History rows kept on cancel and carry no reversed flagpackages/backend/convex/gamificationLoyalty/queries.ts:337
Edge
No card complete UI · Backend wraps stamps to modulo total, so isCompleted never truepackages/backend/convex/gamificationLoyalty/helpers.ts:364
Error
Query failure · No error state; thrown to TabErrorBoundarysrc/app/(app)/(tabs)/loyalty/index.tsx:21
Edge
Unused empty-state types · no-stamps, error, offline and others defined but not wiredsrc/features/loyalty/components/shared/loyalty-empty-state.tsx:59

Captured states

Loyalty, Stamp node: earned (iOS)Loyalty, Stamp node: earned (Android)
Stamp node: earned
Loyalty, Stamp node: special available (iOS)Loyalty, Stamp node: special available (Android)
Stamp node: special available
Loyalty, Stamp node: special completed (iOS)Loyalty, Stamp node: special completed (Android)
Stamp node: special completed
Loyalty, Cancelled order still shows +N (iOS)Loyalty, Cancelled order still shows +N (Android)
Cancelled order still shows +N

Shake to Win!

Wireframe

Shake to Win!

Needs data: Needs a special stamp position awarded to the QA account

SheetSimpleShakeReward

Shake to Win!

Reveal the pre-rolled reward of a special stamp position

src/features/loyalty/components/shake-to-win/simple-shake-reward.tsx

Entry

  • Tap a special-available stamp node

Exits

  • Done or close → reveal recorded, sheet closes
  • Voucher code card → copy

States · 9

Edge
Challenge · Shake your phone 5 times; hard-coded RM50 copy; Let's Go! buttonsrc/features/loyalty/components/shake-to-win/components/challenge-view.tsx
Edge
Shaking · Progress copy by ratio; manual shake button on simulatorsrc/features/loyalty/components/shake-to-win/components/shaking-view.tsx:88
Success
Result: won · You won name; voucher chip and tap-to-copy codesrc/features/loyalty/components/shake-to-win/hooks/use-shake-processing.ts:120
Edge
Result: lost · Better luck next time; keep collecting stampssrc/features/loyalty/components/shake-to-win/hooks/use-shake-processing.ts:126
Edge
Reward still syncing · History not loaded yet; no reveal recordedsrc/features/loyalty/components/shake-to-win/hooks/use-shake-processing.ts:128
Edge
Already claimed · No stored outcome for that position; also fires for reversed rowssrc/features/loyalty/components/shake-to-win/hooks/use-shake-processing.ts:142
Error
Processing error · Friendly error result and error hapticsrc/features/loyalty/components/shake-to-win/hooks/use-shake-processing.ts:177
Error
Render crash · Error boundary titles with Try Again (n attempts left)src/features/loyalty/components/shake-to-win/components/error-boundary.tsx:95
Edge
maxAttempts unused · shakeLimit passed to ResultView but ignoredsrc/features/loyalty/components/shake-to-win/components/result-view.tsx:141

Captured states

Shake to Win!, Challenge (iOS)Shake to Win!, Challenge (Android)
Challenge
Shake to Win!, Shaking (iOS)Shake to Win!, Shaking (Android)
Shaking
Shake to Win!, Result: won (iOS)Shake to Win!, Result: won (Android)
Result: won
Shake to Win!, Result: lost (iOS)
Result: lost

Stamp preview

Wireframe

Stamp preview

Needs data: Needs at least one earned stamp on the QA account

ModalStampPreviewModal

Stamp preview

Full-screen circular preview of an earned stamp

src/features/loyalty/components/pages/loyalty-page-redesigned.tsx

Entry

  • Tap an earned stamp node

Exits

  • Tap anywhere or Android back → close

States · 1

Success
Preview open · Single state; hides status barsrc/features/loyalty/components/pages/loyalty-page-redesigned.tsx:305

Loyalty banners

Loyalty banners (iOS)Loyalty banners (Android)
InlineLoyaltyMarketingCarousel

Loyalty banners

Dashboard-managed banner slides above the stamp board

src/features/loyalty/components/marketing/loyalty-marketing-carousel.tsx

Entry

  • Loyalty tab only

Exits

  • Slide tap → campaign, product, loyalty or external link

States · 4

Loading
Carousel skeleton · Slides loadingsrc/features/loyalty/components/marketing/loyalty-marketing-carousel.tsx:320
Empty
No slides · Renders nothingsrc/features/loyalty/components/marketing/loyalty-marketing-carousel.tsx:324
Edge
Single slide · No loop, autoplay or dotssrc/features/loyalty/components/marketing/loyalty-marketing-carousel.tsx:331
Edge
External link unsupported · Tap silently does nothingsrc/features/loyalty/components/marketing/loyalty-marketing-carousel.tsx:277

My Discounts

My Discounts (iOS)My Discounts (Android)
Screen(app)/(tabs)/profile/settings/account/discount-pool

My Discounts

Wallet of earned vouchers

src/app/(app)/(tabs)/profile/settings/account/discount-pool.tsx

Entry

  • Settings > Shopping
  • Push or inbox voucher type
  • Duit Raya claim

Exits

  • Shop pill → Shop tab
  • Tap active card → copy code
  • Use → copy and open Shop
  • Start Earning → Loyalty tab
  • Sign in → auth sheet

States · 14

Loading
Loading · Auth loadingsrc/app/(app)/(tabs)/profile/settings/account/discount-pool.tsx:54
Gate
Sign in to view discounts · Guestsrc/app/(app)/(tabs)/profile/settings/account/discount-pool.tsx:60
Loading
Voucher skeleton · Vouchers query undefinedsrc/features/vouchers/components/voucher-list.tsx:411
Error
Unable to load vouchers · Branch exists but screen passes error null; can never showsrc/features/vouchers/components/voucher-list.tsx:366
Empty
No Discounts Yet · No vouchers at all; Start Earning buttonsrc/features/vouchers/components/voucher-empty-state.tsx:25
Empty
No ready discounts · Ready bucket empty; View historysrc/features/vouchers/components/voucher-list.tsx:394
Empty
No discount history · History bucket empty; View readysrc/features/vouchers/components/voucher-list.tsx:394
Success
Voucher wallet · Hero banner, Ready and History chips, Expiring Soon groupsrc/features/vouchers/components/voucher-list.tsx:195
Success
Code Copied toast · Tap or Use on an active vouchersrc/features/vouchers/hooks/use-voucher-actions.ts:43
Edge
Voucher Unavailable toast · Tap on expired, used or cancelled vouchersrc/features/vouchers/utils/voucher-helpers.ts:68
Error
Copy Failed toast · Clipboard copy threwsrc/features/vouchers/hooks/use-voucher-actions.ts:98
Edge
Expiry warning · Expires today or n days left within 7 dayssrc/features/vouchers/utils/voucher-helpers.ts:138
Edge
60 voucher cap · Query limit 60, no paginationpackages/backend/convex/mobileLoyalty/queries.ts:242
Edge
Use only copies code · Applying happens in the bag, not heresrc/features/vouchers/hooks/use-voucher-actions.ts:113

Captured states

My Discounts, Voucher wallet (iOS)My Discounts, Voucher wallet (Android)
Voucher wallet
My Discounts, Code Copied toast (iOS)
Code Copied toast
My Discounts, Voucher Unavailable toast (iOS)My Discounts, Voucher Unavailable toast (Android)
Voucher Unavailable toast
My Discounts, Expiry warning (iOS)My Discounts, Expiry warning (Android)
Expiry warning

On this page