From 0f588a66ed31f52911713ebae83b0be63245f138 Mon Sep 17 00:00:00 2001 From: Chris Almond Date: Sat, 28 Jan 2023 22:31:32 -0700 Subject: [PATCH 1/3] Add profile expiration time to settings page --- Loop/Managers/ProfileExpirationAlerter.swift | 32 +++++++++++++++++- Loop/Views/SettingsView.swift | 34 ++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/Loop/Managers/ProfileExpirationAlerter.swift b/Loop/Managers/ProfileExpirationAlerter.swift index f262a7d82b..b0cf3aade9 100644 --- a/Loop/Managers/ProfileExpirationAlerter.swift +++ b/Loop/Managers/ProfileExpirationAlerter.swift @@ -14,6 +14,7 @@ import LoopCore class ProfileExpirationAlerter { static let expirationAlertWindow: TimeInterval = .days(20) + static let settingsPageExpirationWarningModeWindow: TimeInterval = .days(3) static func alertIfNeeded(viewControllerToPresentFrom: UIViewController) { @@ -40,9 +41,11 @@ class ProfileExpirationAlerter { formatter.maximumUnitCount = 1 let timeUntilExpirationStr = formatter.string(from: timeUntilExpiration) + let alertMessage = createVerboseAlertMessage(timeUntilExpirationStr: timeUntilExpirationStr!) + let dialog = UIAlertController( title: NSLocalizedString("Profile Expires Soon", comment: "The title for notification of upcoming profile expiration"), - message: String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to update before that, with a new provisioning profile.", comment: "Format string for body for notification of upcoming provisioning profile expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr!), + message: alertMessage, preferredStyle: .alert) dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil)) dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming profile expiration"), style: .default, handler: { (_) in @@ -52,4 +55,31 @@ class ProfileExpirationAlerter { UserDefaults.appGroup?.lastProfileExpirationAlertDate = now } + + static func createVerboseAlertMessage(timeUntilExpirationStr:String) -> String { + return String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to update before that, with a new provisioning profile.", comment: "Format string for body for notification of upcoming provisioning profile expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr) + } + + static func isNearProfileExpiration(profileExpiration:Date) -> Bool { + return profileExpiration.timeIntervalSinceNow < settingsPageExpirationWarningModeWindow + } + + static func createProfileExpirationSettingsMessage(profileExpiration:Date) -> String { + let nearExpiration = isNearProfileExpiration(profileExpiration: profileExpiration) + let maxUnitCount = nearExpiration ? 2 : 1 // only include hours in the msg if near expiration + let readableRelativeTime: String? = relativeTimeFormatter(maxUnitCount: maxUnitCount).string(from: profileExpiration.timeIntervalSinceNow) + let relativeTimeRemaining: String = readableRelativeTime ?? NSLocalizedString("Unknown time", comment: "Unknown amount of time in settings' profile expiration section") + let verboseMessage = createVerboseAlertMessage(timeUntilExpirationStr: relativeTimeRemaining) + let conciseMessage = relativeTimeRemaining + NSLocalizedString(" remaining", comment: "remaining time in setting's profile expiration section") + return nearExpiration ? verboseMessage : conciseMessage + } + + private static func relativeTimeFormatter(maxUnitCount:Int) -> DateComponentsFormatter { + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.day, .hour] + formatter.unitsStyle = .full + formatter.zeroFormattingBehavior = .dropLeading + formatter.maximumUnitCount = maxUnitCount + return formatter; + } } diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index d325019e22..5256658dd0 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -58,6 +58,9 @@ public struct SettingsView: View { servicesSection } supportSection + if let profileExpiration = Bundle.main.profileExpiration { + profileExpirationSection(profileExpiration: profileExpiration) + } } .insetGroupedListStyle() .navigationBarTitle(Text(NSLocalizedString("Settings", comment: "Settings screen title"))) @@ -335,6 +338,37 @@ extension SettingsView { } } } + + private func profileExpirationSection(profileExpiration:Date) -> some View { + let nearExpiration : Bool = ProfileExpirationAlerter.isNearProfileExpiration(profileExpiration: profileExpiration) + let profileExpirationMsg = ProfileExpirationAlerter.createProfileExpirationSettingsMessage(profileExpiration: profileExpiration) + let readableExpirationTime = Self.dateFormatter.string(from: profileExpiration) + + return Section(header: SectionHeader(label: NSLocalizedString("App Profile", comment: "Settings app profile section")), + footer: Text(NSLocalizedString("Profile expires ", comment: "Time that profile expires") + readableExpirationTime)) { + if(nearExpiration) { + Text(profileExpirationMsg).foregroundColor(.red) + } else { + HStack { + Text("Profile Expiration", comment: "Settings App Profile expiration view") + Spacer() + Text(profileExpirationMsg).foregroundColor(Color.secondary) + } + } + Button(action: { + UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) + }) { + Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) + } + } + } + + private static var dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .long + dateFormatter.timeStyle = .short + return dateFormatter // formats date like "February 4, 2023 at 2:35 PM" + }() private var plusImage: some View { Image(systemName: "plus.circle") From 270c139292b29c79f74b725e912aa933ef6cd120 Mon Sep 17 00:00:00 2001 From: Chris Almond Date: Wed, 1 Feb 2023 22:58:23 -0700 Subject: [PATCH 2/3] Add profile expiration view behind feature flag PROFILE_EXPIRATION_SETTINGS_VIEW_ENABLED --- Common/FeatureFlags.swift | 10 +++++++++- Loop/Views/SettingsView.swift | 5 ++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Common/FeatureFlags.swift b/Common/FeatureFlags.swift index 636d99f1ee..4c80272cd7 100644 --- a/Common/FeatureFlags.swift +++ b/Common/FeatureFlags.swift @@ -37,6 +37,7 @@ struct FeatureFlagConfiguration: Decodable { let usePositiveMomentumAndRCForManualBoluses: Bool let dynamicCarbAbsorptionEnabled: Bool let adultChildInsulinModelSelectionEnabled: Bool + let profileExpirationSettingsViewEnabled: Bool fileprivate init() { @@ -212,6 +213,12 @@ struct FeatureFlagConfiguration: Decodable { #endif self.dynamicCarbAbsorptionEnabled = true + + #if PROFILE_EXPIRATION_SETTINGS_VIEW_ENABLED + self.profileExpirationSettingsViewEnabled = true + #else + self.profileExpirationSettingsViewEnabled = false + #endif } } @@ -244,7 +251,8 @@ extension FeatureFlagConfiguration : CustomDebugStringConvertible { "* simpleBolusCalculatorEnabled: \(simpleBolusCalculatorEnabled)", "* usePositiveMomentumAndRCForManualBoluses: \(usePositiveMomentumAndRCForManualBoluses)", "* dynamicCarbAbsorptionEnabled: \(dynamicCarbAbsorptionEnabled)", - "* adultChildInsulinModelSelectionEnabled: \(adultChildInsulinModelSelectionEnabled)" + "* adultChildInsulinModelSelectionEnabled: \(adultChildInsulinModelSelectionEnabled)", + "* profileExpirationSettingsViewEnabled: \(profileExpirationSettingsViewEnabled)" ].joined(separator: "\n") } } diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index 5256658dd0..364f7520bf 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -58,7 +58,7 @@ public struct SettingsView: View { servicesSection } supportSection - if let profileExpiration = Bundle.main.profileExpiration { + if let profileExpiration = Bundle.main.profileExpiration, FeatureFlags.profileExpirationSettingsViewEnabled { profileExpirationSection(profileExpiration: profileExpiration) } } @@ -339,6 +339,9 @@ extension SettingsView { } } + /* + DIY loop specific component to show users the amount of time remaining on their build before a rebuild is necessary. + */ private func profileExpirationSection(profileExpiration:Date) -> some View { let nearExpiration : Bool = ProfileExpirationAlerter.isNearProfileExpiration(profileExpiration: profileExpiration) let profileExpirationMsg = ProfileExpirationAlerter.createProfileExpirationSettingsMessage(profileExpiration: profileExpiration) From 4135b3eb191b50b7468dcbc33d11d3cbf90afd5d Mon Sep 17 00:00:00 2001 From: Chris Almond Date: Sun, 5 Feb 2023 16:31:05 -0700 Subject: [PATCH 3/3] invert feature toggle. Remove hour rendering when hours is 0 --- Common/FeatureFlags.swift | 9 +++++---- Loop/Managers/ProfileExpirationAlerter.swift | 3 ++- Loop/Views/SettingsView.swift | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Common/FeatureFlags.swift b/Common/FeatureFlags.swift index 4c80272cd7..92b1aa1d91 100644 --- a/Common/FeatureFlags.swift +++ b/Common/FeatureFlags.swift @@ -213,11 +213,12 @@ struct FeatureFlagConfiguration: Decodable { #endif self.dynamicCarbAbsorptionEnabled = true - - #if PROFILE_EXPIRATION_SETTINGS_VIEW_ENABLED - self.profileExpirationSettingsViewEnabled = true - #else + + // ProfileExpirationSettingsView is inverse, since the default state is enabled. + #if PROFILE_EXPIRATION_SETTINGS_VIEW_DISABLED self.profileExpirationSettingsViewEnabled = false + #else + self.profileExpirationSettingsViewEnabled = true #endif } } diff --git a/Loop/Managers/ProfileExpirationAlerter.swift b/Loop/Managers/ProfileExpirationAlerter.swift index b0cf3aade9..a28d452783 100644 --- a/Loop/Managers/ProfileExpirationAlerter.swift +++ b/Loop/Managers/ProfileExpirationAlerter.swift @@ -76,7 +76,8 @@ class ProfileExpirationAlerter { private static func relativeTimeFormatter(maxUnitCount:Int) -> DateComponentsFormatter { let formatter = DateComponentsFormatter() - formatter.allowedUnits = [.day, .hour] + let includeHours = maxUnitCount == 2 + formatter.allowedUnits = includeHours ? [.day, .hour] : [.day] formatter.unitsStyle = .full formatter.zeroFormattingBehavior = .dropLeading formatter.maximumUnitCount = maxUnitCount diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index 364f7520bf..90859273e3 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -365,7 +365,7 @@ extension SettingsView { } } } - + private static var dateFormatter: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .long