Skip to content

Commit

Permalink
Merge pull request #2052 from loopandlearn/github-build-expiration-date
Browse files Browse the repository at this point in the history
Update the app expiration alert for GitHub and Xcode builds
  • Loading branch information
ps2 authored Sep 8, 2023
2 parents 3f6d57d + 180eceb commit 2360edb
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 103 deletions.
8 changes: 4 additions & 4 deletions Loop.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,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 */; };
Expand Down Expand Up @@ -1563,7 +1563,7 @@
C1EB0D22299581D900628475 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/ckcomplication.strings; sourceTree = "<group>"; };
C1EE9E802A38D0FB0064784A /* BuildDetails.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = BuildDetails.plist; sourceTree = "<group>"; };
C1EF747128D6A44A00C8C083 /* CrashRecoveryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashRecoveryManager.swift; sourceTree = "<group>"; };
C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileExpirationAlerter.swift; sourceTree = "<group>"; };
C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExpirationAlerter.swift; sourceTree = "<group>"; };
C1F48FF62995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
C1F48FF72995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
C1F48FF82995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2306,7 +2306,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 */,
Expand Down Expand Up @@ -3649,7 +3649,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 */,
Expand Down
159 changes: 159 additions & 0 deletions Loop/Managers/AppExpirationAlerter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
//
// 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 else {
return
}

let expirationDate = calculateExpirationDate(profileExpiration: profileExpiration)

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 {
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 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/")!)
}))

} 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 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 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 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
dateFormatter.timeZone = TimeZone(identifier: "UTC")

guard let dateString = BuildDetails.default.buildDateString,
let date = dateFormatter.date(from: dateString) else {
return nil
}

return date
}

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 {
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()

if isTestFlight, let buildDate = buildDate() {
let testflightExpiration = Calendar.current.date(byAdding: .day, value: 90, to: buildDate)!

return profileExpiration < testflightExpiration ? profileExpiration : testflightExpiration
} else {
return profileExpiration
}
}
}
2 changes: 1 addition & 1 deletion Loop/Managers/LoopAppManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
86 changes: 0 additions & 86 deletions Loop/Managers/ProfileExpirationAlerter.swift

This file was deleted.

Loading

0 comments on commit 2360edb

Please sign in to comment.