// // Copyright 2019 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation import SignalCoreKit 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: - @objc public enum StorageMode: Int { // Use GRDB. case grdb // These modes can be used while running tests. // They are more permissive than the release modes. // // The build shepherd should be running the test // suites in .grdbTests mode before each release. case grdbTests } // MARK: - extension StorageMode: CustomStringConvertible { public var description: String { switch self { case .grdb: return ".grdb" case .grdbTests: return ".grdbTests" } } } // MARK: - /// By centralizing feature flags here and documenting their rollout plan, it's easier to review /// which feature flags are in play. @objc(SSKFeatureFlags) public class FeatureFlags: NSObject { public static let choochoo = build.includes(.internal) public static let linkedPhones = build.includes(.internal) public static let preRegDeviceTransfer = build.includes(.dev) public static let paymentsScrubDetails = false public static let deprecateREST = false public static let isPrerelease = build.includes(.beta) @objc public static var notificationServiceExtension: Bool { // The CallKit APIs for the NSE are only available from iOS 14.5 and on, // however there is a significant bug in iOS 14 where the NSE will not // launch properly after a crash so we only support it in iOS 15. if #available(iOS 15, *) { return true } return false } /// 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 static let doNotSendGroupChangeMessagesOnProfileKeyRotation = false public static let messageBackupFileAlpha = build.includes(.internal) public static let messageBackupFileAlphaRegistrationFlow = build.includes(.dev) public static let readV2Attachments = false public static let newAttachmentsUseV2 = false public static let v2ThreadAttachments = readV2Attachments && false public static let callLinkCreate = build.includes(.dev) public static let callRaiseHandReceiveSupport = build.includes(.internal) public static let callRaiseHandSendSupport = build.includes(.internal) public static let enableDeleteSyncCapability = build.includes(.internal) } // 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 } @objc public static var storageMode: StorageMode { if CurrentAppContext().isRunningTests { return .grdbTests } else { return .grdb } } @objc public static var storageModeDescription: String { return "\(storageMode)" } } // MARK: - /// Flags that we'll leave in the code base indefinitely that are helpful for /// development should go here, rather than cluttering up FeatureFlags. @objc(SSKDebugFlags) public class DebugFlags: NSObject { @objc 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) @objc public static let reduceLogChatter: Bool = { // This is a little verbose to make it easy to change while developing. if CurrentAppContext().isRunningTests { return true } return false }() public static let aggressiveProfileFetching = TestableFlag( false, title: LocalizationNotNeeded("Aggressive profile fetching"), details: LocalizationNotNeeded("Client will update profiles aggressively.") ) // Currently this flag is only honored by NetworkManager, // but we could eventually honor in other places as well: // // * The socket manager. // * Places we make requests using tasks. public static let logCurlOnSuccess = false 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 dontSendContactOrGroupSyncMessages = TestableFlag( false, title: LocalizationNotNeeded("Don't send contact or group sync messages"), details: LocalizationNotNeeded("Client will not send contact or group info to linked devices.") ) public static let forceAttachmentDownloadFailures = TestableFlag( false, title: LocalizationNotNeeded("Force attachment download failures."), details: LocalizationNotNeeded("All attachment downloads will fail.") ) public static let forceAttachmentDownloadPendingMessageRequest = TestableFlag( false, title: LocalizationNotNeeded("Attachment download vs. message request."), details: LocalizationNotNeeded("Attachment downloads will be blocked by pending message request.") ) public static let forceAttachmentDownloadPendingManualDownload = TestableFlag( false, title: LocalizationNotNeeded("Attachment download vs. manual download."), details: LocalizationNotNeeded("Attachment downloads will be blocked by manual download.") ) public static let fastPerfTests = false public static let extraDebugLogs = build.includes(.internal) @objc public static let paymentsIgnoreBlockTimestamps = TestableFlag( false, title: LocalizationNotNeeded("Payments: Ignore ledger block timestamps"), details: LocalizationNotNeeded("Payments will not fill in missing ledger block timestamps") ) public static let paymentsIgnoreCurrencyConversions = TestableFlag( false, title: LocalizationNotNeeded("Payments: Ignore currency conversions"), details: LocalizationNotNeeded("App will behave as though currency conversions are unavailable") ) public static let paymentsHaltProcessing = TestableFlag( false, title: LocalizationNotNeeded("Payments: Halt Processing"), details: LocalizationNotNeeded("Processing of payments will pause") ) public static let paymentsIgnoreBadData = TestableFlag( false, title: LocalizationNotNeeded("Payments: Ignore bad data"), details: LocalizationNotNeeded("App will skip asserts for invalid data") ) public static let paymentsFailOutgoingSubmission = TestableFlag( false, title: LocalizationNotNeeded("Payments: Fail outgoing submission"), details: LocalizationNotNeeded("Submission of outgoing transactions will always fail") ) public static let paymentsFailOutgoingVerification = TestableFlag( false, title: LocalizationNotNeeded("Payments: Fail outgoing verification"), details: LocalizationNotNeeded("Verification of outgoing transactions will always fail") ) public static let paymentsFailIncomingVerification = TestableFlag( false, title: LocalizationNotNeeded("Payments: Fail incoming verification"), details: LocalizationNotNeeded("Verification of incoming receipts will always fail") ) public static let paymentsDoubleNotify = TestableFlag( false, title: LocalizationNotNeeded("Payments: Double notify"), details: LocalizationNotNeeded("App will send two payment notifications and sync messages for each outgoing payment") ) public static let paymentsNoRequestsComplete = TestableFlag( false, title: LocalizationNotNeeded("Payments: No requests complete"), details: LocalizationNotNeeded("MC SDK network activity never completes") ) public static let paymentsMalformedMessages = TestableFlag( false, title: LocalizationNotNeeded("Payments: Malformed messages"), details: LocalizationNotNeeded("Payment notifications and sync messages are malformed.") ) public static let paymentsSkipSubmissionAndOutgoingVerification = TestableFlag( false, title: LocalizationNotNeeded("Payments: Skip Submission And Verification"), details: LocalizationNotNeeded("Outgoing payments won't be submitted or verified.") ) public static let messageSendsFail = TestableFlag( false, title: LocalizationNotNeeded("Message Sends Fail"), details: LocalizationNotNeeded("All outgoing message sends will fail.") ) public static let disableUD = TestableFlag( false, title: LocalizationNotNeeded("Disable sealed sender"), details: LocalizationNotNeeded("Sealed sender will be disabled for all messages.") ) 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("Sender Key: Delayed message resend"), details: LocalizationNotNeeded("Waits 10s before responding to a resend request.") ) @objc public static let fastPlaceholderExpiration = TestableFlag( false, title: LocalizationNotNeeded("Sender Key: Early placeholder expiration"), details: LocalizationNotNeeded("Shortens the valid window for message resend+recovery."), toggleHandler: { _ in NSObject.messageDecrypter.cleanUpExpiredPlaceholders() } ) public static func allTestableFlags() -> [TestableFlag] { return [ aggressiveProfileFetching, callingUseTestSFU, delayedMessageResend, disableUD, dontSendContactOrGroupSyncMessages, fastPlaceholderExpiration, forceAttachmentDownloadFailures, forceAttachmentDownloadPendingManualDownload, forceAttachmentDownloadPendingMessageRequest, messageSendsFail, paymentsDoubleNotify, paymentsFailIncomingVerification, paymentsFailOutgoingSubmission, paymentsFailOutgoingVerification, paymentsHaltProcessing, paymentsIgnoreBadData, paymentsIgnoreBlockTimestamps, paymentsIgnoreCurrencyConversions, paymentsMalformedMessages, paymentsNoRequestsComplete, paymentsSkipSubmissionAndOutgoingVerification, ] } } // MARK: - @objc public class TestableFlag: NSObject { private let defaultValue: Bool private let affectsCapabilities: 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, affectsCapabilities: Bool = false, toggleHandler: ((Bool) -> Void)? = nil) { self.defaultValue = defaultValue self.title = title self.details = details self.affectsCapabilities = affectsCapabilities self.flag = AtomicBool(defaultValue, lock: .sharedGlobal) self.toggleHandler = toggleHandler super.init() // 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) } } @objc @available(swift, obsoleted: 1.0) public var value: Bool { self.get() } @objc public func get() -> Bool { guard build.includes(.internal) else { return defaultValue } return flag.get() } public func set(_ value: Bool) { flag.set(value) if affectsCapabilities { updateCapabilities() } toggleHandler?(value) } @objc public func switchDidChange(_ sender: UISwitch) { set(sender.isOn) } @objc public var switchSelector: Selector { #selector(switchDidChange(_:)) } @objc public static let ResetAllTestableFlagsNotification = NSNotification.Name("ResetAllTestableFlags") private func updateCapabilities() { firstly(on: DispatchQueue.global()) { () -> Promise in return Promise.wrapAsync { try await DependenciesBridge.shared.accountAttributesUpdater.updateAccountAttributes(authedAccount: .implicit()) } }.done { Logger.info("") }.catch { error in owsFailDebug("Error: \(error)") } } }