Real quests, broken buttons: how Phase C and N closed bb finance's behaviour loop

Device-QA round my test iPhone. Three findings, all on the Home screen’s gamification surface.

The Quests block was showing real quests. QuestGenerator was live. The Level block displayed the correct XP. The Streak counter reflected actual check-in history. Everything rendered accurately.

The blocks didn’t respond to taps.

Finding #2: the Streak block had a “Check in” button. Tapping it opened the transaction form.

That’s the failure mode. Not a crash, not wrong data, but a broken action path. The user sees an accurate quest, tries to act on it, and routes somewhere irrelevant. Phase N fixed all three findings. Phase N could only fix them because Phase C had already replaced the fake data underneath.

From mockQuests to QuestGenerator

The Phase C article documented the architectural decision to replace the hardcoded mockQuests array with a pure QuestGenerator. This piece covers what the generator grew into and what it took to make its output actionable.

mockQuests was a static list. Fixed labels, fixed progress values, no relationship to actual spending. You could complete every quest by doing nothing. The gamification layer was a display problem rather than a behaviour problem: it looked like a quest system without being one.

QuestGenerator is pure: no side effects, no direct store access, no async calls. It takes transaction history and claim state as inputs and emits up to four quests per day. The same inputs always produce the same output. Whether a quest appears depends entirely on what the user has actually done.

Phase C shipped with two initial quest types. StreakBreaker fires when the generator detects a payee with five or more transactions. SaveX fires against a daily save target and shows progress toward it.

Both are grounded in data. The quest exists because the transaction history says it should.

Extending the generator

Two extensions landed in the same PR.

categoryBudgetQuest adds a new generator slot that fires when any spending category has three or more outflow transactions in the last seven days with total spend at or above £20. The generator picks the category with the largest spend. The quest is specific: not a general “spend less” nudge but a named category, a named total, and a named target. It only fires when the pattern is present in the data.

The specificity matters. A generic prompt teaches nothing actionable. A prompt that surfaces the exact category where overspend is happening gives the user something concrete to reason about and act on.

SaveX user-configurable target closed PE-005. UserProfileStore gains a dailySaveTarget property: Decimal, default £15, persisted as a Decimal-string in UserDefaults. Before this change, the SaveX quest compared against a hardcoded threshold. After it, the generator emits a quest calibrated to what the user has declared as their target. The quest is personal rather than generic.

Both extensions follow the same constraint as the base generator: derived from real state, never authored.

Streak milestones

Separate from quest generation: StreakMilestones, a pure type with one method, crossed(previous:current:) -> Int?, which returns the highest milestone crossed given two consecutive streak counts. Thresholds are 7, 14, 30, 60, and 100 days.

When the streak counter updates and a milestone threshold is crossed, the UI renders a flame-themed celebration overlay. The milestone logic sits in the model; the view renders whatever the model returns.

This is reinforcement, not decoration. The overlay fires because you have actually checked in seven, fourteen, or thirty times in a row. The system is acknowledging a fact about your consistency, not emitting a canned animation.

What Phase N wired

Phase C built a generator that produces correct output. Device-QA found the output wasn’t actionable.

QA findings #1 through #3 shared the same root. The three Home gamification blocks (Quests, Level, Streak) were non-interactive labels. They showed real data with no tap behaviour attached. Phase N wired them.

The Streak block’s routing bug was the sharpest illustration of why this matters. A user sees their streak, taps “Check in”, and the transaction form opens instead. The connection between the stated intent (“maintain streak”) and the system response (transaction form) is wrong. The user learns that tapping gamification elements does something unpredictable. They stop tapping.

Once users stop engaging with the quest surface, the generator’s accuracy stops mattering. The loop has already broken at the interaction layer.

Phase N corrected each path. The quest block navigates to quest detail. The streak button checks in. The level block opens the level detail screen.

Theatre vs. behaviour loop

The distinction between gamification theatre and a behaviour loop is whether the system teaches the user something true about their own spending, and then lets them act on it.

mockQuests failed the first condition. Quests were authored, not derived. A user could clear the quest list without changing a single transaction. The system taught nothing because its signals had no connection to real behaviour.

QuestGenerator satisfies the first condition. StreakBreaker fires because you actually bought from the same payee five or more times. categoryBudgetQuest fires because you actually spent above the threshold in a real category. SaveX fires against a target you chose. Each quest is a true observation about your financial behaviour, framed as a prompt.

Phase N satisfies the second condition. The tap routes correctly. The check-in records state. The quest claim closes the loop. The user can act on what the system has surfaced, and the system registers that they did.

That’s the closed loop: generator reads real behaviour, surfaces an accurate prompt, user acts, system records the action. No invented data at any step in the chain.

Out of scope

Adaptive thresholds. The trigger conditions are static: five transactions for StreakBreaker, three transactions and £20 for categoryBudget, £15 default for SaveX. A user making forty transactions a week will hit StreakBreaker trivially; a user making six may never reach it. Per-user calibration is future work.

Quest history across days. Claim state exists within a day; there’s no longitudinal record of which quests the user has seen or completed over time. The view can’t show a user that they’ve completed categoryBudget for four consecutive weeks. Also future work.

Both are tractable. Neither warranted scope in this sprint.

All writing