diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj
index aaa0a470..ecba1c2d 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 = "<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>"; };
@@ -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 00000000..032cb95d
--- /dev/null
+++ b/Loop/Managers/AppExpirationAlerter.swift
@@ -0,0 +1,203 @@
+//
+//  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 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 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 = "yyyy-MM-dd'T'HH:mm:ssZ"
+        
+        guard let dateString = BuildDetails.default.buildDateString,
+              let date = dateFormatter.date(from: dateString) else {
+            return nil
+        }
+        
+        return date
+    }
+    
+    static func isTestFlightBuild() -> Bool {
+        if let provision = MobileProvision.read() {
+            return provision.entitlements.apsEnvironment == .development ? true : false
+        }
+        return false
+    }
+    
+    static func calculateExpirationDate(profileExpiration: Date) -> Date {
+        let isTestFlight = isTestFlightBuild()
+        
+        guard isTestFlight, let buildDate = buildDate() else {
+            return profileExpiration
+        }
+        
+        let testflightExpiration = Calendar.current.date(byAdding: .day, value: 90, to: buildDate)!
+        
+        return profileExpiration < testflightExpiration ? profileExpiration : testflightExpiration
+    }
+}
+
+struct MobileProvision: Decodable {
+    var name: String
+    var appIDName: String
+    var platform: [String]
+    var isXcodeManaged: Bool? = false
+    var creationDate: Date
+    var expirationDate: Date
+    var entitlements: Entitlements
+    
+    private enum CodingKeys : String, CodingKey {
+        case name = "Name"
+        case appIDName = "AppIDName"
+        case platform = "Platform"
+        case isXcodeManaged = "IsXcodeManaged"
+        case creationDate = "CreationDate"
+        case expirationDate = "ExpirationDate"
+        case entitlements = "Entitlements"
+    }
+    
+    // Sublevel: decode entitlements informations
+    struct Entitlements: Decodable {
+        let keychainAccessGroups: [String]
+        let getTaskAllow: Bool
+        let apsEnvironment: Environment
+        
+        private enum CodingKeys: String, CodingKey {
+            case keychainAccessGroups = "keychain-access-groups"
+            case getTaskAllow = "get-task-allow"
+            case apsEnvironment = "aps-environment"
+        }
+        
+        enum Environment: String, Decodable {
+            case development, production, disabled
+        }
+        
+        init(keychainAccessGroups: Array<String>, getTaskAllow: Bool, apsEnvironment: Environment) {
+            self.keychainAccessGroups = keychainAccessGroups
+            self.getTaskAllow = getTaskAllow
+            self.apsEnvironment = apsEnvironment
+        }
+        
+        init(from decoder: Decoder) throws {
+            let container = try decoder.container(keyedBy: CodingKeys.self)
+            let keychainAccessGroups: [String] = (try? container.decode([String].self, forKey: .keychainAccessGroups)) ?? []
+            let getTaskAllow: Bool = (try? container.decode(Bool.self, forKey: .getTaskAllow)) ?? false
+            let apsEnvironment: Environment = (try? container.decode(Environment.self, forKey: .apsEnvironment)) ?? .disabled
+            
+            self.init(keychainAccessGroups: keychainAccessGroups, getTaskAllow: getTaskAllow, apsEnvironment: apsEnvironment)
+        }
+    }
+}
+
+// Factory methods
+extension MobileProvision {
+    // Read mobileprovision file embedded in app.
+    static func read() -> MobileProvision? {
+        let profilePath: String? = Bundle.main.path(forResource: "embedded", ofType: "mobileprovision")
+        guard let path = profilePath else { return nil }
+        return read(from: path)
+    }
+    
+    // Read a .mobileprovision file on disk
+    static func read(from profilePath: String) -> MobileProvision? {
+        guard let plistDataString = try? NSString.init(contentsOfFile: profilePath,
+                                                       encoding: String.Encoding.isoLatin1.rawValue) else { return nil }
+        
+        // Skip binary part at the start of the mobile provisionning profile
+        let scanner = Scanner(string: plistDataString as String)
+        guard scanner.scanUpTo("<plist", into: nil) != false else { return nil }
+        
+        // ... and extract plist until end of plist payload (skip the end binary part.
+        var extractedPlist: NSString?
+        guard scanner.scanUpTo("</plist>", into: &extractedPlist) != false else { return nil }
+        
+        guard let plist = extractedPlist?.appending("</plist>").data(using: .isoLatin1) else { return nil }
+        let decoder = PropertyListDecoder()
+        do {
+            let provision = try decoder.decode(MobileProvision.self, from: plist)
+            return provision
+        } catch {
+            // TODO: log / handle error
+            return nil
+        }
+    }
+}
diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift
index 43c62d12..9edf481b 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 3aa74273..00000000
--- 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 0b4cb551..ff465413 100644
--- a/Loop/Views/SettingsView.swift
+++ b/Loop/Views/SettingsView.swift
@@ -417,25 +417,50 @@ 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"))
+                }
             }
         }
     }