Skip to content

feat(ios): choose which price the app shows - #141

Merged
frahlg merged 5 commits into
mainfrom
ios-price-choice
Nov 1, 2026
Merged

frahlg merged 5 commits into
mainfrom
ios-price-choice

Conversation

@frahlg

@frahlg frahlg commented Nov 1, 2026

Copy link
Copy Markdown
Member

Why

Fredrik, 1 November 2026: the shown price should be a setting. He wants it without grid fees, and wants to know whether it is with or without VAT. This is step 4 of four. It stacks on #140 (and so on #139 and #138).

Paired with srcfl/ftw#1949 (Core sends vatPermille on price.get and on FTW Cloud's price tile) and srcfl/ftw-webapp#116 (the protocol doc and the web client's type). The app works with an older box too; see below.

Change

  • Home › Price shown: two switches, "With grid fee and tax" and "With VAT", kept on this phone. The footer names what is shown.
  • PriceShown (FTWShared, tested) turns the box's spotMinor and totalMinor into the chosen price:
    • total with VAT: totalMinor, as before and the default;
    • total without VAT: totalMinor / (1 + v);
    • spot with VAT: spotMinor × (1 + v);
    • spot without VAT: spotMinor.
      It puts the result in totalMinor, so every chart that drew the total draws the choice.
  • Now's strip and the plan's strip take their prices from the price record with the choice applied. Their header says which price it is: "NEXT 12 H · SPOT INCL. VAT", "NÄSTA 12 H · ELPRIS INKL. MOMS". The price page draws the same and its line says "incl. VAT", "spot price only" and so on.
  • The rate on the wire. Prices.vatPermille is read from price.get (Messages.swift) and from FTW Cloud's tile (CloudWire.Prices), and kept in the cache (optional, so an older record still reads). The demo box sends 250.
  • An older box sends no rate. The total then keeps its VAT and the spot price goes without, and the label says what is shown. A price is never shown under the wrong name. Home says why when that happens.
  • Not in this PR: the widgets and the watch still show the total with VAT. Home says so. The phone keeps no rate of its own; the box's is the only one.

Evidence

  • swift test in appleApp/FTWKit: 638 tests in 96 suites and 348 Swift Testing tests pass locally. New: PriceShownTests (Erik's 462 öre: 369.6 total without VAT, 311.25 spot with VAT, 249 spot), PriceWireTests (the answer with 255, with 0, and without the key), and the cloud tile test now carries 250 to the widgets' record.
  • Simulator, iPhone 17, iOS 26.5, demo in Swedish with "With grid fee and tax" off:
Now Home

The default is Fredrik's call

The PR keeps the total with VAT as the default, so nothing changes for anyone who does not touch the switches.

🤖 Generated with Claude Code

frahlg and others added 4 commits November 1, 2026 15:11
Erik, 1 November 2026: Now's strip made 450 against 550 öre look five
times dearer. It put the window's cheapest quarter at the floor and the
dearest at the top.

Every price bar now reads one PriceScale: zero at the foot and a round
top just over the dearest (1, 1.5, 2, 2.5, 3, 4, 5, 6 or 8 times a power
of ten). Now's strip draws the bars from zero, marks half the top and the
top with figures in a gutter on the right, and carries the price now on
as a dashed line. A price below zero hangs under the zero line. The dial's
ticks, the lock-screen glyphs, the plan widget and the watch's bars take
the same scale in eight steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fredrik picked proposal 5 for Now on 1 November 2026. Each run of the
battery and each car window on the strip is a pass (PlanPass, FTWShared,
tested). A tap chooses one: it stands in full colour over a faint column
of its colour, the bars under it take that colour, and three figures
under the strip say when it runs, what it does to the level and the
price it meets. The strip opens on the battery's run under way or next.
A run on spare sun says it buys nothing. With no pass, the sentence
stays; under a finger, the quarter's line still takes its place.

The colours are Phosphor's, not the proposal's: gold gives power, cyan
stores, violet for the car. Nothing glows, since a plan is not live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Erik, 1 November 2026: on the 24-hour dial 18 is not straight down as on
a clock, so you lose the feel for when things happen and have to read the
figures at the edge. Fredrik said yes to round 3 of the design canvas the
same day.

- The dial is a clock: one turn is the next twelve hours, 12 at the top
  and 18 straight down, with the turn's whole hours round the rim. Its
  ticks are as long as the battery's planned power and coloured by what
  it does, gold giving power as on the strip, cyan storing. The price
  leaves the dial. The phone, its widget and the watch draw the same face.
- Under the dial, and as the lanes, the plan shows Now's strip: the
  passes, the price from zero and the chosen pass's figures. A tap on a
  pass lights its ticks on the dial. The lanes run the whole plan with
  the battery's level in a row of its own.
- The old lanes, the story under the dial and the rows under the lanes
  go; the figures say what they said. CLAUDE.md's dial rules follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fredrik, 1 November 2026: he wants the price without grid fees, and to
know whether it is with or without VAT. Home gets "Price shown" with two
switches: with the grid fee and tax, and with VAT. Now's strip, the plan
and the price page draw the chosen price (PriceShown, FTWShared, tested)
and name it: "SPOT INCL. VAT", "ELPRIS INKL. MOMS".

The box sends spotMinor and totalMinor, and with srcfl/ftw#1949 the VAT
rate it taxed at, as vatPermille (250 is 25 %), on price.get and on FTW
Cloud's price tile. The phone reads both and keeps the rate with the
prices. A box that does not send it gets the price the phone has: the
total keeps its VAT, the spot price goes without, and the label says so.
The widgets and the watch still show the total with VAT. The demo box
sends 250.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-01T14:37:53.843087Z 9a29346 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a29346897

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let prices = (entry?.prices ?? []).filter { $0.totalMinor.isFinite && $0.durationMs > 0 }.sorted { $0.startMs < $1.startMs }
// The prices this phone chose to show, where the price record has
// them, else the ones under the plan.
let prices = (shown ?? entry?.prices ?? []).filter { $0.totalMinor.isFinite && $0.durationMs > 0 }.sorted { $0.startMs < $1.startMs }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve plan prices outside the price record's coverage

When the separately fetched price record is older or shorter than the plan, this replaces all of the plan's prices instead of falling back for uncovered quarters. Both Now and Plan now pass PriceModel.prices.slots here, and PriceModel.load() deliberately retains its previous window when fetching fails. Consequently, a newly received plan can contain tomorrow's prices while these strips show missing-price stubs for tomorrow—or no prices at all if the retained window has ended—even with the default display settings. Preserve coverage from the plan, applying the selected price representation consistently, rather than treating any non-nil price array as complete.

Useful? React with 👍 / 👎.

…ip ci]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg changed the base branch from ios-plan-clock to main November 1, 2026 17:50
@frahlg
frahlg merged commit 6844be2 into main Nov 1, 2026
@frahlg
frahlg deleted the ios-price-choice branch November 1, 2026 17:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant