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