|
| 1 | +diff --git a/Loop/Loop/Managers/ProfileExpirationAlerter.swift b/Loop/Managers/ProfileExpirationAlerter.swift |
| 2 | +index a28d4527..068e6327 100644 |
| 3 | +--- a/Loop/Loop/Managers/ProfileExpirationAlerter.swift |
| 4 | ++++ b/Loop/Loop/Managers/ProfileExpirationAlerter.swift |
| 5 | +@@ -19,13 +19,19 @@ class ProfileExpirationAlerter { |
| 6 | + static func alertIfNeeded(viewControllerToPresentFrom: UIViewController) { |
| 7 | + |
| 8 | + let now = Date() |
| 9 | +- |
| 10 | +- guard let profileExpiration = Bundle.main.profileExpiration, now > profileExpiration - expirationAlertWindow else { |
| 11 | ++ |
| 12 | ++ guard let profileExpiration = Bundle.main.profileExpiration else { |
| 13 | + return |
| 14 | + } |
| 15 | + |
| 16 | +- let timeUntilExpiration = profileExpiration.timeIntervalSince(now) |
| 17 | ++ let expirationDate = calculateExpirationDate(profileExpiration: profileExpiration) |
| 18 | ++ |
| 19 | ++ let timeUntilExpiration = expirationDate.timeIntervalSince(now) |
| 20 | + |
| 21 | ++ if timeUntilExpiration > expirationAlertWindow { |
| 22 | ++ return |
| 23 | ++ } |
| 24 | ++ |
| 25 | + let minimumTimeBetweenAlerts: TimeInterval = timeUntilExpiration > .hours(24) ? .days(2) : .hours(1) |
| 26 | + |
| 27 | + if let lastAlertDate = UserDefaults.appGroup?.lastProfileExpirationAlertDate { |
| 28 | +@@ -43,31 +49,48 @@ class ProfileExpirationAlerter { |
| 29 | + |
| 30 | + let alertMessage = createVerboseAlertMessage(timeUntilExpirationStr: timeUntilExpirationStr!) |
| 31 | + |
| 32 | +- let dialog = UIAlertController( |
| 33 | +- title: NSLocalizedString("Profile Expires Soon", comment: "The title for notification of upcoming profile expiration"), |
| 34 | +- message: alertMessage, |
| 35 | +- preferredStyle: .alert) |
| 36 | +- dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil)) |
| 37 | +- dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming profile expiration"), style: .default, handler: { (_) in |
| 38 | +- UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) |
| 39 | +- })) |
| 40 | ++ var dialog: UIAlertController |
| 41 | ++ if isTestFlightBuild() { |
| 42 | ++ dialog = UIAlertController( |
| 43 | ++ title: NSLocalizedString("TestFlight Expires Soon", comment: "The title for notification of upcoming TestFlight expiration"), |
| 44 | ++ message: alertMessage, |
| 45 | ++ preferredStyle: .alert) |
| 46 | ++ dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming TestFlight expiration"), style: .default, handler: nil)) |
| 47 | ++ dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming TestFlight expiration"), style: .default, handler: { (_) in |
| 48 | ++ UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/")!) |
| 49 | ++ })) |
| 50 | ++ |
| 51 | ++ } else { |
| 52 | ++ dialog = UIAlertController( |
| 53 | ++ title: NSLocalizedString("Profile Expires Soon", comment: "The title for notification of upcoming profile expiration"), |
| 54 | ++ message: alertMessage, |
| 55 | ++ preferredStyle: .alert) |
| 56 | ++ dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil)) |
| 57 | ++ dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming profile expiration"), style: .default, handler: { (_) in |
| 58 | ++ UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) |
| 59 | ++ })) |
| 60 | ++ } |
| 61 | + viewControllerToPresentFrom.present(dialog, animated: true, completion: nil) |
| 62 | + |
| 63 | + UserDefaults.appGroup?.lastProfileExpirationAlertDate = now |
| 64 | + } |
| 65 | + |
| 66 | + static func createVerboseAlertMessage(timeUntilExpirationStr:String) -> String { |
| 67 | +- 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) |
| 68 | ++ if isTestFlightBuild() { |
| 69 | ++ return String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to rebuild before that.", comment: "Format string for body for notification of upcoming expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr) |
| 70 | ++ } else { |
| 71 | ++ 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) |
| 72 | ++ } |
| 73 | + } |
| 74 | + |
| 75 | +- static func isNearProfileExpiration(profileExpiration:Date) -> Bool { |
| 76 | +- return profileExpiration.timeIntervalSinceNow < settingsPageExpirationWarningModeWindow |
| 77 | ++ static func isNearExpiration(expirationDate:Date) -> Bool { |
| 78 | ++ return expirationDate.timeIntervalSinceNow < settingsPageExpirationWarningModeWindow |
| 79 | + } |
| 80 | + |
| 81 | +- static func createProfileExpirationSettingsMessage(profileExpiration:Date) -> String { |
| 82 | +- let nearExpiration = isNearProfileExpiration(profileExpiration: profileExpiration) |
| 83 | ++ static func createProfileExpirationSettingsMessage(expirationDate:Date) -> String { |
| 84 | ++ let nearExpiration = isNearExpiration(expirationDate: expirationDate) |
| 85 | + let maxUnitCount = nearExpiration ? 2 : 1 // only include hours in the msg if near expiration |
| 86 | +- let readableRelativeTime: String? = relativeTimeFormatter(maxUnitCount: maxUnitCount).string(from: profileExpiration.timeIntervalSinceNow) |
| 87 | ++ let readableRelativeTime: String? = relativeTimeFormatter(maxUnitCount: maxUnitCount).string(from: expirationDate.timeIntervalSinceNow) |
| 88 | + let relativeTimeRemaining: String = readableRelativeTime ?? NSLocalizedString("Unknown time", comment: "Unknown amount of time in settings' profile expiration section") |
| 89 | + let verboseMessage = createVerboseAlertMessage(timeUntilExpirationStr: relativeTimeRemaining) |
| 90 | + let conciseMessage = relativeTimeRemaining + NSLocalizedString(" remaining", comment: "remaining time in setting's profile expiration section") |
| 91 | +@@ -81,6 +104,50 @@ class ProfileExpirationAlerter { |
| 92 | + formatter.unitsStyle = .full |
| 93 | + formatter.zeroFormattingBehavior = .dropLeading |
| 94 | + formatter.maximumUnitCount = maxUnitCount |
| 95 | +- return formatter; |
| 96 | ++ return formatter |
| 97 | ++ } |
| 98 | ++ |
| 99 | ++ static func buildDate() -> Date? { |
| 100 | ++ let dateFormatter = DateFormatter() |
| 101 | ++ dateFormatter.dateFormat = "EEE MMM d HH:mm:ss 'UTC' yyyy" |
| 102 | ++ dateFormatter.locale = Locale(identifier: "en_US_POSIX") // Set locale to ensure parsing works |
| 103 | ++ dateFormatter.timeZone = TimeZone(identifier: "UTC") |
| 104 | ++ |
| 105 | ++ guard let dateString = Bundle.main.buildDateString, |
| 106 | ++ let date = dateFormatter.date(from: dateString) else { |
| 107 | ++ return nil |
| 108 | ++ } |
| 109 | ++ |
| 110 | ++ return date |
| 111 | ++ } |
| 112 | ++ |
| 113 | ++ static func isTestFlightBuild() -> Bool { |
| 114 | ++ // If an "embedded.mobileprovision" is present in the main bundle, then |
| 115 | ++ // this is an Xcode, Ad-Hoc, or Enterprise distribution. Return false. |
| 116 | ++ if Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision") != nil { |
| 117 | ++ return false |
| 118 | ++ } |
| 119 | ++ |
| 120 | ++ // If an app store receipt is not present in the main bundle, then we cannot |
| 121 | ++ // say whether this is a TestFlight or App Store distribution. Return false. |
| 122 | ++ guard let receiptName = Bundle.main.appStoreReceiptURL?.lastPathComponent else { |
| 123 | ++ return false |
| 124 | ++ } |
| 125 | ++ |
| 126 | ++ // A TestFlight distribution presents a "sandboxReceipt", while an App Store |
| 127 | ++ // distribution presents a "receipt". Return true if we have a TestFlight receipt. |
| 128 | ++ return "sandboxReceipt".caseInsensitiveCompare(receiptName) == .orderedSame |
| 129 | ++ } |
| 130 | ++ |
| 131 | ++ static func calculateExpirationDate(profileExpiration: Date) -> Date { |
| 132 | ++ let isTestFlight = isTestFlightBuild() |
| 133 | ++ |
| 134 | ++ if isTestFlight, let buildDate = buildDate() { |
| 135 | ++ let testflightExpiration = Calendar.current.date(byAdding: .day, value: 90, to: buildDate)! |
| 136 | ++ |
| 137 | ++ return profileExpiration < testflightExpiration ? profileExpiration : testflightExpiration |
| 138 | ++ } else { |
| 139 | ++ return profileExpiration |
| 140 | ++ } |
| 141 | + } |
| 142 | + } |
| 143 | +diff --git a/Loop/Loop/Views/SettingsView.swift b/Loop/Loop/Views/SettingsView.swift |
| 144 | +index 90859273..e41ebfa2 100644 |
| 145 | +--- a/Loop/Loop/Views/SettingsView.swift |
| 146 | ++++ b/Loop/Loop/Views/SettingsView.swift |
| 147 | +@@ -343,25 +343,48 @@ extension SettingsView { |
| 148 | + DIY loop specific component to show users the amount of time remaining on their build before a rebuild is necessary. |
| 149 | + */ |
| 150 | + private func profileExpirationSection(profileExpiration:Date) -> some View { |
| 151 | +- let nearExpiration : Bool = ProfileExpirationAlerter.isNearProfileExpiration(profileExpiration: profileExpiration) |
| 152 | +- let profileExpirationMsg = ProfileExpirationAlerter.createProfileExpirationSettingsMessage(profileExpiration: profileExpiration) |
| 153 | +- let readableExpirationTime = Self.dateFormatter.string(from: profileExpiration) |
| 154 | ++ let expirationDate = ProfileExpirationAlerter.calculateExpirationDate(profileExpiration: profileExpiration) |
| 155 | ++ let isTestFlight = ProfileExpirationAlerter.isTestFlightBuild() |
| 156 | + |
| 157 | +- return Section(header: SectionHeader(label: NSLocalizedString("App Profile", comment: "Settings app profile section")), |
| 158 | +- footer: Text(NSLocalizedString("Profile expires ", comment: "Time that profile expires") + readableExpirationTime)) { |
| 159 | +- if(nearExpiration) { |
| 160 | +- Text(profileExpirationMsg).foregroundColor(.red) |
| 161 | +- } else { |
| 162 | +- HStack { |
| 163 | +- Text("Profile Expiration", comment: "Settings App Profile expiration view") |
| 164 | +- Spacer() |
| 165 | +- Text(profileExpirationMsg).foregroundColor(Color.secondary) |
| 166 | ++ let nearExpiration : Bool = ProfileExpirationAlerter.isNearExpiration(expirationDate: expirationDate) |
| 167 | ++ let profileExpirationMsg = ProfileExpirationAlerter.createProfileExpirationSettingsMessage(expirationDate: expirationDate) |
| 168 | ++ let readableExpirationTime = Self.dateFormatter.string(from: expirationDate) |
| 169 | ++ |
| 170 | ++ if isTestFlight { |
| 171 | ++ return Section(header: SectionHeader(label: NSLocalizedString("TestFlight", comment: "Settings app TestFlight section")), |
| 172 | ++ footer: Text(NSLocalizedString("TestFlight expires ", comment: "Time that build expires") + readableExpirationTime)) { |
| 173 | ++ if(nearExpiration) { |
| 174 | ++ Text(profileExpirationMsg).foregroundColor(.red) |
| 175 | ++ } else { |
| 176 | ++ HStack { |
| 177 | ++ Text("TestFlight Expiration", comment: "Settings TestFlight expiration view") |
| 178 | ++ Spacer() |
| 179 | ++ Text(profileExpirationMsg).foregroundColor(Color.secondary) |
| 180 | ++ } |
| 181 | ++ } |
| 182 | ++ Button(action: { |
| 183 | ++ UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/")!) |
| 184 | ++ }) { |
| 185 | ++ Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) |
| 186 | + } |
| 187 | + } |
| 188 | +- Button(action: { |
| 189 | +- UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) |
| 190 | +- }) { |
| 191 | +- Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) |
| 192 | ++ } else { |
| 193 | ++ return Section(header: SectionHeader(label: NSLocalizedString("App Profile", comment: "Settings app profile section")), |
| 194 | ++ footer: Text(NSLocalizedString("Profile expires ", comment: "Time that profile expires") + readableExpirationTime)) { |
| 195 | ++ if(nearExpiration) { |
| 196 | ++ Text(profileExpirationMsg).foregroundColor(.red) |
| 197 | ++ } else { |
| 198 | ++ HStack { |
| 199 | ++ Text("Profile Expiration", comment: "Settings App Profile expiration view") |
| 200 | ++ Spacer() |
| 201 | ++ Text(profileExpirationMsg).foregroundColor(Color.secondary) |
| 202 | ++ } |
| 203 | ++ } |
| 204 | ++ Button(action: { |
| 205 | ++ UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) |
| 206 | ++ }) { |
| 207 | ++ Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) |
| 208 | ++ } |
| 209 | + } |
| 210 | + } |
| 211 | + } |
0 commit comments