Skip to content

Commit 2360edb

Browse files
authored
Merge pull request #2052 from loopandlearn/github-build-expiration-date
Update the app expiration alert for GitHub and Xcode builds
2 parents 3f6d57d + 180eceb commit 2360edb

File tree

5 files changed

+202
-103
lines changed

5 files changed

+202
-103
lines changed

Loop.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@
478478
C1EF747228D6A44A00C8C083 /* CrashRecoveryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EF747128D6A44A00C8C083 /* CrashRecoveryManager.swift */; };
479479
C1F00C60285A802A006302C5 /* SwiftCharts in Frameworks */ = {isa = PBXBuildFile; productRef = C1F00C5F285A802A006302C5 /* SwiftCharts */; };
480480
C1F00C78285A8256006302C5 /* SwiftCharts in Embed Frameworks */ = {isa = PBXBuildFile; productRef = C1F00C5F285A802A006302C5 /* SwiftCharts */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
481-
C1F2075C26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */; };
481+
C1F2075C26D6F9B0007AB7EB /* AppExpirationAlerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */; };
482482
C1F7822627CC056900C0919A /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F7822527CC056900C0919A /* SettingsManager.swift */; };
483483
C1F8B243223E73FD00DD66CF /* BolusProgressTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F8B1D122375E4200DD66CF /* BolusProgressTableViewCell.swift */; };
484484
C1FB428C217806A400FAB378 /* StateColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FB428B217806A300FAB378 /* StateColorPalette.swift */; };
@@ -1563,7 +1563,7 @@
15631563
C1EB0D22299581D900628475 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/ckcomplication.strings; sourceTree = "<group>"; };
15641564
C1EE9E802A38D0FB0064784A /* BuildDetails.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = BuildDetails.plist; sourceTree = "<group>"; };
15651565
C1EF747128D6A44A00C8C083 /* CrashRecoveryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashRecoveryManager.swift; sourceTree = "<group>"; };
1566-
C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileExpirationAlerter.swift; sourceTree = "<group>"; };
1566+
C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExpirationAlerter.swift; sourceTree = "<group>"; };
15671567
C1F48FF62995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
15681568
C1F48FF72995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
15691569
C1F48FF82995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -2306,7 +2306,7 @@
23062306
1DA6499D2441266400F61E75 /* Alerts */,
23072307
E95D37FF24EADE68005E2F50 /* Store Protocols */,
23082308
E9B355232935906B0076AB04 /* Missed Meal Detection */,
2309-
C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */,
2309+
C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */,
23102310
A96DAC2B2838F31200D94E38 /* SharedLogging.swift */,
23112311
7E69CFFB2A16A77E00203CBD /* ResetLoopManager.swift */,
23122312
84AA81E42A4A3981000B658B /* DeeplinkManager.swift */,
@@ -3649,7 +3649,7 @@
36493649
C1D289B522F90A52003FFBD9 /* BasalDeliveryState.swift in Sources */,
36503650
4F2C15821E074FC600E160D4 /* NSTimeInterval.swift in Sources */,
36513651
4311FB9B1F37FE1B00D4C0A7 /* TitleSubtitleTextFieldTableViewCell.swift in Sources */,
3652-
C1F2075C26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift in Sources */,
3652+
C1F2075C26D6F9B0007AB7EB /* AppExpirationAlerter.swift in Sources */,
36533653
B4FEEF7D24B8A71F00A8DF9B /* DeviceDataManager+DeviceStatus.swift in Sources */,
36543654
142CB7592A60BF2E0075748A /* EditMode.swift in Sources */,
36553655
E95D380324EADF36005E2F50 /* CarbStoreProtocol.swift in Sources */,
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
//
2+
// AppExpirationAlerter.swift
3+
// Loop
4+
//
5+
// Created by Pete Schwamb on 8/21/21.
6+
// Copyright © 2021 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import UserNotifications
11+
import LoopCore
12+
13+
14+
class AppExpirationAlerter {
15+
16+
static let expirationAlertWindow: TimeInterval = .days(20)
17+
static let settingsPageExpirationWarningModeWindow: TimeInterval = .days(3)
18+
19+
static func alertIfNeeded(viewControllerToPresentFrom: UIViewController) {
20+
21+
let now = Date()
22+
23+
guard let profileExpiration = BuildDetails.default.profileExpiration else {
24+
return
25+
}
26+
27+
let expirationDate = calculateExpirationDate(profileExpiration: profileExpiration)
28+
29+
let timeUntilExpiration = expirationDate.timeIntervalSince(now)
30+
31+
if timeUntilExpiration > expirationAlertWindow {
32+
return
33+
}
34+
35+
let minimumTimeBetweenAlerts: TimeInterval = timeUntilExpiration > .hours(24) ? .days(2) : .hours(1)
36+
37+
if let lastAlertDate = UserDefaults.appGroup?.lastProfileExpirationAlertDate {
38+
guard now > lastAlertDate + minimumTimeBetweenAlerts else {
39+
return
40+
}
41+
}
42+
43+
let formatter = DateComponentsFormatter()
44+
formatter.allowedUnits = [.day, .hour]
45+
formatter.unitsStyle = .full
46+
formatter.zeroFormattingBehavior = .dropLeading
47+
formatter.maximumUnitCount = 1
48+
let timeUntilExpirationStr = formatter.string(from: timeUntilExpiration)
49+
50+
let alertMessage = createVerboseAlertMessage(timeUntilExpirationStr: timeUntilExpirationStr!)
51+
52+
var dialog: UIAlertController
53+
if isTestFlightBuild() {
54+
dialog = UIAlertController(
55+
title: NSLocalizedString("TestFlight Expires Soon", comment: "The title for notification of upcoming TestFlight expiration"),
56+
message: alertMessage,
57+
preferredStyle: .alert)
58+
dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming TestFlight expiration"), style: .default, handler: nil))
59+
dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming TestFlight expiration"), style: .default, handler: { (_) in
60+
UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/")!)
61+
}))
62+
63+
} else {
64+
dialog = UIAlertController(
65+
title: NSLocalizedString("Profile Expires Soon", comment: "The title for notification of upcoming profile expiration"),
66+
message: alertMessage,
67+
preferredStyle: .alert)
68+
dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil))
69+
dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming profile expiration"), style: .default, handler: { (_) in
70+
UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!)
71+
}))
72+
}
73+
viewControllerToPresentFrom.present(dialog, animated: true, completion: nil)
74+
75+
UserDefaults.appGroup?.lastProfileExpirationAlertDate = now
76+
}
77+
78+
static func createVerboseAlertMessage(timeUntilExpirationStr:String) -> String {
79+
if isTestFlightBuild() {
80+
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)
81+
} else {
82+
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)
83+
}
84+
}
85+
86+
static func isNearExpiration(expirationDate:Date) -> Bool {
87+
return expirationDate.timeIntervalSinceNow < settingsPageExpirationWarningModeWindow
88+
}
89+
90+
static func createProfileExpirationSettingsMessage(expirationDate:Date) -> String {
91+
let nearExpiration = isNearExpiration(expirationDate: expirationDate)
92+
let maxUnitCount = nearExpiration ? 2 : 1 // only include hours in the msg if near expiration
93+
let readableRelativeTime: String? = relativeTimeFormatter(maxUnitCount: maxUnitCount).string(from: expirationDate.timeIntervalSinceNow)
94+
let relativeTimeRemaining: String = readableRelativeTime ?? NSLocalizedString("Unknown time", comment: "Unknown amount of time in settings' profile expiration section")
95+
let verboseMessage = createVerboseAlertMessage(timeUntilExpirationStr: relativeTimeRemaining)
96+
let conciseMessage = relativeTimeRemaining + NSLocalizedString(" remaining", comment: "remaining time in setting's profile expiration section")
97+
return nearExpiration ? verboseMessage : conciseMessage
98+
}
99+
100+
private static func relativeTimeFormatter(maxUnitCount:Int) -> DateComponentsFormatter {
101+
let formatter = DateComponentsFormatter()
102+
let includeHours = maxUnitCount == 2
103+
formatter.allowedUnits = includeHours ? [.day, .hour] : [.day]
104+
formatter.unitsStyle = .full
105+
formatter.zeroFormattingBehavior = .dropLeading
106+
formatter.maximumUnitCount = maxUnitCount
107+
return formatter
108+
}
109+
110+
static func buildDate() -> Date? {
111+
let dateFormatter = DateFormatter()
112+
dateFormatter.dateFormat = "EEE MMM d HH:mm:ss 'UTC' yyyy"
113+
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // Set locale to ensure parsing works
114+
dateFormatter.timeZone = TimeZone(identifier: "UTC")
115+
116+
guard let dateString = BuildDetails.default.buildDateString,
117+
let date = dateFormatter.date(from: dateString) else {
118+
return nil
119+
}
120+
121+
return date
122+
}
123+
124+
static func isTestFlightBuild() -> Bool {
125+
// If the target environment is a simulator, then
126+
// this is not a TestFlight distribution. Return false.
127+
#if targetEnvironment(simulator)
128+
return false
129+
#endif
130+
131+
// If an "embedded.mobileprovision" is present in the main bundle, then
132+
// this is an Xcode, Ad-Hoc, or Enterprise distribution. Return false.
133+
if Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision") != nil {
134+
return false
135+
}
136+
137+
// If an app store receipt is not present in the main bundle, then we cannot
138+
// say whether this is a TestFlight or App Store distribution. Return false.
139+
guard let receiptName = Bundle.main.appStoreReceiptURL?.lastPathComponent else {
140+
return false
141+
}
142+
143+
// A TestFlight distribution presents a "sandboxReceipt", while an App Store
144+
// distribution presents a "receipt". Return true if we have a TestFlight receipt.
145+
return "sandboxReceipt".caseInsensitiveCompare(receiptName) == .orderedSame
146+
}
147+
148+
static func calculateExpirationDate(profileExpiration: Date) -> Date {
149+
let isTestFlight = isTestFlightBuild()
150+
151+
if isTestFlight, let buildDate = buildDate() {
152+
let testflightExpiration = Calendar.current.date(byAdding: .day, value: 90, to: buildDate)!
153+
154+
return profileExpiration < testflightExpiration ? profileExpiration : testflightExpiration
155+
} else {
156+
return profileExpiration
157+
}
158+
}
159+
}

Loop/Managers/LoopAppManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ class LoopAppManager: NSObject {
323323

324324
func didBecomeActive() {
325325
if let rootViewController = rootViewController {
326-
ProfileExpirationAlerter.alertIfNeeded(viewControllerToPresentFrom: rootViewController)
326+
AppExpirationAlerter.alertIfNeeded(viewControllerToPresentFrom: rootViewController)
327327
}
328328
settingsManager?.didBecomeActive()
329329
deviceDataManager?.didBecomeActive()

Loop/Managers/ProfileExpirationAlerter.swift

Lines changed: 0 additions & 86 deletions
This file was deleted.

0 commit comments

Comments
 (0)