Compare commits

..
Author SHA1 Message Date
FuelBoard Contributor f04a0fe0f5 Key sheet: gate the How Best Value row on the feature, like the More info row
The More info row went inside the Best Value card's `if let fuelSavings`, but the
Key sheet row was left ungated, so it stayed visible with Best Value switched off.
It is now gated on `fuelSavingsSettings.isConfigured` — exactly the condition
`savingsForStation` uses, i.e. the same gate as the card — so the two rows appear
and disappear together.

Verified on the simulator by toggling Best Value in Settings and scrolling the Key
sheet to its end (four drags, sheet still up and its footer in view each time):
- OFF: the row is absent at every scroll position, and the More info sheet shows
  neither the card nor the row;
- ON: the row is present, and the More info card and its row are both back.
210 tests pass; Release builds.
2026-10-14 15:19:20 +01:00
FuelBoard Contributor f35f605b58 Onboarding welcome: the app icon stands on its own, with no circle behind it
The first onboarding screen showed the app icon inside AnimatedOnboardingIcon's
soft circle — a 10% tinted disc with an 18% ring around it. The wrapper now takes
`showsHalo`, defaulting to true so the other five pages and BestValueIntroView keep
theirs unchanged; the welcome page passes false.

Verified with before/after installs on the simulator:
- the icon's artwork is pixel-identical and unmoved (centre 603,661, 95 pt);
- the halo is gone — the tile's non-background footprint falls from the 118 pt
  ring to exactly the 96 pt tile, and the ring pixels at r=59 pt lose their tint;
- page 1 differs only by the status-bar clock, so the other pages are untouched.

The copy below the icon tightens by ~11 pt: the page block is centred between two
spacers and the icon's container no longer contributes 22 pt of height. The page
dots and the Continue button do not move. The icon keeps its soft accent shadow.
2026-10-14 15:02:29 +01:00
FuelBoard Contributor 7da277547b More info sheet: the copy confirmation reads "Postcode copied"
The capsule label was just "Copied", which reads oddly above a row whose hint is
"Copies the postcode". It is now "Postcode copied".

Still centred over the address row (centre x 603 of a 1206 px screen) and still a
pure overlay: the bands above and below the capsule are pixel-identical while it
is up (no reflow), and the row stays tappable straight through it.

The "Copied" localisation key goes with its only use; "Postcode copied" replaces it.
2026-10-14 14:52:07 +01:00
4 changed files with 29 additions and 21 deletions
+15 -10
View File
@@ -98,7 +98,7 @@ struct OnboardingView: View {
private var welcomePage: some View { private var welcomePage: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
Spacer() Spacer()
AnimatedOnboardingIcon(tint: .accentColor) { AnimatedOnboardingIcon(tint: .accentColor, showsHalo: false) {
// The app icon itself, with the standard iOS rounded-corner // The app icon itself, with the standard iOS rounded-corner
// treatment (~22% of the size), so onboarding matches the // treatment (~22% of the size), so onboarding matches the
// Home Screen. // Home Screen.
@@ -393,6 +393,9 @@ struct AnimatedOnboardingIcon<Content: View>: View {
let tint: Color let tint: Color
var motion: Motion = .gentle var motion: Motion = .gentle
/// The soft circle behind the glyph. The welcome page turns it off: it shows
/// the app icon itself and reads better with no container around it.
var showsHalo: Bool = true
@ViewBuilder var content: Content @ViewBuilder var content: Content
@State private var settled = false @State private var settled = false
@@ -402,16 +405,18 @@ struct AnimatedOnboardingIcon<Content: View>: View {
var body: some View { var body: some View {
ZStack { ZStack {
Circle() if showsHalo {
.fill(tint.opacity(0.10)) Circle()
.frame(width: 108, height: 108) .fill(tint.opacity(0.10))
.scaleEffect(settled ? 1.0 : 0.94) .frame(width: 108, height: 108)
.scaleEffect(settled ? 1.0 : 0.94)
Circle() Circle()
.stroke(tint.opacity(0.18), lineWidth: 1) .stroke(tint.opacity(0.18), lineWidth: 1)
.frame(width: 118, height: 118) .frame(width: 118, height: 118)
.scaleEffect(settled ? 1.03 : 0.97) .scaleEffect(settled ? 1.03 : 0.97)
.opacity(settled ? 1.0 : 0.55) .opacity(settled ? 1.0 : 0.55)
}
content content
.scaleEffect(iconScale) .scaleEffect(iconScale)
+5 -5
View File
@@ -29,7 +29,7 @@ struct StationDetailView: View {
var onToggleFavourite: (() -> Void)? = nil var onToggleFavourite: (() -> Void)? = nil
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var showingReportIssueConfirmation = false @State private var showingReportIssueConfirmation = false
/// Transient "Copied" confirmation on the Address row. Cleared by a task so /// Transient "Postcode copied" confirmation on the Address row. Cleared by a task so
/// the caption fades out on its own. /// the caption fades out on its own.
@State private var showingCopied = false @State private var showingCopied = false
@State private var copyResetTask: Task<Void, Never>? @State private var copyResetTask: Task<Void, Never>?
@@ -137,7 +137,7 @@ struct StationDetailView: View {
// The whole row is a copy control: it puts JUST the postcode // The whole row is a copy control: it puts JUST the postcode
// on the clipboard so it can be pasted into another // on the clipboard so it can be pasted into another
// navigation app. The copy glyph sits ON the address line, // navigation app. The copy glyph sits ON the address line,
// and the "Copied" confirmation is a capsule that floats OVER // and the "Postcode copied" confirmation is a capsule that floats OVER
// THIS ROW — it confirms the address, so it must not hover // THIS ROW — it confirms the address, so it must not hover
// over the map. It is an overlay, so nothing moves while it is // over the map. It is an overlay, so nothing moves while it is
// up, and `allowsHitTesting(false)` keeps the row tappable // up, and `allowsHitTesting(false)` keeps the row tappable
@@ -172,7 +172,7 @@ struct StationDetailView: View {
if showingCopied { if showingCopied {
HStack(spacing: 4) { HStack(spacing: 4) {
Image(systemName: "checkmark") Image(systemName: "checkmark")
Text("Copied") Text("Postcode copied")
} }
.font(.footnote.weight(.semibold)) .font(.footnote.weight(.semibold))
.foregroundStyle(.green) .foregroundStyle(.green)
@@ -296,12 +296,12 @@ struct StationDetailView: View {
) )
} }
} }
// The "Copied" fade-out task must not outlive the sheet. // The "Postcode copied" fade-out task must not outlive the sheet.
.onDisappear { copyResetTask?.cancel() } .onDisappear { copyResetTask?.cancel() }
#if DEBUG #if DEBUG
// QA hook: `-qaCopyPostcode` runs the REAL copy on open, so the // QA hook: `-qaCopyPostcode` runs the REAL copy on open, so the
// clipboard value (read back with `xcrun simctl pbpaste`) and the // clipboard value (read back with `xcrun simctl pbpaste`) and the
// "Copied" confirmation can be verified without a tap — simctl // "Postcode copied" confirmation can be verified without a tap — simctl
// cannot drive taps into the Simulator. // cannot drive taps into the Simulator.
.task { .task {
guard ProcessInfo.processInfo.arguments.contains("-qaCopyPostcode") else { return } guard ProcessInfo.processInfo.arguments.contains("-qaCopyPostcode") else { return }
+8 -5
View File
@@ -317,11 +317,14 @@ struct StationsView: View {
} }
// The explanation is its own sheet now, opened from this // The explanation is its own sheet now, opened from this
// row. Deliberately NOT gated by Best Value: the Key sheet // row. Gated exactly like the More info row and the Best
// is the legend and is reachable whatever the feature is // Value card itself: `isConfigured` is the same condition
// doing. // `savingsForStation` uses, so the row appears and
Section { // disappears with the feature.
HowBestValueRow { showBestValueInfo = true } if fuelSavingsSettings.isConfigured {
Section {
HowBestValueRow { showBestValueInfo = true }
}
} }
} }
.navigationTitle("Key") .navigationTitle("Key")
+1 -1
View File
@@ -71,8 +71,8 @@
"Name" = "Name"; "Name" = "Name";
"Address" = "Address"; "Address" = "Address";
"Distance" = "Distance"; "Distance" = "Distance";
"Copied" = "Copied";
"Copies the postcode" = "Copies the postcode"; "Copies the postcode" = "Copies the postcode";
"Postcode copied" = "Postcode copied";
"Address: %@" = "Address: %@"; "Address: %@" = "Address: %@";
"Directions to %@" = "Directions to %@"; "Directions to %@" = "Directions to %@";
"Opens Apple Maps with driving directions" = "Opens Apple Maps with driving directions"; "Opens Apple Maps with driving directions" = "Opens Apple Maps with driving directions";