Skip to content

Commit 2b58a68

Browse files
committed
Detection of github build, calculate date for testflight expire
1 parent c68d2cc commit 2b58a68

File tree

7 files changed

+187
-117
lines changed

7 files changed

+187
-117
lines changed

Common/Models/BuildDetails.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,9 @@ class BuildDetails {
6565
var workspaceGitBranch: String? {
6666
return dict["com-loopkit-LoopWorkspace-git-branch"] as? String
6767
}
68+
69+
var isGitHubBuild: Bool? {
70+
return dict["com-loopkit-GitHub-build"] as? Bool
71+
}
6872
}
6973

Loop.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@
479479
C1EF747228D6A44A00C8C083 /* CrashRecoveryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EF747128D6A44A00C8C083 /* CrashRecoveryManager.swift */; };
480480
C1F00C60285A802A006302C5 /* SwiftCharts in Frameworks */ = {isa = PBXBuildFile; productRef = C1F00C5F285A802A006302C5 /* SwiftCharts */; };
481481
C1F00C78285A8256006302C5 /* SwiftCharts in Embed Frameworks */ = {isa = PBXBuildFile; productRef = C1F00C5F285A802A006302C5 /* SwiftCharts */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
482-
C1F2075C26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */; };
482+
C1F2075C26D6F9B0007AB7EB /* AppExpirationAlerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */; };
483483
C1F7822627CC056900C0919A /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F7822527CC056900C0919A /* SettingsManager.swift */; };
484484
C1F8B243223E73FD00DD66CF /* BolusProgressTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F8B1D122375E4200DD66CF /* BolusProgressTableViewCell.swift */; };
485485
C1FB428C217806A400FAB378 /* StateColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FB428B217806A300FAB378 /* StateColorPalette.swift */; };
@@ -1565,7 +1565,7 @@
15651565
C1EB0D22299581D900628475 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/ckcomplication.strings; sourceTree = "<group>"; };
15661566
C1EE9E802A38D0FB0064784A /* BuildDetails.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = BuildDetails.plist; sourceTree = "<group>"; };
15671567
C1EF747128D6A44A00C8C083 /* CrashRecoveryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashRecoveryManager.swift; sourceTree = "<group>"; };
1568-
C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileExpirationAlerter.swift; sourceTree = "<group>"; };
1568+
C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExpirationAlerter.swift; sourceTree = "<group>"; };
15691569
C1F48FF62995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
15701570
C1F48FF72995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
15711571
C1F48FF82995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -2307,7 +2307,7 @@
23072307
1DA6499D2441266400F61E75 /* Alerts */,
23082308
E95D37FF24EADE68005E2F50 /* Store Protocols */,
23092309
E9B355232935906B0076AB04 /* Missed Meal Detection */,
2310-
C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */,
2310+
C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */,
23112311
A96DAC2B2838F31200D94E38 /* SharedLogging.swift */,
23122312
7E69CFFB2A16A77E00203CBD /* ResetLoopManager.swift */,
23132313
84AA81E42A4A3981000B658B /* DeeplinkManager.swift */,
@@ -3654,7 +3654,7 @@
36543654
C1D289B522F90A52003FFBD9 /* BasalDeliveryState.swift in Sources */,
36553655
4F2C15821E074FC600E160D4 /* NSTimeInterval.swift in Sources */,
36563656
4311FB9B1F37FE1B00D4C0A7 /* TitleSubtitleTextFieldTableViewCell.swift in Sources */,
3657-
C1F2075C26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift in Sources */,
3657+
C1F2075C26D6F9B0007AB7EB /* AppExpirationAlerter.swift in Sources */,
36583658
B4FEEF7D24B8A71F00A8DF9B /* DeviceDataManager+DeviceStatus.swift in Sources */,
36593659
142CB7592A60BF2E0075748A /* EditMode.swift in Sources */,
36603660
E95D380324EADF36005E2F50 /* CarbStoreProtocol.swift in Sources */,
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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, now > profileExpiration - expirationAlertWindow else {
24+
return
25+
}
26+
27+
let expirationDate = calculateExpirationDate(profileExpiration: profileExpiration)
28+
29+
let timeUntilExpiration = expirationDate.timeIntervalSince(now)
30+
31+
let minimumTimeBetweenAlerts: TimeInterval = timeUntilExpiration > .hours(24) ? .days(2) : .hours(1)
32+
33+
if let lastAlertDate = UserDefaults.appGroup?.lastProfileExpirationAlertDate {
34+
guard now > lastAlertDate + minimumTimeBetweenAlerts else {
35+
return
36+
}
37+
}
38+
39+
let formatter = DateComponentsFormatter()
40+
formatter.allowedUnits = [.day, .hour]
41+
formatter.unitsStyle = .full
42+
formatter.zeroFormattingBehavior = .dropLeading
43+
formatter.maximumUnitCount = 1
44+
let timeUntilExpirationStr = formatter.string(from: timeUntilExpiration)
45+
46+
let alertMessage = createVerboseAlertMessage(timeUntilExpirationStr: timeUntilExpirationStr!)
47+
48+
var dialog: UIAlertController
49+
if isTestFlightBuild() {
50+
dialog = UIAlertController(
51+
title: NSLocalizedString("TestFlight Expires Soon", comment: "The title for notification of upcoming TestFlight expiration"),
52+
message: alertMessage,
53+
preferredStyle: .alert)
54+
dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil))
55+
dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming TestFlight expiration"), style: .default, handler: { (_) in
56+
UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/")!)
57+
}))
58+
59+
} else {
60+
dialog = UIAlertController(
61+
title: NSLocalizedString("Profile Expires Soon", comment: "The title for notification of upcoming profile expiration"),
62+
message: alertMessage,
63+
preferredStyle: .alert)
64+
dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil))
65+
dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming profile expiration"), style: .default, handler: { (_) in
66+
UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!)
67+
}))
68+
}
69+
viewControllerToPresentFrom.present(dialog, animated: true, completion: nil)
70+
71+
UserDefaults.appGroup?.lastProfileExpirationAlertDate = now
72+
}
73+
74+
static func createVerboseAlertMessage(timeUntilExpirationStr:String) -> String {
75+
if isTestFlightBuild() {
76+
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)
77+
} else {
78+
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 provisioning profile expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr)
79+
}
80+
}
81+
82+
static func isNearExpiration(expirationDate:Date) -> Bool {
83+
return expirationDate.timeIntervalSinceNow < settingsPageExpirationWarningModeWindow
84+
}
85+
86+
static func createProfileExpirationSettingsMessage(expirationDate:Date) -> String {
87+
let nearExpiration = isNearExpiration(expirationDate: expirationDate)
88+
let maxUnitCount = nearExpiration ? 2 : 1 // only include hours in the msg if near expiration
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+
return nearExpiration ? verboseMessage : conciseMessage
94+
}
95+
96+
private static func relativeTimeFormatter(maxUnitCount:Int) -> DateComponentsFormatter {
97+
let formatter = DateComponentsFormatter()
98+
let includeHours = maxUnitCount == 2
99+
formatter.allowedUnits = includeHours ? [.day, .hour] : [.day]
100+
formatter.unitsStyle = .full
101+
formatter.zeroFormattingBehavior = .dropLeading
102+
formatter.maximumUnitCount = maxUnitCount
103+
return formatter;
104+
}
105+
106+
static func buildDate() -> Date? {
107+
let dateFormatter = DateFormatter()
108+
dateFormatter.dateFormat = "EEE MMM d HH:mm:ss 'UTC' yyyy"
109+
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // Set locale to ensure parsing works
110+
111+
guard let dateString = BuildDetails.default.buildDateString,
112+
let date = dateFormatter.date(from: dateString) else {
113+
return nil
114+
}
115+
116+
return date
117+
}
118+
119+
static func isTestFlightBuild() -> Bool {
120+
return BuildDetails.default.isGitHubBuild ?? false
121+
}
122+
123+
static func calculateExpirationDate(profileExpiration: Date) -> Date {
124+
let isTestFlight = isTestFlightBuild()
125+
126+
if isTestFlight, let buildDate = buildDate() {
127+
let testflightExpiration = Calendar.current.date(byAdding: .day, value: 90, to: buildDate)!
128+
129+
return profileExpiration < testflightExpiration ? profileExpiration : testflightExpiration
130+
} else {
131+
return profileExpiration
132+
}
133+
}
134+
}

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.

Loop/Views/SettingsView.swift

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -417,25 +417,48 @@ extension SettingsView {
417417
DIY loop specific component to show users the amount of time remaining on their build before a rebuild is necessary.
418418
*/
419419
private func profileExpirationSection(profileExpiration:Date) -> some View {
420-
let nearExpiration : Bool = ProfileExpirationAlerter.isNearProfileExpiration(profileExpiration: profileExpiration)
421-
let profileExpirationMsg = ProfileExpirationAlerter.createProfileExpirationSettingsMessage(profileExpiration: profileExpiration)
422-
let readableExpirationTime = Self.dateFormatter.string(from: profileExpiration)
420+
let expirationDate = AppExpirationAlerter.calculateExpirationDate(profileExpiration: profileExpiration)
421+
let isTestFlight = AppExpirationAlerter.isTestFlightBuild()
423422

424-
return Section(header: SectionHeader(label: NSLocalizedString("App Profile", comment: "Settings app profile section")),
425-
footer: Text(NSLocalizedString("Profile expires ", comment: "Time that profile expires") + readableExpirationTime)) {
426-
if(nearExpiration) {
427-
Text(profileExpirationMsg).foregroundColor(.red)
428-
} else {
429-
HStack {
430-
Text("Profile Expiration", comment: "Settings App Profile expiration view")
431-
Spacer()
432-
Text(profileExpirationMsg).foregroundColor(Color.secondary)
423+
let nearExpiration : Bool = AppExpirationAlerter.isNearExpiration(expirationDate: expirationDate)
424+
let profileExpirationMsg = AppExpirationAlerter.createProfileExpirationSettingsMessage(expirationDate: expirationDate)
425+
let readableExpirationTime = Self.dateFormatter.string(from: expirationDate)
426+
427+
if isTestFlight {
428+
return Section(header: SectionHeader(label: NSLocalizedString("TestFlight", comment: "Settings app TestFlight section")),
429+
footer: Text(NSLocalizedString("TestFlight expires ", comment: "Time that build expires") + readableExpirationTime)) {
430+
if(nearExpiration) {
431+
Text(profileExpirationMsg).foregroundColor(.red)
432+
} else {
433+
HStack {
434+
Text("TestFlight Expiration", comment: "Settings TestFlight expiration view")
435+
Spacer()
436+
Text(profileExpirationMsg).foregroundColor(Color.secondary)
437+
}
438+
}
439+
Button(action: {
440+
UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/")!)
441+
}) {
442+
Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update"))
433443
}
434444
}
435-
Button(action: {
436-
UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!)
437-
}) {
438-
Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update"))
445+
} else {
446+
return Section(header: SectionHeader(label: NSLocalizedString("App Profile", comment: "Settings app profile section")),
447+
footer: Text(NSLocalizedString("Profile expires ", comment: "Time that profile expires") + readableExpirationTime)) {
448+
if(nearExpiration) {
449+
Text(profileExpirationMsg).foregroundColor(.red)
450+
} else {
451+
HStack {
452+
Text("Profile Expiration", comment: "Settings App Profile expiration view")
453+
Spacer()
454+
Text(profileExpirationMsg).foregroundColor(Color.secondary)
455+
}
456+
}
457+
Button(action: {
458+
UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!)
459+
}) {
460+
Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update"))
461+
}
439462
}
440463
}
441464
}

0 commit comments

Comments
 (0)