Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 21 additions & 14 deletions FlipcashCore/Sources/FlipcashCore/Push/NotificationStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ extension NotificationStatus {
/// i4
case registration

/// The check's name, used in the report and the passing row.
/// The check's name: the row's title in every state, and the report's label.
public var name: String {
switch self {
case .permission: "Notifications Allowed"
Expand Down Expand Up @@ -229,7 +229,9 @@ extension NotificationStatus {
public struct Row: Equatable, Identifiable, Sendable {
public var check: Check
public var status: Status
/// The check's name, whatever the status.
public var title: String
/// On a warning or fail, the problem on the first line and any explanation on the second.
public var detail: String?
public var fix: Fix?
/// The raw value behind the status, for the report.
Expand Down Expand Up @@ -347,9 +349,9 @@ extension NotificationStatus {
case .authorized, .provisional, .ephemeral:
return Row(check: .permission, status: .pass, title: Check.permission.name, detail: "Allowed in iOS Settings", raw: raw)
case .denied:
return Row(check: .permission, status: .fail, title: "Notifications are turned off", detail: "Turned off for Flipcash in iOS Settings", fix: .openNotificationSettings, raw: raw)
return Row(check: .permission, status: .fail, title: Check.permission.name, detail: problem("Notifications are turned off", "Turned off for Flipcash in iOS Settings"), fix: .openNotificationSettings, raw: raw)
case .notDetermined:
return Row(check: .permission, status: .fail, title: "Notifications aren't turned on yet", detail: "Flipcash hasn't asked yet", fix: .requestAuthorization, raw: raw)
return Row(check: .permission, status: .fail, title: Check.permission.name, detail: problem("Notifications aren't turned on yet", "Flipcash hasn't asked yet"), fix: .requestAuthorization, raw: raw)
}
}

Expand All @@ -364,16 +366,16 @@ extension NotificationStatus {
let raw = off.isEmpty ? nil : off.joined(separator: ", ")

if bannersOff {
return Row(check: .display, status: .warning, title: "Banners are off", detail: "New notifications only go to Notification Center", fix: .openNotificationSettings, raw: raw)
return Row(check: .display, status: .warning, title: Check.display.name, detail: problem("Banners are off", "New notifications only go to Notification Center"), fix: .openNotificationSettings, raw: raw)
}
if lockScreenOff {
return Row(check: .display, status: .warning, title: "Hidden on the Lock Screen", fix: .openNotificationSettings, raw: raw)
return Row(check: .display, status: .warning, title: Check.display.name, detail: problem("Hidden on the Lock Screen"), fix: .openNotificationSettings, raw: raw)
}
return Row(check: .display, status: .pass, title: Check.display.name, detail: "Both on", raw: raw)
}

static func soundsAndBadgesRow(_ snapshot: Snapshot) -> Row {
let title: String? = switch (snapshot.sound.isOff, snapshot.badge.isOff) {
let problem: String? = switch (snapshot.sound.isOff, snapshot.badge.isOff) {
case (true, true): "Sounds and badges are off"
case (true, false): "Sounds are off"
case (false, true): "Badges are off"
Expand All @@ -385,31 +387,31 @@ extension NotificationStatus {
if snapshot.badge.isOff { off.append("badge disabled") }
let raw = off.isEmpty ? nil : off.joined(separator: ", ")

guard let title else {
guard let problem else {
return Row(check: .soundsAndBadges, status: .pass, title: Check.soundsAndBadges.name, detail: "Both on", raw: raw)
}
return Row(check: .soundsAndBadges, status: .warning, title: title, fix: .openNotificationSettings, raw: raw)
return Row(check: .soundsAndBadges, status: .warning, title: Check.soundsAndBadges.name, detail: Self.problem(problem), fix: .openNotificationSettings, raw: raw)
}

static func registrationRow(_ registration: Registration, saved: RegistrationRecord?, now: Date, locale: Locale) -> Row {
let name = Check.registration.name
switch registration {
case .checking:
return Row(check: .registration, status: .checking, title: "Checking registration…", detail: "Contacting Flipcash", raw: "checking")
return Row(check: .registration, status: .checking, title: name, detail: "Contacting Flipcash", raw: "checking")
case .noAPNsToken:
return Row(check: .registration, status: .fail, title: "This phone isn't registered with Apple", fix: .retryRegistration, raw: "noAPNsToken")
return Row(check: .registration, status: .fail, title: name, detail: problem("This phone isn't registered with Apple"), fix: .retryRegistration, raw: "noAPNsToken")
case .finished(.ok):
return Row(check: .registration, status: .pass, title: name, detail: "Confirmed just now", raw: RegistrationResult.ok.rawValue)
case .finished(.invalidPushToken):
return Row(check: .registration, status: .fail, title: "Flipcash rejected this phone's push token", fix: .retryRegistrationDeletingToken, raw: RegistrationResult.invalidPushToken.rawValue)
return Row(check: .registration, status: .fail, title: name, detail: problem("Flipcash rejected this phone's push token"), fix: .retryRegistrationDeletingToken, raw: RegistrationResult.invalidPushToken.rawValue)
case .finished(.transport):
let raw = RegistrationResult.transport.rawValue
if let okAt = saved?.lastRegistrationOkAt {
return Row(check: .registration, status: .warning, title: "Couldn't reach Flipcash", detail: "Last confirmed \(relative(okAt, to: now, locale: locale))", fix: .retryRegistration, raw: raw)
return Row(check: .registration, status: .warning, title: name, detail: problem("Couldn't reach Flipcash", "Last confirmed \(relative(okAt, to: now, locale: locale))"), fix: .retryRegistration, raw: raw)
}
return Row(check: .registration, status: .fail, title: "Couldn't reach Flipcash", detail: "Check your connection", fix: .retryRegistration, raw: raw)
return Row(check: .registration, status: .fail, title: name, detail: problem("Couldn't reach Flipcash", "Check your connection"), fix: .retryRegistration, raw: raw)
case .finished(.unknown):
return Row(check: .registration, status: .fail, title: "Registration failed", fix: .retryRegistration, raw: RegistrationResult.unknown.rawValue)
return Row(check: .registration, status: .fail, title: name, detail: problem("Registration failed"), fix: .retryRegistration, raw: RegistrationResult.unknown.rawValue)
}
}

Expand All @@ -435,6 +437,11 @@ extension NotificationStatus {
return Summary(status: .pass, title: "Notifications are working", detail: checked)
}

/// A problem row's detail: the problem, then the explanation on its own line.
static func problem(_ problem: String, _ explanation: String? = nil) -> String {
[problem, explanation].compactMap { $0 }.joined(separator: "\n")
}

static func plural(_ count: Int, _ one: String, _ many: String) -> String {
"\(count) \(count == 1 ? one : many)"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ struct NotificationStatusTests {
#expect(page.summary.status == .fail)
#expect(page.summary.title == "Notifications are off")
#expect(page.rows.map(\.check) == [.permission, .registration])
#expect(Self.row(.permission, in: page)?.title == "Notifications Allowed")
#expect(Self.row(.permission, in: page)?.detail == "Notifications are turned off\nTurned off for Flipcash in iOS Settings")
#expect(Self.row(.permission, in: page)?.fix == .openNotificationSettings)
#expect(Self.row(.permission, in: page)?.fixTitle == "Turn On")
#expect(page.isTestEnabled == false)
Expand All @@ -66,8 +68,8 @@ struct NotificationStatusTests {
let row = Self.row(.permission, in: Self.page(.init(authorization: .notDetermined)))

#expect(row?.status == .fail)
#expect(row?.title == "Notifications aren't turned on yet")
#expect(row?.detail == "Flipcash hasn't asked yet")
#expect(row?.title == "Notifications Allowed")
#expect(row?.detail == "Notifications aren't turned on yet\nFlipcash hasn't asked yet")
#expect(row?.fix == .requestAuthorization)
#expect(row?.fixTitle == "Turn On")
}
Expand All @@ -78,7 +80,8 @@ struct NotificationStatusTests {
let row = Self.row(.display, in: page)

#expect(row?.status == .warning)
#expect(row?.title == "Banners are off")
#expect(row?.title == "Banners and Lock Screen")
#expect(row?.detail == "Banners are off\nNew notifications only go to Notification Center")
#expect(row?.fix == .openNotificationSettings)
#expect(row?.fixTitle == "Fix")
#expect(page.summary.status == .warning)
Expand All @@ -92,19 +95,21 @@ struct NotificationStatusTests {
let row = Self.row(.display, in: Self.page(.init(authorization: .authorized, lockScreen: .disabled)))

#expect(row?.status == .warning)
#expect(row?.title == "Hidden on the Lock Screen")
#expect(row?.title == "Banners and Lock Screen")
#expect(row?.detail == "Hidden on the Lock Screen")
}

@Test("Sounds and badges name what is off", arguments: [
(NotificationStatus.Snapshot.Setting.disabled, NotificationStatus.Snapshot.Setting.enabled, "Sounds are off"),
(.enabled, .disabled, "Badges are off"),
(.disabled, .disabled, "Sounds and badges are off"),
])
func soundsAndBadgesOff(sound: NotificationStatus.Snapshot.Setting, badge: NotificationStatus.Snapshot.Setting, title: String) {
func soundsAndBadgesOff(sound: NotificationStatus.Snapshot.Setting, badge: NotificationStatus.Snapshot.Setting, problem: String) {
let row = Self.row(.soundsAndBadges, in: Self.page(.init(authorization: .authorized, sound: sound, badge: badge)))

#expect(row?.status == .warning)
#expect(row?.title == title)
#expect(row?.title == "Sounds and Badges")
#expect(row?.detail == problem)
}

@Test("10: an invalid push token fails and its retry deletes the local token first")
Expand All @@ -113,7 +118,8 @@ struct NotificationStatusTests {
let row = Self.row(.registration, in: page)

#expect(row?.status == .fail)
#expect(row?.title == "Flipcash rejected this phone's push token")
#expect(row?.title == "Registered for Push")
#expect(row?.detail == "Flipcash rejected this phone's push token")
#expect(row?.fix == .retryRegistrationDeletingToken)
#expect(row?.fixTitle == "Try Again")
#expect(page.summary.title == "1 thing to fix")
Expand All @@ -125,8 +131,8 @@ struct NotificationStatusTests {
let row = Self.row(.registration, in: Self.page(registration: .finished(.transport), saved: saved))

#expect(row?.status == .fail)
#expect(row?.title == "Couldn't reach Flipcash")
#expect(row?.detail == "Check your connection")
#expect(row?.title == "Registered for Push")
#expect(row?.detail == "Couldn't reach Flipcash\nCheck your connection")
#expect(row?.fix == .retryRegistration)
}

Expand All @@ -137,8 +143,8 @@ struct NotificationStatusTests {
let row = Self.row(.registration, in: page)

#expect(row?.status == .warning)
#expect(row?.title == "Couldn't reach Flipcash")
#expect(row?.detail == "Last confirmed 5 minutes ago")
#expect(row?.title == "Registered for Push")
#expect(row?.detail == "Couldn't reach Flipcash\nLast confirmed 5 minutes ago")
#expect(page.summary.title == "1 thing to check")
}

Expand All @@ -148,23 +154,23 @@ struct NotificationStatusTests {
let row = Self.row(.registration, in: Self.page(registration: .finished(.transport), saved: saved))

#expect(row?.status == .warning)
#expect(row?.detail == "Last confirmed 2 hours ago")
#expect(row?.detail == "Couldn't reach Flipcash\nLast confirmed 2 hours ago")
}

@Test("A save under a minute old reads just now, not 0 minutes ago", arguments: [0.0, -1, -59])
func transportWithFreshSavedOK(offset: TimeInterval) {
let saved = NotificationStatus.RegistrationRecord(at: Self.now.addingTimeInterval(offset), result: .ok, source: .refresh, lastRegistrationOkAt: Self.now.addingTimeInterval(offset))
let page = Self.page(registration: .finished(.transport), saved: saved)

#expect(Self.row(.registration, in: page)?.detail == "Last confirmed just now")
#expect(Self.row(.registration, in: page)?.detail == "Couldn't reach Flipcash\nLast confirmed just now")
}

@Test("A save a minute old reads in minutes")
func transportWithMinuteOldSavedOK() {
let saved = NotificationStatus.RegistrationRecord(at: Self.now.addingTimeInterval(-60), result: .ok, source: .refresh, lastRegistrationOkAt: Self.now.addingTimeInterval(-60))
let page = Self.page(registration: .finished(.transport), saved: saved)

#expect(Self.row(.registration, in: page)?.detail == "Last confirmed 1 minute ago")
#expect(Self.row(.registration, in: page)?.detail == "Couldn't reach Flipcash\nLast confirmed 1 minute ago")
}

@Test("Group headers are title case")
Expand All @@ -179,7 +185,7 @@ struct NotificationStatusTests {
let row = Self.row(.registration, in: page)

#expect(row?.status == .checking)
#expect(row?.title == "Checking registration…")
#expect(row?.title == "Registered for Push")
#expect(row?.detail == "Contacting Flipcash")
#expect(page.summary.status == .checking)
#expect(page.summary.title == "Checking…")
Expand All @@ -192,7 +198,7 @@ struct NotificationStatusTests {

#expect(page.rows.map(\.check) == NotificationStatus.Check.allCases)
#expect(page.rows.allSatisfy { $0.status == .checking })
#expect(Self.row(.registration, in: page)?.title == "Checking registration…")
#expect(page.rows.map(\.title) == NotificationStatus.Check.allCases.map(\.name))
#expect(page.summary.status == .checking)
#expect(page.isTestEnabled == false)
#expect(page.foldedTitle == nil)
Expand Down Expand Up @@ -252,7 +258,8 @@ struct NotificationStatusTests {
func noAPNsToken() {
let page = Self.page(registration: .noAPNsToken)
#expect(page.summary.title == "1 thing to fix")
#expect(Self.row(.registration, in: page)?.title == "This phone isn't registered with Apple")
#expect(Self.row(.registration, in: page)?.title == "Registered for Push")
#expect(Self.row(.registration, in: page)?.detail == "This phone isn't registered with Apple")
}

@Test("14: notSupported settings are not counted as off")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ struct NotificationStatusViewModelTests {
let row = try #require(model.page?.rows.first { $0.check == .registration })
#expect(store.record?.result == .transport)
#expect(row.status == .warning)
#expect(row.detail == "Last confirmed 5 minutes ago")
#expect(row.title == "Registered for Push")
#expect(row.detail == "Couldn't reach Flipcash\nLast confirmed 5 minutes ago")
#expect(model.page?.summary.status == .warning)
}

Expand Down
Loading