Skip to content

Commit 016f9fd

Browse files
authored
Merge pull request #28 from loopandlearn/testflight_expiration_warning
Testflight expiration warning
2 parents 54a9aba + 8a3fee3 commit 016f9fd

File tree

1 file changed

+215
-0
lines changed

1 file changed

+215
-0
lines changed
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
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

Comments
 (0)