Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalServiceKit/Environment/BuildFlags.swift
1 views
//
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
import LibSignalClient

enum FeatureBuild: Int, Comparable {
    case dev
    case `internal`
    case beta
    case production

    static func <(lhs: Self, rhs: Self) -> Bool {
        return lhs.rawValue < rhs.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 BuildFlags {

    public static let choochoo = build <= .internal

    public static let failDebug = build <= .internal

    public static let linkedPhones = build <= .internal

    public static let isPrerelease = build <= .beta

    public static let shouldUseTestIntervals = build <= .beta

    public enum Backups {
        /// This is also controlled via remote-config.
        /// - SeeAlso ``RemoteConfig/backupsMegaphone``.
        public static let showMegaphones = build <= .beta

        public static let showOptimizeMedia = build <= .dev

        public static let restoreFailOnAnyError = build <= .beta
        public static let detailedBenchLogging = build <= .internal
        public static let archiveErrorDisplay = build <= .internal

        public static let avoidAppAttestForDevs = build <= .dev
        public static let avoidStoreKitForTesters = build <= .beta

        public static let mediaErrorDisplay = build <= .beta
        public static let useLowerDefaultListMediaRefreshInterval = build <= .beta
    }

    public enum RemoteMute {
        public static let send = build <= .internal
    }

    static let netBuildVariant: Net.BuildVariant = build <= .beta ? .beta : .production

    // Turn this off after all still-registered clients have run this
    // migration. That should happen by 2026-06-01. Then, delete all the code
    // that's now dead because this is false.
    public static let decodeDeprecatedPreKeys = true

    // Turn this off after all still-registered clients have run this
    // migration. That should happen by 2026-08-04. Then, delete all the code
    // that's now dead because this is false.
    public static let migrateDeprecatedSessions = true

    public enum MemberLabel {
        public static let display = true
        public static let send = true
    }

    public enum KeyTransparency {
        public static let enabled = build <= .internal
        public static let conservativeSelfCheck = build <= .internal
    }

    public static let pollOneOnOneSend = build <= .internal

    public enum AdminDelete {
        public static let receive = true
        public static let send = build <= .internal
    }

    public enum GroupTerminate {
        public static let receive = true
        public static let send = build <= .internal
    }

    public enum AttachmentBackfill {
        public static let handleRequests = true
    }

    public static let collapsingChatEvents = build <= .internal
}

// MARK: -

extension BuildFlags {
    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 buildFlagString: String?
        switch build {
        case .dev:
            buildFlagString = LocalizationNotNeeded("Dev")
        case .internal:
            buildFlagString = LocalizationNotNeeded("Internal")
        case .beta:
            buildFlagString = LocalizationNotNeeded("Beta")
        case .production:
            // Production can be inferred from the lack of flag
            buildFlagString = nil
        }

        let configuration: String? = {
#if DEBUG
            LocalizationNotNeeded("Debug")
#elseif TESTABLE_BUILD
            LocalizationNotNeeded("Testable")
#else
            // RELEASE can be inferred from the lack of configuration.
            nil
#endif
        }()

        return [buildFlagString, 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 BuildFlags.
public enum DebugFlags {
    public static let internalLogging = build <= .internal

    public static let betaLogging = build <= .beta

    public static let testPopulationErrorAlerts = build <= .beta

    public static let audibleErrorLogging = build <= .internal

    public static let internalSettings = build <= .internal

    public static let internalMegaphoneEligible = build <= .internal

    public static let verboseNotificationLogging = build <= .internal

    public static let deviceTransferVerboseProgressLogging = build <= .internal

    public static let messageDetailsExtraInfo = build <= .internal

    public static let exposeCensorshipCircumvention = build <= .internal

    public static let extraDebugLogs = build <= .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 callingNeverRelay = TestableFlag(
        false,
        title: LocalizationNotNeeded("Calling: Never use relay"),
        details: LocalizationNotNeeded("1:1 calls will not connect to a TURN server (remote party may still use TURN)."),
    )

    public static let callingForceVp9Off = TestableFlag(
        false,
        title: LocalizationNotNeeded("Calling: Never use VP9"),
        details: LocalizationNotNeeded("1:1 calls will never use VP9 (overrides remote config)."),
    )

    public static let callingForceVp9On = TestableFlag(
        false,
        title: LocalizationNotNeeded("Calling: Always offer VP9"),
        details: LocalizationNotNeeded("1:1 calls will always offer VP9 (overrides remote config and \"Never use VP9\")."),
    )

    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,
            callingNeverRelay,
            callingForceVp9Off,
            callingForceVp9On,
            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 else { return }
            self.set(self.defaultValue)
        }
    }

    public func get() -> Bool {
        guard build <= .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")
}