Four quest types. None of them read your transactions.

You tap “Check in” on the streak counter. The transaction form opens.

Device QA caught that in round one. And round two. And round three. The same defect across three separate passes. Not because it was hard to fix, but because the gamification surface had shipped before the interactivity it implied was wired up. The block looked like it did something. It didn’t do the right thing.

That was Phase N’s problem. Phase C had a different one.

The mock array

Every quest in an iOS budget app drew from mockQuests: a static array defined once, returning the same quest shapes for every user on every load. Four quest types existed in the design (StreakBreaker, CategoryBudget, SaveX, StreakGuard), and all four needed transaction data to be meaningful. The mock array had none.

The SaveX quest is the clearest example. It had a hardcoded target. Just a number the developer had written in. The per-user dailySaveTarget preference didn’t exist yet. PE-005 was open. So every user saw the same save goal, regardless of what they’d tracked, what they’d committed to, or what their spending patterns suggested was realistic. The quest was personalisation in appearance only.

The structural issue here is common. Gamification surfaces ship early because the design needs them to validate layout and visual hierarchy before there are enough real transactions to derive anything from. mockQuests lived in that gap between “the surface exists” and “the surface has data.” The gap closed when real transaction history accumulated. Phase C is the pass that reads it.

QuestGenerator

QuestGenerator is a pure function. Inputs: transaction history, claim state. Output: up to four quests per day. No side effects. The content is derived, not declared.

StreakBreaker detects any payee with five or more appearances in the transaction history. If the pattern exists, the quest names the payee. It fires because of a real spending habit, not because a designer decided a “repeat payee” quest slot should exist.

CategoryBudget fires when any category has three or more outflow transactions in the last seven days with total spend at or above £20. It picks the category with the largest spend. A user with no category concentration doesn’t see this quest. A user who’s distributed £150 evenly across eight categories doesn’t see it either, because no single category clears the £20 floor. The threshold is a filter. The generator contracts to what’s relevant.

SaveX targets a daily save amount. Phase C closes PE-005: UserProfileStore now carries dailySaveTarget: Decimal, published, defaulting to £15, persisted as a Decimal-string in UserDefaults. The user can set it. The quest’s threshold is a preference, not a constant in a mock array.

StreakGuard derives from the active streak state.

The generator can return fewer than four quests when the rules don’t have enough signal. That’s correct behaviour. A quest that fires without basis is noise. The quest grid doesn’t need to be full to justify the screen real estate.

Claim flow

Phase C ships the claim flow alongside the generator. Quest presented, quest claimed, claim acknowledged. The claim state feeds back into QuestGenerator: a quest that’s been claimed today doesn’t regenerate until the next cycle.

Out of scope: adaptive thresholds. CategoryBudget’s £20 floor and three-transaction minimum are fixed. Adjusting them based on observed spending patterns is a future sprint.

Dynamic Type

PE-005 also ships the Dynamic Type clamp on the SaveX quest UI. Dynamic Type is Apple’s system-wide text scaling preference; at large accessibility sizes, quest card text can overflow its container or truncate in ways that break the layout. The clamp sets an upper bound on font scaling inside the card without disabling Dynamic Type globally. Standard accessibility sizes behave normally; extreme sizes stay within bounds.

Streak milestones

Same PR, side branch: StreakMilestones. A pure function, crossed(previous:current:) -> Int?, finds the highest milestone the streak has just crossed from the set 7, 14, 30, 60, and 100 days. Non-nil: show the overlay. Nil: nothing.

The flame-themed overlay fires on crossing, not on every load above the threshold. The function compares the previous streak count against the current one. No server call. Nothing stored, nothing scheduled. The check is inline; the result is binary.

Milestones are crossings, not states. Once the overlay fires at day 14, it won’t fire again until day 30. The function signature enforces this, because it compares two counts, not one.

Interactivity

Phase N closes the UX debt.

The three Home gamification blocks (Quests, Level, Streak) were non-interactive labels. Tapping them did nothing. Phase N makes them navigable: Quests opens the quest detail screen, Level opens level progress, Streak opens streak history and the direct check-in surface.

The “Check in” bug on the Streak block is fixed. The button now runs the streak check-in directly. Device QA found the transaction-form misdirection in three separate passes; the fix is in.

What the data layer proves

The thesis is narrow: gamification only works when quests reflect what the user is actually doing.

The gap between mockQuests and QuestGenerator isn’t cosmetic. QuestGenerator knows the user has spent heavily in one category this week. mockQuests didn’t know anything. One produces a quest worth reading; the other produces content the user learns to ignore because it never changes.

Budget apps have an advantage that most gamification contexts don’t: the data that would make the quests relevant already exists. Every transaction is a signal. A real repeat-payee pattern triggers StreakBreaker. Category concentration that actually shows up in the week’s outflows triggers CategoryBudget. SaveX targets a number the user has actually set. None of this required building a sophisticated recommendation engine. It required reading the data that was already there.

The engagement shortcut is understandable: ship the surface, add real data later. Layout, interaction, and visual hierarchy all need to be validated before there are enough transactions to derive meaningful rules from. The shortcut becomes debt when “later” doesn’t arrive.

Phase C is “later.” The quest types are data-derived. Claim state feeds back into generation. Milestones fire on genuine crossings, and the detail screens are navigable. The check-in button does what it says.

The next phase doesn’t need to argue for any of this. It can build on it.

All writing