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

@@ -9,28 +9,29 @@ import UIKit
/// Customizes the system shield. Apps under an open-limit rule show how many
/// opens were used and offer an "Open" secondary button while opens remain;
/// everything else gets a plain blocked shield named after its rule.
/// everything else gets a plain blocked shield. Shields never name a rule the
/// text is decided by the pure `ShieldPresentation`.
final class ShieldConfigurationExtension: ShieldConfigurationDataSource {
override func configuration(shielding application: Application) -> ShieldConfiguration {
guard let token = application.token else { return blockedConfiguration(ruleName: nil) }
guard let token = application.token else { return configuration(for: .blocked) }
return configuration(forApplicationToken: token)
}
override func configuration(
shielding application: Application, in category: ActivityCategory
) -> ShieldConfiguration {
guard let token = application.token else { return blockedConfiguration(ruleName: nil) }
guard let token = application.token else { return configuration(for: .blocked) }
return configuration(forApplicationToken: token)
}
override func configuration(shielding webDomain: WebDomain) -> ShieldConfiguration {
blockedConfiguration(ruleName: nil)
configuration(for: .blocked)
}
override func configuration(
shielding webDomain: WebDomain, in category: ActivityCategory
) -> ShieldConfiguration {
blockedConfiguration(ruleName: nil)
configuration(for: .blocked)
}
private func configuration(
@@ -41,45 +42,26 @@ final class ShieldConfigurationExtension: ShieldConfigurationDataSource {
let snapshot = ShieldLookup.openLimitSnapshot(
containingApplication: token, in: snapshots)
else {
let name = ShieldLookup.snapshot(containingApplication: token, in: snapshots)?.name
return blockedConfiguration(ruleName: name)
return configuration(for: .blocked)
}
let usage = UsageLedger().usage(for: snapshot.id, onDayContaining: .now)
let remaining = max(0, snapshot.maxOpens - usage.opensUsed)
guard remaining > 0 else {
return ShieldConfiguration(
backgroundBlurStyle: .systemMaterial,
title: .init(text: snapshot.name, color: .label),
subtitle: .init(
text: "No opens left today — the block lifts tomorrow.",
color: .secondaryLabel),
primaryButtonLabel: .init(text: "OK", color: .white),
primaryButtonBackgroundColor: .systemBlue
)
}
return ShieldConfiguration(
backgroundBlurStyle: .systemMaterial,
title: .init(text: snapshot.name, color: .label),
subtitle: .init(
text: "Opened \(usage.opensUsed) of \(snapshot.maxOpens) times today. "
+ "Each open lasts \(MonitoringPlan.openSessionMinutes) minutes.",
color: .secondaryLabel),
primaryButtonLabel: .init(text: "OK", color: .white),
primaryButtonBackgroundColor: .systemBlue,
secondaryButtonLabel: .init(
text: remaining == 1 ? "Open (1 left)" : "Open (\(remaining) left)",
color: .systemBlue)
)
return configuration(
for: .openLimit(
opensUsed: usage.opensUsed,
maxOpens: snapshot.maxOpens,
sessionMinutes: MonitoringPlan.openSessionMinutes))
}
private func blockedConfiguration(ruleName: String?) -> ShieldConfiguration {
private func configuration(for presentation: ShieldPresentation) -> ShieldConfiguration {
ShieldConfiguration(
backgroundBlurStyle: .systemMaterial,
title: .init(text: ruleName ?? "Blocked", color: .label),
subtitle: .init(text: "This app is blocked by OpenAppLock.", color: .secondaryLabel),
title: .init(text: presentation.title, color: .label),
subtitle: .init(text: presentation.subtitle, color: .secondaryLabel),
primaryButtonLabel: .init(text: "OK", color: .white),
primaryButtonBackgroundColor: .systemBlue
primaryButtonBackgroundColor: .systemBlue,
secondaryButtonLabel: presentation.secondaryButton.map {
.init(text: $0, color: .systemBlue)
}
)
}
}