Signal-iOS/SignalServiceKit/Environment/FeatureFlags.swift
2025-09-23 09:13:46 -04:00

236 lines
7.6 KiB
Swift

//
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
enum FeatureBuild: Int {
case dev
case `internal`
case beta
case production
}
private extension FeatureBuild {
func includes(_ level: FeatureBuild) -> Bool {
return self.rawValue <= level.rawValue
}
}
private let build = FeatureBuild.current
// MARK: -
/// By centralizing feature flags here and documenting their rollout plan,
/// it's easier to review which feature flags are in play.
public enum FeatureFlags {
public static let choochoo = build.includes(.internal)
public static let failDebug = build.includes(.internal)
public static let linkedPhones = build.includes(.internal)
public static let preRegDeviceTransfer = build.includes(.dev)
public static let isPrerelease = build.includes(.beta)
public static let shouldUseTestIntervals = build.includes(.beta)
/// If we ever need to internally detect database corruption again in the
/// future, we can re-enable this.
public static let periodicallyCheckDatabaseIntegrity: Bool = false
public enum Backups {
public static let supported = build.includes(.internal)
public static let showSettings = build.includes(.internal)
public static let showMegaphones = build.includes(.internal)
public static let showOptimizeMedia = build.includes(.dev)
public static let restoreFailOnAnyError = build.includes(.beta)
public static let detailedBenchLogging = build.includes(.internal)
public static let errorDisplay = build.includes(.internal)
public static let avoidAppAttestForDevs = build.includes(.dev)
public static let avoidStoreKitForTesters = build.includes(.beta)
}
public static let runTSAttachmentMigrationInMainAppBackground = true
public static let runTSAttachmentMigrationBlockingOnLaunch = true
public static let useNewConversationLoadIndex = true
public static let libsignalEnforceMinTlsVersion = false
public static let moveDraftsUpChatList = true
/// We are still making Xcode 16 builds as of writing this, and some iOS 26
/// changes must only be applied if the SDK is also iOS 26.
#if compiler(>=6.2)
public static let iOS26SDKIsAvailable = true
#else
public static let iOS26SDKIsAvailable = false
#endif
public static let pollSend = build.includes(.dev)
}
// MARK: -
extension FeatureFlags {
public static var buildVariantString: String? {
// Leaving this internal only for now. If we ever move this to
// HelpSettings we need to localize these strings
guard DebugFlags.internalSettings else {
owsFailDebug("Incomplete implementation. Needs localization")
return nil
}
let featureFlagString: String?
switch build {
case .dev:
featureFlagString = LocalizationNotNeeded("Dev")
case .internal:
featureFlagString = LocalizationNotNeeded("Internal")
case .beta:
featureFlagString = LocalizationNotNeeded("Beta")
case .production:
// Production can be inferred from the lack of flag
featureFlagString = nil
}
let configuration: String? = {
#if DEBUG
LocalizationNotNeeded("Debug")
#elseif TESTABLE_BUILD
LocalizationNotNeeded("Testable")
#else
// RELEASE can be inferred from the lack of configuration. This will only be hit if the outer #if is removed.
nil
#endif
}()
// If we're Production+Release, this will return nil and won't show up in Help Settings
return [featureFlagString, configuration]
.compactMap { $0 }
.joined(separator: "")
.nilIfEmpty
}
}
// MARK: -
/// Flags that we'll leave in the code base indefinitely that are helpful for
/// development should go here, rather than cluttering up FeatureFlags.
public enum DebugFlags {
public static let internalLogging = build.includes(.internal)
public static let betaLogging = build.includes(.beta)
public static let testPopulationErrorAlerts = build.includes(.beta)
public static let audibleErrorLogging = build.includes(.internal)
public static let internalSettings = build.includes(.internal)
public static let internalMegaphoneEligible = build.includes(.internal)
public static let verboseNotificationLogging = build.includes(.internal)
public static let deviceTransferVerboseProgressLogging = build.includes(.internal)
public static let messageDetailsExtraInfo = build.includes(.internal)
public static let exposeCensorshipCircumvention = build.includes(.internal)
public static let extraDebugLogs = build.includes(.internal)
public static let messageSendsFail = TestableFlag(
false,
title: LocalizationNotNeeded("Message Sends Fail"),
details: LocalizationNotNeeded("All outgoing message sends will fail.")
)
public static let callingUseTestSFU = TestableFlag(
false,
title: LocalizationNotNeeded("Calling: Use Test SFU"),
details: LocalizationNotNeeded("Group calls will connect to sfu.test.voip.signal.org.")
)
public static let delayedMessageResend = TestableFlag(
false,
title: LocalizationNotNeeded("Delayed message resend"),
details: LocalizationNotNeeded("Waits 10s before responding to a resend request.")
)
public static let fastPlaceholderExpiration = TestableFlag(
false,
title: LocalizationNotNeeded("Early placeholder expiration"),
details: LocalizationNotNeeded("Shortens the valid window for message resend+recovery."),
toggleHandler: { _ in
SSKEnvironment.shared.messageDecrypterRef.cleanUpExpiredPlaceholders()
}
)
public static func allTestableFlags() -> [TestableFlag] {
return [
callingUseTestSFU,
delayedMessageResend,
fastPlaceholderExpiration,
messageSendsFail,
]
}
}
// MARK: -
public class TestableFlag {
private let defaultValue: Bool
private let flag: AtomicBool
public let title: String
public let details: String
public let toggleHandler: ((Bool) -> Void)?
fileprivate init(_ defaultValue: Bool,
title: String,
details: String,
toggleHandler: ((Bool) -> Void)? = nil) {
self.defaultValue = defaultValue
self.title = title
self.details = details
self.flag = AtomicBool(defaultValue, lock: .sharedGlobal)
self.toggleHandler = toggleHandler
// Normally we'd store the observer here and remove it in deinit.
// But TestableFlags are always static; they don't *get* deinitialized except in testing.
NotificationCenter.default.addObserver(forName: Self.ResetAllTestableFlagsNotification,
object: nil, queue: nil) { [weak self] _ in
guard let self = self else { return }
self.set(self.defaultValue)
}
}
public func get() -> Bool {
guard build.includes(.internal) else {
return defaultValue
}
return flag.get()
}
public func set(_ value: Bool) {
flag.set(value)
toggleHandler?(value)
}
@objc
private func switchDidChange(_ sender: UISwitch) {
set(sender.isOn)
}
public var switchSelector: Selector { #selector(switchDidChange(_:)) }
public static let ResetAllTestableFlagsNotification = NSNotification.Name("ResetAllTestableFlags")
}