fix: show a generic "App Blocked" shield instead of mis-attributing a rule

The shield titled itself with the first enabled rule whose selection
contained the app, regardless of whether that rule was the one actually
blocking. When several rules covered the same app (e.g. a dormant
schedule + the active limit) it named the wrong rule.

Drop the rule-name lookup entirely: every shield now carries a generic
"App Blocked" title. Open-limit shields keep their functional detail
(running count + "Open (N left)" button) beneath that title. The
text-only decision moves into a pure, unit-tested ShieldPresentation in
Shared/, and the now-dead ShieldLookup.snapshot(containingApplication:)
is removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-13 15:32:26 -04:00
parent d92a8588bb
commit e5f571a63e
5 changed files with 127 additions and 52 deletions

View File

@@ -29,16 +29,4 @@ enum ShieldLookup {
.categoryTokens.contains(token)
}
}
/// Any enabled rule whose selection includes the application (for naming
/// plain blocked shields).
static func snapshot(
containingApplication token: ApplicationToken, in snapshots: [RuleSnapshot]
) -> RuleSnapshot? {
snapshots.first { snapshot in
guard snapshot.isEnabled else { return false }
return AppSelectionCodec.decode(snapshot.selectionData)
.applicationTokens.contains(token)
}
}
}

View File

@@ -0,0 +1,47 @@
//
// ShieldPresentation.swift
// OpenAppLock
//
/// The text a shield shows, decided independently of any UI type so it can be
/// unit tested. Shields never name the rule that blocks an app: when several
/// rules cover the same app the responsible rule can't be told apart, so every
/// shield carries the same generic "App Blocked" title. Open-limit shields add
/// their functional detail (remaining opens + an "Open" button) beneath it.
struct ShieldPresentation: Equatable {
let title: String
let subtitle: String
/// Label for the secondary "Open" button, or `nil` when the shield offers
/// no way through (a full block, or a spent open-limit budget).
let secondaryButton: String?
static let blockedTitle = "App Blocked"
/// A plain, fully-blocked app: no counts, no way through.
static let blocked = ShieldPresentation(
title: blockedTitle,
subtitle: "This app is blocked by OpenAppLock.",
secondaryButton: nil
)
/// An open-limit shield. While opens remain it shows the running count and
/// an "Open (N left)" button; once spent it reads like a plain block.
static func openLimit(
opensUsed: Int, maxOpens: Int, sessionMinutes: Int
) -> ShieldPresentation {
let remaining = max(0, maxOpens - opensUsed)
guard remaining > 0 else {
return ShieldPresentation(
title: blockedTitle,
subtitle: "No opens left today — the block lifts tomorrow.",
secondaryButton: nil
)
}
return ShieldPresentation(
title: blockedTitle,
subtitle: "Opened \(opensUsed) of \(maxOpens) times today. "
+ "Each open lasts \(sessionMinutes) minutes.",
secondaryButton: remaining == 1 ? "Open (1 left)" : "Open (\(remaining) left)"
)
}
}