From dae9b622a28b563db0cb2c06191fcb57bfcb57c2 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 21 Aug 2023 18:05:46 -0700 Subject: [PATCH 01/11] Overwrite profile_expire_date for GitHub built --- Scripts/capture-build-details.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Scripts/capture-build-details.sh b/Scripts/capture-build-details.sh index 66f827d7c3..de8a438b96 100755 --- a/Scripts/capture-build-details.sh +++ b/Scripts/capture-build-details.sh @@ -89,5 +89,14 @@ then if [ -n "$branch" ]; then plutil -replace com-loopkit-LoopWorkspace-git-branch -string "${branch}" "${info_plist_path}" fi + # determine if this is a GitHub Action build (with 90 day expiration) + folderName=$(pwd) + runnerString="/Users/runner" + if [ "${folderName:0:13}" == "$runnerString" ]; then + # overwrite profile_expire_date + profile_expire_date=$(date -j -v+90d +"%Y-%m-%dT%H:%M:%SZ") + echo "runnerString detected, update profile_expire_date to ${profile_expire_date}" + plutil -replace com-loopkit-Loop-profile-expiration -date "${profile_expire_date}" "${info_plist_path}" + fi popd . > /dev/null fi From c68d2cc2f7521678b18bc3cb17a9f53aa5f36442 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Tue, 22 Aug 2023 15:48:22 -0700 Subject: [PATCH 02/11] use GITHUB_ACTION to update profile expire date --- Scripts/capture-build-details.sh | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Scripts/capture-build-details.sh b/Scripts/capture-build-details.sh index de8a438b96..ea7b1f27f8 100755 --- a/Scripts/capture-build-details.sh +++ b/Scripts/capture-build-details.sh @@ -72,6 +72,17 @@ if [ -e "${provisioning_profile_path}" ]; then profile_expire_date=$(security cms -D -i "${provisioning_profile_path}" | plutil -p - | grep ExpirationDate | cut -b 23-) # Convert to plutil format profile_expire_date=$(date -j -f "%Y-%m-%d %H:%M:%S" "${profile_expire_date}" +"%Y-%m-%dT%H:%M:%SZ") + # Handle github action, testflight builds that expire <= 90 days + if [ -n "$GITHUB_ACTIONS" ]; then + github_expire_date=$(date -j -v+90d +"%Y-%m-%dT%H:%M:%SZ") + + if [ "$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "${github_expire_date}" +%s)" -lt "$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "${profile_expire_date}" +%s)" ]; then + profile_expire_date=$github_expire_date + else + echo "GitHub Actions detected, expiration date is not more than 90 days in the future." + fi + fi + plutil -replace com-loopkit-Loop-profile-expiration -date "${profile_expire_date}" "${info_plist_path}" else warn "Invalid provisioning profile path ${provisioning_profile_path}" @@ -89,14 +100,5 @@ then if [ -n "$branch" ]; then plutil -replace com-loopkit-LoopWorkspace-git-branch -string "${branch}" "${info_plist_path}" fi - # determine if this is a GitHub Action build (with 90 day expiration) - folderName=$(pwd) - runnerString="/Users/runner" - if [ "${folderName:0:13}" == "$runnerString" ]; then - # overwrite profile_expire_date - profile_expire_date=$(date -j -v+90d +"%Y-%m-%dT%H:%M:%SZ") - echo "runnerString detected, update profile_expire_date to ${profile_expire_date}" - plutil -replace com-loopkit-Loop-profile-expiration -date "${profile_expire_date}" "${info_plist_path}" - fi popd . > /dev/null fi From 2b58a68e81e71986771be7273d3242182a1d85b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sun, 27 Aug 2023 21:16:31 +0200 Subject: [PATCH 03/11] Detection of github build, calculate date for testflight expire --- Common/Models/BuildDetails.swift | 4 + Loop.xcodeproj/project.pbxproj | 8 +- Loop/Managers/AppExpirationAlerter.swift | 134 +++++++++++++++++++ Loop/Managers/LoopAppManager.swift | 2 +- Loop/Managers/ProfileExpirationAlerter.swift | 86 ------------ Loop/Views/SettingsView.swift | 55 +++++--- Scripts/capture-build-details.sh | 15 +-- 7 files changed, 187 insertions(+), 117 deletions(-) create mode 100644 Loop/Managers/AppExpirationAlerter.swift delete mode 100644 Loop/Managers/ProfileExpirationAlerter.swift diff --git a/Common/Models/BuildDetails.swift b/Common/Models/BuildDetails.swift index 63517e7e79..0badb257ac 100644 --- a/Common/Models/BuildDetails.swift +++ b/Common/Models/BuildDetails.swift @@ -65,5 +65,9 @@ class BuildDetails { var workspaceGitBranch: String? { return dict["com-loopkit-LoopWorkspace-git-branch"] as? String } + + var isGitHubBuild: Bool? { + return dict["com-loopkit-GitHub-build"] as? Bool + } } diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index aaa0a470ac..ecba1c2d01 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -479,7 +479,7 @@ C1EF747228D6A44A00C8C083 /* CrashRecoveryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EF747128D6A44A00C8C083 /* CrashRecoveryManager.swift */; }; C1F00C60285A802A006302C5 /* SwiftCharts in Frameworks */ = {isa = PBXBuildFile; productRef = C1F00C5F285A802A006302C5 /* SwiftCharts */; }; C1F00C78285A8256006302C5 /* SwiftCharts in Embed Frameworks */ = {isa = PBXBuildFile; productRef = C1F00C5F285A802A006302C5 /* SwiftCharts */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - C1F2075C26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */; }; + C1F2075C26D6F9B0007AB7EB /* AppExpirationAlerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */; }; C1F7822627CC056900C0919A /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F7822527CC056900C0919A /* SettingsManager.swift */; }; C1F8B243223E73FD00DD66CF /* BolusProgressTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F8B1D122375E4200DD66CF /* BolusProgressTableViewCell.swift */; }; C1FB428C217806A400FAB378 /* StateColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FB428B217806A300FAB378 /* StateColorPalette.swift */; }; @@ -1565,7 +1565,7 @@ C1EB0D22299581D900628475 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/ckcomplication.strings; sourceTree = ""; }; C1EE9E802A38D0FB0064784A /* BuildDetails.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = BuildDetails.plist; sourceTree = ""; }; C1EF747128D6A44A00C8C083 /* CrashRecoveryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashRecoveryManager.swift; sourceTree = ""; }; - C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileExpirationAlerter.swift; sourceTree = ""; }; + C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExpirationAlerter.swift; sourceTree = ""; }; C1F48FF62995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; C1F48FF72995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; C1F48FF82995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; @@ -2307,7 +2307,7 @@ 1DA6499D2441266400F61E75 /* Alerts */, E95D37FF24EADE68005E2F50 /* Store Protocols */, E9B355232935906B0076AB04 /* Missed Meal Detection */, - C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */, + C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */, A96DAC2B2838F31200D94E38 /* SharedLogging.swift */, 7E69CFFB2A16A77E00203CBD /* ResetLoopManager.swift */, 84AA81E42A4A3981000B658B /* DeeplinkManager.swift */, @@ -3654,7 +3654,7 @@ C1D289B522F90A52003FFBD9 /* BasalDeliveryState.swift in Sources */, 4F2C15821E074FC600E160D4 /* NSTimeInterval.swift in Sources */, 4311FB9B1F37FE1B00D4C0A7 /* TitleSubtitleTextFieldTableViewCell.swift in Sources */, - C1F2075C26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift in Sources */, + C1F2075C26D6F9B0007AB7EB /* AppExpirationAlerter.swift in Sources */, B4FEEF7D24B8A71F00A8DF9B /* DeviceDataManager+DeviceStatus.swift in Sources */, 142CB7592A60BF2E0075748A /* EditMode.swift in Sources */, E95D380324EADF36005E2F50 /* CarbStoreProtocol.swift in Sources */, diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift new file mode 100644 index 0000000000..23ceb1e7a3 --- /dev/null +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -0,0 +1,134 @@ +// +// AppExpirationAlerter.swift +// Loop +// +// Created by Pete Schwamb on 8/21/21. +// Copyright © 2021 LoopKit Authors. All rights reserved. +// + +import Foundation +import UserNotifications +import LoopCore + + +class AppExpirationAlerter { + + static let expirationAlertWindow: TimeInterval = .days(20) + static let settingsPageExpirationWarningModeWindow: TimeInterval = .days(3) + + static func alertIfNeeded(viewControllerToPresentFrom: UIViewController) { + + let now = Date() + + guard let profileExpiration = BuildDetails.default.profileExpiration, now > profileExpiration - expirationAlertWindow else { + return + } + + let expirationDate = calculateExpirationDate(profileExpiration: profileExpiration) + + let timeUntilExpiration = expirationDate.timeIntervalSince(now) + + let minimumTimeBetweenAlerts: TimeInterval = timeUntilExpiration > .hours(24) ? .days(2) : .hours(1) + + if let lastAlertDate = UserDefaults.appGroup?.lastProfileExpirationAlertDate { + guard now > lastAlertDate + minimumTimeBetweenAlerts else { + return + } + } + + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.day, .hour] + formatter.unitsStyle = .full + formatter.zeroFormattingBehavior = .dropLeading + formatter.maximumUnitCount = 1 + let timeUntilExpirationStr = formatter.string(from: timeUntilExpiration) + + let alertMessage = createVerboseAlertMessage(timeUntilExpirationStr: timeUntilExpirationStr!) + + var dialog: UIAlertController + if isTestFlightBuild() { + dialog = UIAlertController( + title: NSLocalizedString("TestFlight Expires Soon", comment: "The title for notification of upcoming TestFlight expiration"), + message: alertMessage, + preferredStyle: .alert) + dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil)) + dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming TestFlight expiration"), style: .default, handler: { (_) in + UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/")!) + })) + + } else { + dialog = UIAlertController( + title: NSLocalizedString("Profile Expires Soon", comment: "The title for notification of upcoming profile expiration"), + message: alertMessage, + preferredStyle: .alert) + dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil)) + dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming profile expiration"), style: .default, handler: { (_) in + UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) + })) + } + viewControllerToPresentFrom.present(dialog, animated: true, completion: nil) + + UserDefaults.appGroup?.lastProfileExpirationAlertDate = now + } + + static func createVerboseAlertMessage(timeUntilExpirationStr:String) -> String { + if isTestFlightBuild() { + 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) + } else { + 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) + } + } + + static func isNearExpiration(expirationDate:Date) -> Bool { + return expirationDate.timeIntervalSinceNow < settingsPageExpirationWarningModeWindow + } + + static func createProfileExpirationSettingsMessage(expirationDate:Date) -> String { + let nearExpiration = isNearExpiration(expirationDate: expirationDate) + let maxUnitCount = nearExpiration ? 2 : 1 // only include hours in the msg if near expiration + let readableRelativeTime: String? = relativeTimeFormatter(maxUnitCount: maxUnitCount).string(from: expirationDate.timeIntervalSinceNow) + let relativeTimeRemaining: String = readableRelativeTime ?? NSLocalizedString("Unknown time", comment: "Unknown amount of time in settings' profile expiration section") + let verboseMessage = createVerboseAlertMessage(timeUntilExpirationStr: relativeTimeRemaining) + let conciseMessage = relativeTimeRemaining + NSLocalizedString(" remaining", comment: "remaining time in setting's profile expiration section") + return nearExpiration ? verboseMessage : conciseMessage + } + + private static func relativeTimeFormatter(maxUnitCount:Int) -> DateComponentsFormatter { + let formatter = DateComponentsFormatter() + let includeHours = maxUnitCount == 2 + formatter.allowedUnits = includeHours ? [.day, .hour] : [.day] + formatter.unitsStyle = .full + formatter.zeroFormattingBehavior = .dropLeading + formatter.maximumUnitCount = maxUnitCount + return formatter; + } + + static func buildDate() -> Date? { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EEE MMM d HH:mm:ss 'UTC' yyyy" + dateFormatter.locale = Locale(identifier: "en_US_POSIX") // Set locale to ensure parsing works + + guard let dateString = BuildDetails.default.buildDateString, + let date = dateFormatter.date(from: dateString) else { + return nil + } + + return date + } + + static func isTestFlightBuild() -> Bool { + return BuildDetails.default.isGitHubBuild ?? false + } + + static func calculateExpirationDate(profileExpiration: Date) -> Date { + let isTestFlight = isTestFlightBuild() + + if isTestFlight, let buildDate = buildDate() { + let testflightExpiration = Calendar.current.date(byAdding: .day, value: 90, to: buildDate)! + + return profileExpiration < testflightExpiration ? profileExpiration : testflightExpiration + } else { + return profileExpiration + } + } +} diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index 43c62d128b..9edf481ba2 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -323,7 +323,7 @@ class LoopAppManager: NSObject { func didBecomeActive() { if let rootViewController = rootViewController { - ProfileExpirationAlerter.alertIfNeeded(viewControllerToPresentFrom: rootViewController) + AppExpirationAlerter.alertIfNeeded(viewControllerToPresentFrom: rootViewController) } settingsManager?.didBecomeActive() deviceDataManager?.didBecomeActive() diff --git a/Loop/Managers/ProfileExpirationAlerter.swift b/Loop/Managers/ProfileExpirationAlerter.swift deleted file mode 100644 index 3aa742732b..0000000000 --- a/Loop/Managers/ProfileExpirationAlerter.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// ProfileExpirationAlerter.swift -// Loop -// -// Created by Pete Schwamb on 8/21/21. -// Copyright © 2021 LoopKit Authors. All rights reserved. -// - -import Foundation -import UserNotifications -import LoopCore - - -class ProfileExpirationAlerter { - - static let expirationAlertWindow: TimeInterval = .days(20) - static let settingsPageExpirationWarningModeWindow: TimeInterval = .days(3) - - static func alertIfNeeded(viewControllerToPresentFrom: UIViewController) { - - let now = Date() - - guard let profileExpiration = BuildDetails.default.profileExpiration, now > profileExpiration - expirationAlertWindow else { - return - } - - let timeUntilExpiration = profileExpiration.timeIntervalSince(now) - - let minimumTimeBetweenAlerts: TimeInterval = timeUntilExpiration > .hours(24) ? .days(2) : .hours(1) - - if let lastAlertDate = UserDefaults.appGroup?.lastProfileExpirationAlertDate { - guard now > lastAlertDate + minimumTimeBetweenAlerts else { - return - } - } - - let formatter = DateComponentsFormatter() - formatter.allowedUnits = [.day, .hour] - formatter.unitsStyle = .full - formatter.zeroFormattingBehavior = .dropLeading - formatter.maximumUnitCount = 1 - let timeUntilExpirationStr = formatter.string(from: timeUntilExpiration) - - let alertMessage = createVerboseAlertMessage(timeUntilExpirationStr: timeUntilExpirationStr!) - - let dialog = UIAlertController( - title: NSLocalizedString("Profile Expires Soon", comment: "The title for notification of upcoming profile expiration"), - message: alertMessage, - preferredStyle: .alert) - dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil)) - dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming profile expiration"), style: .default, handler: { (_) in - UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) - })) - viewControllerToPresentFrom.present(dialog, animated: true, completion: nil) - - UserDefaults.appGroup?.lastProfileExpirationAlertDate = now - } - - static func createVerboseAlertMessage(timeUntilExpirationStr:String) -> String { - 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) - } - - static func isNearProfileExpiration(profileExpiration:Date) -> Bool { - return profileExpiration.timeIntervalSinceNow < settingsPageExpirationWarningModeWindow - } - - static func createProfileExpirationSettingsMessage(profileExpiration:Date) -> String { - let nearExpiration = isNearProfileExpiration(profileExpiration: profileExpiration) - let maxUnitCount = nearExpiration ? 2 : 1 // only include hours in the msg if near expiration - let readableRelativeTime: String? = relativeTimeFormatter(maxUnitCount: maxUnitCount).string(from: profileExpiration.timeIntervalSinceNow) - let relativeTimeRemaining: String = readableRelativeTime ?? NSLocalizedString("Unknown time", comment: "Unknown amount of time in settings' profile expiration section") - let verboseMessage = createVerboseAlertMessage(timeUntilExpirationStr: relativeTimeRemaining) - let conciseMessage = relativeTimeRemaining + NSLocalizedString(" remaining", comment: "remaining time in setting's profile expiration section") - return nearExpiration ? verboseMessage : conciseMessage - } - - private static func relativeTimeFormatter(maxUnitCount:Int) -> DateComponentsFormatter { - let formatter = DateComponentsFormatter() - let includeHours = maxUnitCount == 2 - formatter.allowedUnits = includeHours ? [.day, .hour] : [.day] - formatter.unitsStyle = .full - formatter.zeroFormattingBehavior = .dropLeading - formatter.maximumUnitCount = maxUnitCount - return formatter; - } -} diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index 0b4cb55133..a07d623f40 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -417,25 +417,48 @@ extension SettingsView { DIY loop specific component to show users the amount of time remaining on their build before a rebuild is necessary. */ private func profileExpirationSection(profileExpiration:Date) -> some View { - let nearExpiration : Bool = ProfileExpirationAlerter.isNearProfileExpiration(profileExpiration: profileExpiration) - let profileExpirationMsg = ProfileExpirationAlerter.createProfileExpirationSettingsMessage(profileExpiration: profileExpiration) - let readableExpirationTime = Self.dateFormatter.string(from: profileExpiration) + let expirationDate = AppExpirationAlerter.calculateExpirationDate(profileExpiration: profileExpiration) + let isTestFlight = AppExpirationAlerter.isTestFlightBuild() - return Section(header: SectionHeader(label: NSLocalizedString("App Profile", comment: "Settings app profile section")), - footer: Text(NSLocalizedString("Profile expires ", comment: "Time that profile expires") + readableExpirationTime)) { - if(nearExpiration) { - Text(profileExpirationMsg).foregroundColor(.red) - } else { - HStack { - Text("Profile Expiration", comment: "Settings App Profile expiration view") - Spacer() - Text(profileExpirationMsg).foregroundColor(Color.secondary) + let nearExpiration : Bool = AppExpirationAlerter.isNearExpiration(expirationDate: expirationDate) + let profileExpirationMsg = AppExpirationAlerter.createProfileExpirationSettingsMessage(expirationDate: expirationDate) + let readableExpirationTime = Self.dateFormatter.string(from: expirationDate) + + if isTestFlight { + return Section(header: SectionHeader(label: NSLocalizedString("TestFlight", comment: "Settings app TestFlight section")), + footer: Text(NSLocalizedString("TestFlight expires ", comment: "Time that build expires") + readableExpirationTime)) { + if(nearExpiration) { + Text(profileExpirationMsg).foregroundColor(.red) + } else { + HStack { + Text("TestFlight Expiration", comment: "Settings TestFlight expiration view") + Spacer() + Text(profileExpirationMsg).foregroundColor(Color.secondary) + } + } + Button(action: { + UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/")!) + }) { + Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) } } - Button(action: { - UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) - }) { - Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) + } else { + return Section(header: SectionHeader(label: NSLocalizedString("App Profile", comment: "Settings app profile section")), + footer: Text(NSLocalizedString("Profile expires ", comment: "Time that profile expires") + readableExpirationTime)) { + if(nearExpiration) { + Text(profileExpirationMsg).foregroundColor(.red) + } else { + HStack { + Text("Profile Expiration", comment: "Settings App Profile expiration view") + Spacer() + Text(profileExpirationMsg).foregroundColor(Color.secondary) + } + } + Button(action: { + UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) + }) { + Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) + } } } } diff --git a/Scripts/capture-build-details.sh b/Scripts/capture-build-details.sh index ea7b1f27f8..5431b4f239 100755 --- a/Scripts/capture-build-details.sh +++ b/Scripts/capture-build-details.sh @@ -72,16 +72,6 @@ if [ -e "${provisioning_profile_path}" ]; then profile_expire_date=$(security cms -D -i "${provisioning_profile_path}" | plutil -p - | grep ExpirationDate | cut -b 23-) # Convert to plutil format profile_expire_date=$(date -j -f "%Y-%m-%d %H:%M:%S" "${profile_expire_date}" +"%Y-%m-%dT%H:%M:%SZ") - # Handle github action, testflight builds that expire <= 90 days - if [ -n "$GITHUB_ACTIONS" ]; then - github_expire_date=$(date -j -v+90d +"%Y-%m-%dT%H:%M:%SZ") - - if [ "$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "${github_expire_date}" +%s)" -lt "$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "${profile_expire_date}" +%s)" ]; then - profile_expire_date=$github_expire_date - else - echo "GitHub Actions detected, expiration date is not more than 90 days in the future." - fi - fi plutil -replace com-loopkit-Loop-profile-expiration -date "${profile_expire_date}" "${info_plist_path}" else @@ -102,3 +92,8 @@ then fi popd . > /dev/null fi + +# Handle github action +if [ -n "$GITHUB_ACTIONS" ]; then + plutil -replace com-loopkit-GitHub-build -bool true "${info_plist_path}" +fi From 78a75cdf718e90f7ec9debb7e0b8ea0a99383bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 28 Aug 2023 09:51:48 +0200 Subject: [PATCH 04/11] TF text adjustments --- Loop/Managers/AppExpirationAlerter.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift index 23ceb1e7a3..08ff824d58 100644 --- a/Loop/Managers/AppExpirationAlerter.swift +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -51,7 +51,7 @@ class AppExpirationAlerter { title: NSLocalizedString("TestFlight Expires Soon", comment: "The title for notification of upcoming TestFlight expiration"), message: alertMessage, preferredStyle: .alert) - dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil)) + dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming TestFlight expiration"), style: .default, handler: nil)) dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming TestFlight expiration"), style: .default, handler: { (_) in UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/")!) })) @@ -73,9 +73,9 @@ class AppExpirationAlerter { static func createVerboseAlertMessage(timeUntilExpirationStr:String) -> String { if isTestFlightBuild() { - 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) + 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) } else { - 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) + 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) } } From 874c393130a8f91c348e64675ec34485b44d17f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 28 Aug 2023 13:10:26 +0200 Subject: [PATCH 05/11] Fix for UTC timezone --- Loop/Managers/AppExpirationAlerter.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift index 08ff824d58..6b8c0a88a2 100644 --- a/Loop/Managers/AppExpirationAlerter.swift +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -107,6 +107,7 @@ class AppExpirationAlerter { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "EEE MMM d HH:mm:ss 'UTC' yyyy" dateFormatter.locale = Locale(identifier: "en_US_POSIX") // Set locale to ensure parsing works + dateFormatter.timeZone = TimeZone(identifier: "UTC") guard let dateString = BuildDetails.default.buildDateString, let date = dateFormatter.date(from: dateString) else { From 211a337a6a1324bd9a489f1b8fc43d5697170037 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 28 Aug 2023 12:49:20 -0700 Subject: [PATCH 06/11] fix for modal alert --- Loop/Managers/AppExpirationAlerter.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift index 6b8c0a88a2..a637927f37 100644 --- a/Loop/Managers/AppExpirationAlerter.swift +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -19,8 +19,8 @@ class AppExpirationAlerter { static func alertIfNeeded(viewControllerToPresentFrom: UIViewController) { let now = Date() - - guard let profileExpiration = BuildDetails.default.profileExpiration, now > profileExpiration - expirationAlertWindow else { + + guard let profileExpiration = BuildDetails.default.profileExpiration else { return } @@ -28,6 +28,10 @@ class AppExpirationAlerter { let timeUntilExpiration = expirationDate.timeIntervalSince(now) + if timeUntilExpiration > expirationAlertWindow { + return + } + let minimumTimeBetweenAlerts: TimeInterval = timeUntilExpiration > .hours(24) ? .days(2) : .hours(1) if let lastAlertDate = UserDefaults.appGroup?.lastProfileExpirationAlertDate { From b292371e9390c4caa5be5ad8f83b6689c8228edf Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 28 Aug 2023 14:42:37 -0700 Subject: [PATCH 07/11] test an alternative method for isTestFlightBuild --- Loop/Managers/AppExpirationAlerter.swift | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift index a637927f37..cbea4f4941 100644 --- a/Loop/Managers/AppExpirationAlerter.swift +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -104,7 +104,7 @@ class AppExpirationAlerter { formatter.unitsStyle = .full formatter.zeroFormattingBehavior = .dropLeading formatter.maximumUnitCount = maxUnitCount - return formatter; + return formatter } static func buildDate() -> Date? { @@ -122,9 +122,23 @@ class AppExpirationAlerter { } static func isTestFlightBuild() -> Bool { - return BuildDetails.default.isGitHubBuild ?? false + // If an "embedded.mobileprovision" is present in the main bundle, then + // this is an Xcode, Ad-Hoc, or Enterprise distribution. Return false. + if Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision") != nil { + return false + } + + // If an app store receipt is not present in the main bundle, then we cannot + // say whether this is a TestFlight or App Store distribution. Return false. + guard let receiptName = Bundle.main.appStoreReceiptURL?.lastPathComponent else { + return false + } + + // A TestFlight distribution presents a "sandboxReceipt", while an App Store + // distribution presents a "receipt". Return true if we have a TestFlight receipt. + return "sandboxReceipt".caseInsensitiveCompare(receiptName) == .orderedSame } - + static func calculateExpirationDate(profileExpiration: Date) -> Date { let isTestFlight = isTestFlightBuild() From 390938075e9e5e9524050dbe4c758e8f3a36141d Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 28 Aug 2023 15:42:34 -0700 Subject: [PATCH 08/11] Revert changes not needed by isTestFlightBuild function --- Common/Models/BuildDetails.swift | 4 ---- Scripts/capture-build-details.sh | 6 ------ 2 files changed, 10 deletions(-) diff --git a/Common/Models/BuildDetails.swift b/Common/Models/BuildDetails.swift index 0badb257ac..63517e7e79 100644 --- a/Common/Models/BuildDetails.swift +++ b/Common/Models/BuildDetails.swift @@ -65,9 +65,5 @@ class BuildDetails { var workspaceGitBranch: String? { return dict["com-loopkit-LoopWorkspace-git-branch"] as? String } - - var isGitHubBuild: Bool? { - return dict["com-loopkit-GitHub-build"] as? Bool - } } diff --git a/Scripts/capture-build-details.sh b/Scripts/capture-build-details.sh index 5431b4f239..66f827d7c3 100755 --- a/Scripts/capture-build-details.sh +++ b/Scripts/capture-build-details.sh @@ -72,7 +72,6 @@ if [ -e "${provisioning_profile_path}" ]; then profile_expire_date=$(security cms -D -i "${provisioning_profile_path}" | plutil -p - | grep ExpirationDate | cut -b 23-) # Convert to plutil format profile_expire_date=$(date -j -f "%Y-%m-%d %H:%M:%S" "${profile_expire_date}" +"%Y-%m-%dT%H:%M:%SZ") - plutil -replace com-loopkit-Loop-profile-expiration -date "${profile_expire_date}" "${info_plist_path}" else warn "Invalid provisioning profile path ${provisioning_profile_path}" @@ -92,8 +91,3 @@ then fi popd . > /dev/null fi - -# Handle github action -if [ -n "$GITHUB_ACTIONS" ]; then - plutil -replace com-loopkit-GitHub-build -bool true "${info_plist_path}" -fi From b6eb08beba45cfcfcab004f2999cbc0b2b420822 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Tue, 29 Aug 2023 16:15:42 -0700 Subject: [PATCH 09/11] Add simulator check for completeness --- Loop/Managers/AppExpirationAlerter.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift index cbea4f4941..e7768f92b6 100644 --- a/Loop/Managers/AppExpirationAlerter.swift +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -122,6 +122,12 @@ class AppExpirationAlerter { } static func isTestFlightBuild() -> Bool { + // If the target environment is a simulator, then + // this is not a TestFlight distribution. Return false. + #if targetEnvironment(simulator) + return false + #endif + // If an "embedded.mobileprovision" is present in the main bundle, then // this is an Xcode, Ad-Hoc, or Enterprise distribution. Return false. if Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision") != nil { From 156401e333ea66f331989f59ed22f0bb189b7ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 30 Aug 2023 21:52:31 +0200 Subject: [PATCH 10/11] Refactoring and renaming --- Loop/Views/SettingsView.swift | 73 ++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index a07d623f40..680adcc539 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -416,50 +416,53 @@ extension SettingsView { /* DIY loop specific component to show users the amount of time remaining on their build before a rebuild is necessary. */ - private func profileExpirationSection(profileExpiration:Date) -> some View { + private func appExpirationSection(profileExpiration: Date) -> some View { let expirationDate = AppExpirationAlerter.calculateExpirationDate(profileExpiration: profileExpiration) let isTestFlight = AppExpirationAlerter.isTestFlightBuild() - - let nearExpiration : Bool = AppExpirationAlerter.isNearExpiration(expirationDate: expirationDate) + let nearExpiration = AppExpirationAlerter.isNearExpiration(expirationDate: expirationDate) let profileExpirationMsg = AppExpirationAlerter.createProfileExpirationSettingsMessage(expirationDate: expirationDate) let readableExpirationTime = Self.dateFormatter.string(from: expirationDate) if isTestFlight { - return Section(header: SectionHeader(label: NSLocalizedString("TestFlight", comment: "Settings app TestFlight section")), - footer: Text(NSLocalizedString("TestFlight expires ", comment: "Time that build expires") + readableExpirationTime)) { - if(nearExpiration) { - Text(profileExpirationMsg).foregroundColor(.red) - } else { - HStack { - Text("TestFlight Expiration", comment: "Settings TestFlight expiration view") - Spacer() - Text(profileExpirationMsg).foregroundColor(Color.secondary) - } - } - Button(action: { - UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/")!) - }) { - Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) - } - } + return createAppExpirationSection( + headerLabel: NSLocalizedString("TestFlight", comment: "Settings app TestFlight section"), + footerLabel: NSLocalizedString("TestFlight expires ", comment: "Time that build expires") + readableExpirationTime, + expirationLabel: NSLocalizedString("TestFlight Expiration", comment: "Settings TestFlight expiration view"), + updateURL: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/", + nearExpiration: nearExpiration, + expirationMessage: profileExpirationMsg + ) } else { - return Section(header: SectionHeader(label: NSLocalizedString("App Profile", comment: "Settings app profile section")), - footer: Text(NSLocalizedString("Profile expires ", comment: "Time that profile expires") + readableExpirationTime)) { - if(nearExpiration) { - Text(profileExpirationMsg).foregroundColor(.red) - } else { - HStack { - Text("Profile Expiration", comment: "Settings App Profile expiration view") - Spacer() - Text(profileExpirationMsg).foregroundColor(Color.secondary) - } - } - Button(action: { - UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) - }) { - Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) + return createAppExpirationSection( + headerLabel: NSLocalizedString("App Profile", comment: "Settings app profile section"), + footerLabel: NSLocalizedString("Profile expires ", comment: "Time that profile expires") + readableExpirationTime, + expirationLabel: NSLocalizedString("Profile Expiration", comment: "Settings App Profile expiration view"), + updateURL: "https://loopkit.github.io/loopdocs/build/updating/", + nearExpiration: nearExpiration, + expirationMessage: profileExpirationMsg + ) + } + } + + private func createAppExpirationSection(headerLabel: String, footerLabel: String, expirationLabel: String, updateURL: String, nearExpiration: Bool, expirationMessage: String) -> some View { + return Section( + header: SectionHeader(label: headerLabel), + footer: Text(footerLabel) + ) { + if nearExpiration { + Text(expirationMessage).foregroundColor(.red) + } else { + HStack { + Text(expirationLabel) + Spacer() + Text(expirationMessage).foregroundColor(Color.secondary) } } + Button(action: { + UIApplication.shared.open(URL(string: updateURL)!) + }) { + Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) + } } } From 180eceb4c05274d96f0dadc01dc03abe746b91ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 30 Aug 2023 22:05:00 +0200 Subject: [PATCH 11/11] Refactoring and renaming --- Loop/Views/SettingsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index 680adcc539..958e38f100 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -75,7 +75,7 @@ public struct SettingsView: View { supportSection if let profileExpiration = BuildDetails.default.profileExpiration, FeatureFlags.profileExpirationSettingsViewEnabled { - profileExpirationSection(profileExpiration: profileExpiration) + appExpirationSection(profileExpiration: profileExpiration) } } }