RemoteConfig.swift 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111
  1. // Copyright 2025 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import FirebaseABTesting
  15. // TODO: interop
  16. // import FirebaseAnalyticsInterop
  17. import FirebaseCore
  18. import FirebaseCoreExtension
  19. import FirebaseInstallations
  20. import FirebaseRemoteConfigInterop
  21. import Foundation
  22. @_implementationOnly import GoogleUtilities
  23. public let namespaceGoogleMobilePlatform = "firebase"
  24. public let remoteConfigThrottledEndTimeInSecondsKey = "error_throttled_end_time_seconds"
  25. public let remoteConfigActivateNotification =
  26. Notification.Name("FIRRemoteConfigActivateNotification")
  27. /// Listener for the get methods.
  28. public typealias RemoteConfigListener = (String, [String: RemoteConfigValue]) -> Void
  29. @objc(FIRRemoteConfigSettings)
  30. public class RemoteConfigSettings: NSObject, NSCopying {
  31. /// Indicates the default value in seconds to set for the minimum interval that needs to elapse
  32. /// before a fetch request can again be made to the Remote Config backend. After a fetch request
  33. /// to
  34. /// the backend has succeeded, no additional fetch requests to the backend will be allowed until
  35. /// the
  36. /// minimum fetch interval expires. Note that you can override this default on a per-fetch request
  37. /// basis using `RemoteConfig.fetch(withExpirationDuration:)`. For example, setting
  38. /// the expiration duration to 0 in the fetch request will override the `minimumFetchInterval` and
  39. /// allow the request to proceed.
  40. @objc public var minimumFetchInterval: TimeInterval =
  41. .init(ConfigConstants.defaultMinimumFetchInterval)
  42. /// Indicates the default value in seconds to abandon a pending fetch request made to the backend.
  43. /// This value is set for outgoing requests as the `timeoutIntervalForRequest` as well as the
  44. /// `timeoutIntervalForResource` on the `NSURLSession`'s configuration.
  45. @objc public var fetchTimeout: TimeInterval =
  46. .init(ConfigConstants.httpDefaultConnectionTimeout)
  47. // Default init removed to allow for simpler initialization.
  48. @objc public func copy(with zone: NSZone? = nil) -> Any {
  49. let copy = RemoteConfigSettings()
  50. copy.minimumFetchInterval = minimumFetchInterval
  51. copy.fetchTimeout = fetchTimeout
  52. return copy
  53. }
  54. }
  55. /// Indicates whether updated data was successfully fetched.
  56. @objc(FIRRemoteConfigFetchStatus)
  57. public enum RemoteConfigFetchStatus: Int, Sendable {
  58. /// Config has never been fetched.
  59. case noFetchYet
  60. /// Config fetch succeeded.
  61. case success
  62. /// Config fetch failed.
  63. case failure
  64. /// Config fetch was throttled.
  65. case throttled
  66. }
  67. /// Indicates whether updated data was successfully fetched and activated.
  68. @objc(FIRRemoteConfigFetchAndActivateStatus)
  69. public enum RemoteConfigFetchAndActivateStatus: Int {
  70. /// The remote fetch succeeded and fetched data was activated.
  71. case successFetchedFromRemote
  72. /// The fetch and activate succeeded from already fetched but yet unexpired config data. You can
  73. /// control this using minimumFetchInterval property in FIRRemoteConfigSettings.
  74. case successUsingPreFetchedData
  75. /// The fetch and activate failed.
  76. case error
  77. }
  78. @objc(FIRRemoteConfigError)
  79. public enum RemoteConfigError: Int, LocalizedError, CustomNSError {
  80. /// Unknown or no error.
  81. case unknown = 8001
  82. /// Frequency of fetch requests exceeds throttled limit.
  83. case throttled = 8002
  84. /// Internal error that covers all internal HTTP errors.
  85. case internalError = 8003
  86. public var errorDescription: String? {
  87. switch self {
  88. case .unknown:
  89. return "Unknown error."
  90. case .throttled:
  91. return "Frequency of fetch requests exceeds throttled limit."
  92. case .internalError:
  93. return "Internal error."
  94. }
  95. }
  96. }
  97. @objc(FIRRemoteConfigUpdateError)
  98. public enum RemoteConfigUpdateError: Int, LocalizedError, CustomNSError {
  99. /// Unable to make a connection to the Remote Config backend.
  100. case streamError = 8001
  101. /// Unable to fetch the latest version of the config.
  102. case notFetched = 8002
  103. /// The ConfigUpdate message was unparsable.
  104. case messageInvalid = 8003
  105. /// The Remote Config real-time config update service is unavailable.
  106. case unavailable = 8004
  107. public var errorDescription: String? {
  108. switch self {
  109. case .streamError:
  110. return "Unable to make a connection to the Remote Config backend."
  111. case .notFetched:
  112. return "Unable to fetch the latest version of the config."
  113. case .messageInvalid:
  114. return "The ConfigUpdate message was unparsable."
  115. case .unavailable:
  116. return "The Remote Config real-time config update service is unavailable."
  117. }
  118. }
  119. }
  120. /// Firebase Remote Config custom signals error.
  121. @objc(FIRRemoteConfigCustomSignalsError)
  122. public enum RemoteConfigCustomSignalsError: Int, CustomNSError {
  123. /// Unknown error.
  124. case unknown = 8101
  125. /// Invalid value type in the custom signals dictionary.
  126. case invalidValueType = 8102
  127. /// Limit exceeded for key length, value length, or number of signals.
  128. case limitExceeded = 8103
  129. }
  130. /// Enumerated value that indicates the source of Remote Config data. Data can come from
  131. /// the Remote Config service, the DefaultConfig that is available when the app is first
  132. /// installed, or a static initialized value if data is not available from the service or
  133. /// DefaultConfig.
  134. @objc(FIRRemoteConfigSource)
  135. public enum RemoteConfigSource: Int {
  136. /// The data source is the Remote Config service.
  137. case remote
  138. /// The data source is the DefaultConfig defined for this app.
  139. case `default`
  140. /// The data doesn't exist, return a static initialized value.
  141. case `static`
  142. }
  143. // MARK: - RemoteConfig
  144. /// Firebase Remote Config class. The class method `remoteConfig()` can be used
  145. /// to fetch, activate and read config results and set default config results on the default
  146. /// Remote Config instance.
  147. @objc(FIRRemoteConfig)
  148. open class RemoteConfig: NSObject, NSFastEnumeration {
  149. /// All the config content.
  150. private let configContent: ConfigContent
  151. private let dbManager: ConfigDBManager
  152. @objc public var settings: ConfigSettings
  153. let configFetch: ConfigFetch
  154. private let configExperiment: ConfigExperiment
  155. private let configRealtime: ConfigRealtime
  156. private let queue: DispatchQueue
  157. // TODO: remove objc public/
  158. @objc public let appName: String
  159. private var listeners = [RemoteConfigListener]()
  160. let FIRNamespace: String
  161. // MARK: - Public Initializers and Accessors
  162. /// Returns the `RemoteConfig` instance for your (non-default) Firebase appID. Note that Firebase
  163. /// analytics does not work for non-default app instances. This singleton object contains the
  164. /// complete set of Remote Config parameter values available to the app, including the Active
  165. /// Config
  166. /// and Default Config. This object also caches values fetched from the Remote Config Server until
  167. /// they are copied to the Active Config by calling `activate())`. When you fetch values
  168. /// from the Remote Config Server using the non-default Firebase app, you should use this
  169. /// class method to create and reuse shared instance of `RemoteConfig`.
  170. @objc(remoteConfigWithApp:) public static func remoteConfig(app: FirebaseApp) -> RemoteConfig {
  171. return remoteConfig(withFIRNamespace: RemoteConfigConstants.NamespaceGoogleMobilePlatform,
  172. app: app)
  173. }
  174. /// Returns the `RemoteConfig` instance configured for the default Firebase app. This singleton
  175. /// object contains the complete set of Remote Config parameter values available to the app,
  176. /// including the Active Config and Default Config. This object also caches values fetched from
  177. /// the
  178. /// Remote Config server until they are copied to the Active Config by calling `activate()`. When
  179. /// you fetch values from the Remote Config server using the default Firebase app, you should use
  180. /// this class method to create and reuse a shared instance of `RemoteConfig`.
  181. @objc public static func remoteConfig() -> RemoteConfig {
  182. guard let app = FirebaseApp.app() else {
  183. fatalError("The default FirebaseApp instance must be configured before the " +
  184. "default Remote Config instance can be initialized. One way to ensure " +
  185. "this is to call `FirebaseApp.configure()` in the App Delegate's " +
  186. "`application(_:didFinishLaunchingWithOptions:)` or the `@main` struct's " +
  187. "initializer in SwiftUI.")
  188. }
  189. return remoteConfig(withFIRNamespace: RemoteConfigConstants.NamespaceGoogleMobilePlatform,
  190. app: app)
  191. }
  192. /// API for internal use only.
  193. @objc(remoteConfigWithFIRNamespace:)
  194. public static func remoteConfig(withFIRNamespace firebaseNamespace: String) -> RemoteConfig {
  195. guard let app = FirebaseApp.app() else {
  196. fatalError("The default FirebaseApp instance must be configured before the " +
  197. "default Remote Config instance can be initialized. One way to ensure " +
  198. "this is to call `FirebaseApp.configure()` in the App Delegate's " +
  199. "`application(_:didFinishLaunchingWithOptions:)` or the `@main` struct's " +
  200. "initializer in SwiftUI.")
  201. }
  202. return remoteConfig(withFIRNamespace: firebaseNamespace, app: app)
  203. }
  204. /// API for internal use only.
  205. /// Use the provider to generate and return instances of FIRRemoteConfig for this specific app and
  206. /// namespace. This will ensure the app is configured before Remote Config can return an instance.
  207. @objc(remoteConfigWithFIRNamespace:app:)
  208. public static func remoteConfig(withFIRNamespace firebaseNamespace: String = RemoteConfigConstants
  209. .NamespaceGoogleMobilePlatform,
  210. app: FirebaseApp) -> RemoteConfig {
  211. let provider = ComponentType<RemoteConfigInterop>
  212. .instance(
  213. for: RemoteConfigInterop.self,
  214. in: app.container
  215. ) as! any RemoteConfigProvider as RemoteConfigProvider
  216. return provider.remoteConfig(forNamespace: firebaseNamespace)!
  217. }
  218. /// Last successful fetch completion time.
  219. @objc public var lastFetchTime: Date {
  220. queue.sync {
  221. let lastFetchTimeInterval = self.settings.lastFetchTimeInterval
  222. return Date(timeIntervalSince1970: lastFetchTimeInterval)
  223. }
  224. }
  225. /// Last fetch status. The status can be any enumerated value from `RemoteConfigFetchStatus`.
  226. @objc public var lastFetchStatus: RemoteConfigFetchStatus {
  227. queue.sync {
  228. self.configFetch.settings.lastFetchStatus
  229. }
  230. }
  231. /// Config settings are custom settings.
  232. @objc public var configSettings: RemoteConfigSettings {
  233. get {
  234. // These properties *must* be accessed and returned on the lock queue
  235. // to ensure thread safety.
  236. let (minimumFetchInterval, fetchTimeout) = queue.sync {
  237. (self.settings.minimumFetchInterval, self.settings.fetchTimeout)
  238. }
  239. RCLog.debug("I-RCN000066",
  240. "Successfully read configSettings. Minimum Fetch Interval: " +
  241. "\(minimumFetchInterval), Fetch timeout: \(fetchTimeout)")
  242. let settings = RemoteConfigSettings()
  243. settings.minimumFetchInterval = minimumFetchInterval
  244. settings.fetchTimeout = fetchTimeout
  245. /// The NSURLSession needs to be recreated whenever the fetch timeout may be updated.
  246. configFetch.recreateNetworkSession()
  247. RCLog.debug("I-RCN987366",
  248. "Successfully read configSettings. Minimum Fetch Interval: " +
  249. "\(minimumFetchInterval), Fetch timeout: \(fetchTimeout)")
  250. return settings
  251. }
  252. set {
  253. queue.async {
  254. self.settings.minimumFetchInterval = newValue.minimumFetchInterval
  255. self.settings.fetchTimeout = newValue.fetchTimeout
  256. /// The NSURLSession needs to be recreated whenever the fetch timeout may be updated.
  257. self.configFetch.recreateNetworkSession()
  258. RCLog.debug("I-RCN000067",
  259. "Successfully set configSettings. Minimum Fetch Interval: " +
  260. "\(newValue.minimumFetchInterval), " +
  261. "Fetch timeout: \(newValue.fetchTimeout)")
  262. }
  263. }
  264. }
  265. @objc public subscript(key: String) -> RemoteConfigValue {
  266. return configValue(forKey: key)
  267. }
  268. /// Singleton instance of serial queue for queuing all incoming RC calls.
  269. public static let sharedRemoteConfigSerialQueue =
  270. DispatchQueue(label: "com.google.remoteconfig.serialQueue")
  271. // TODO: Designated initializer - Consolidate with next when objc tests are gone.
  272. @objc(initWithAppName:FIROptions:namespace:DBManager:configContent:analytics:)
  273. public
  274. convenience init(appName: String,
  275. options: FirebaseOptions,
  276. namespace: String,
  277. dbManager: ConfigDBManager,
  278. configContent: ConfigContent,
  279. analytics: FIRAnalyticsInterop?) {
  280. self.init(
  281. appName: appName,
  282. options: options,
  283. namespace: namespace,
  284. dbManager: dbManager,
  285. configContent: configContent,
  286. userDefaults: nil,
  287. analytics: analytics,
  288. configFetch: nil,
  289. configRealtime: nil
  290. )
  291. }
  292. /// Designated initializer
  293. @objc(
  294. initWithAppName:FIROptions:namespace:DBManager:configContent:userDefaults:analytics:configFetch:configRealtime:settings:
  295. )
  296. public
  297. init(appName: String,
  298. options: FirebaseOptions,
  299. namespace: String,
  300. dbManager: ConfigDBManager,
  301. configContent: ConfigContent,
  302. userDefaults: UserDefaults?,
  303. analytics: FIRAnalyticsInterop?,
  304. configFetch: ConfigFetch? = nil,
  305. configRealtime: ConfigRealtime? = nil,
  306. settings: ConfigSettings? = nil) {
  307. self.appName = appName
  308. self.dbManager = dbManager
  309. // Initialize RCConfigContent if not already.
  310. self.configContent = configContent
  311. // The fully qualified Firebase namespace is namespace:firappname.
  312. FIRNamespace = "\(namespace):\(appName)"
  313. queue = RemoteConfig.sharedRemoteConfigSerialQueue
  314. self.settings = settings ?? ConfigSettings(
  315. databaseManager: dbManager,
  316. namespace: FIRNamespace,
  317. firebaseAppName: appName,
  318. googleAppID: options.googleAppID,
  319. userDefaults: userDefaults
  320. )
  321. let experimentController = ExperimentController.sharedInstance()
  322. configExperiment = ConfigExperiment(
  323. dbManager: dbManager,
  324. experimentController: experimentController
  325. )
  326. // Initialize with default config settings.
  327. self.configFetch = configFetch ?? ConfigFetch(
  328. content: configContent,
  329. DBManager: dbManager,
  330. settings: self.settings,
  331. analytics: analytics,
  332. experiment: configExperiment,
  333. queue: queue,
  334. namespace: FIRNamespace,
  335. options: options
  336. )
  337. self.configRealtime = configRealtime ?? ConfigRealtime(
  338. configFetch: self.configFetch,
  339. settings: self.settings,
  340. namespace: FIRNamespace,
  341. options: options
  342. )
  343. super.init()
  344. self.settings.loadConfigFromMetadataTable()
  345. if let analytics = analytics {
  346. let personalization = Personalization(analytics: analytics)
  347. addListener { key, config in
  348. personalization.logArmActive(rcParameter: key, config: config)
  349. }
  350. }
  351. }
  352. /// Ensures initialization is complete and clients can begin querying for Remote Config values.
  353. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  354. public func ensureInitialized() async throws {
  355. return try await withCheckedThrowingContinuation { continuation in
  356. self.ensureInitialized { error in
  357. if let error {
  358. continuation.resume(throwing: error)
  359. } else {
  360. continuation.resume()
  361. }
  362. }
  363. }
  364. }
  365. /// Ensures initialization is complete and clients can begin querying for Remote Config values.
  366. /// - Parameter completionHandler: Initialization complete callback with error parameter.
  367. @objc public func ensureInitialized(completionHandler: @Sendable @escaping (Error?) -> Void) {
  368. DispatchQueue.global(qos: .utility).async { [weak self] in
  369. guard let self else { return }
  370. let initializationSuccess = self.configContent.initializationSuccessful()
  371. let error = initializationSuccess ? nil :
  372. NSError(
  373. domain: ConfigConstants.remoteConfigErrorDomain,
  374. code: RemoteConfigError.internalError.rawValue,
  375. userInfo: [NSLocalizedDescriptionKey: "Timed out waiting for database load."]
  376. )
  377. completionHandler(error)
  378. }
  379. }
  380. /// Adds a listener that will be called whenever one of the get methods is called.
  381. /// - Parameter listener: Function that takes in the parameter key and the config.
  382. @objc public func addListener(_ listener: @escaping RemoteConfigListener) {
  383. queue.async {
  384. self.listeners.append(listener)
  385. }
  386. }
  387. private func callListeners(key: String, config: [String: RemoteConfigValue]) {
  388. queue.async { [weak self] in
  389. guard let self else { return }
  390. for listener in self.listeners {
  391. listener(key, config)
  392. }
  393. }
  394. }
  395. // MARK: - Fetch
  396. /// Fetches Remote Config data with a callback. Call `activate()` to make fetched data
  397. /// available to your app.
  398. ///
  399. /// Note: This method uses a Firebase Installations token to identify the app instance, and once
  400. /// it's called, it periodically sends data to the Firebase backend. (see
  401. /// `Installations.authToken(completion:)`).
  402. /// To stop the periodic sync, call `Installations.delete(completion:)`
  403. /// and avoid calling this method again.
  404. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  405. public func fetch() async throws -> RemoteConfigFetchStatus {
  406. return try await withUnsafeThrowingContinuation { continuation in
  407. self.fetch { status, error in
  408. if let error {
  409. continuation.resume(throwing: error)
  410. } else {
  411. continuation.resume(returning: status)
  412. }
  413. }
  414. }
  415. }
  416. /// Fetches Remote Config data with a callback. Call `activate()` to make fetched data
  417. /// available to your app.
  418. ///
  419. /// Note: This method uses a Firebase Installations token to identify the app instance, and once
  420. /// it's called, it periodically sends data to the Firebase backend. (see
  421. /// `Installations.authToken(completion:)`).
  422. /// To stop the periodic sync, call `Installations.delete(completion:)`
  423. /// and avoid calling this method again.
  424. ///
  425. /// - Parameter completionHandler: Fetch operation callback with status and error parameters.
  426. @objc public func fetch(completionHandler: (
  427. @Sendable (RemoteConfigFetchStatus, Error?) -> Void
  428. )? =
  429. nil) {
  430. queue.async {
  431. self.fetch(withExpirationDuration: self.settings.minimumFetchInterval,
  432. completionHandler: completionHandler)
  433. }
  434. }
  435. /// Fetches Remote Config data and sets a duration that specifies how long config data lasts.
  436. /// Call `activateWithCompletion:` to make fetched data available to your app.
  437. ///
  438. /// - Parameter expirationDuration: Override the (default or optionally set `minimumFetchInterval`
  439. /// property in RemoteConfigSettings) `minimumFetchInterval` for only the current request, in
  440. /// seconds. Setting a value of 0 seconds will force a fetch to the backend.
  441. ///
  442. /// Note: This method uses a Firebase Installations token to identify the app instance, and once
  443. /// it's called, it periodically sends data to the Firebase backend. (see
  444. /// `Installations.authToken(completion:)`).
  445. /// To stop the periodic sync, call `Installations.delete(completion:)`
  446. /// and avoid calling this method again.
  447. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  448. public func fetch(withExpirationDuration expirationDuration: TimeInterval) async throws
  449. -> RemoteConfigFetchStatus {
  450. return try await withUnsafeThrowingContinuation { continuation in
  451. configFetch.fetchConfig(withExpirationDuration: expirationDuration) { status, error in
  452. if let error {
  453. continuation.resume(throwing: error)
  454. } else {
  455. continuation.resume(returning: status)
  456. }
  457. }
  458. }
  459. }
  460. /// Fetches Remote Config data and sets a duration that specifies how long config data lasts.
  461. /// Call `activateWithCompletion:` to make fetched data available to your app.
  462. ///
  463. /// - Parameter expirationDuration: Override the (default or optionally set `minimumFetchInterval`
  464. /// property in RemoteConfigSettings) `minimumFetchInterval` for only the current request, in
  465. /// seconds. Setting a value of 0 seconds will force a fetch to the backend.
  466. /// - Parameter completionHandler: Fetch operation callback with status and error parameters.
  467. ///
  468. /// Note: This method uses a Firebase Installations token to identify the app instance, and once
  469. /// it's called, it periodically sends data to the Firebase backend. (see
  470. /// `Installations.authToken(completion:)`).
  471. /// To stop the periodic sync, call `Installations.delete(completion:)`
  472. /// and avoid calling this method again.
  473. @objc public func fetch(withExpirationDuration expirationDuration: TimeInterval,
  474. completionHandler: (
  475. @Sendable (RemoteConfigFetchStatus, Error?) -> Void
  476. )? =
  477. nil) {
  478. configFetch.fetchConfig(withExpirationDuration: expirationDuration,
  479. completionHandler: completionHandler)
  480. }
  481. // MARK: - FetchAndActivate
  482. /// Fetches Remote Config data and if successful, activates fetched data. Optional completion
  483. /// handler callback is invoked after the attempted activation of data, if the fetch call
  484. /// succeeded.
  485. ///
  486. /// Note: This method uses a Firebase Installations token to identify the app instance, and once
  487. /// it's called, it periodically sends data to the Firebase backend. (see
  488. /// `Installations.authToken(completion:)`).
  489. /// To stop the periodic sync, call `Installations.delete(completion:)`
  490. /// and avoid calling this method again.
  491. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  492. public func fetchAndActivate() async throws -> RemoteConfigFetchAndActivateStatus {
  493. return try await withUnsafeThrowingContinuation { continuation in
  494. fetchAndActivate { status, error in
  495. if let error {
  496. continuation.resume(throwing: error)
  497. } else {
  498. continuation.resume(returning: status)
  499. }
  500. }
  501. }
  502. }
  503. /// Fetches Remote Config data and if successful, activates fetched data. Optional completion
  504. /// handler callback is invoked after the attempted activation of data, if the fetch call
  505. /// succeeded.
  506. ///
  507. /// Note: This method uses a Firebase Installations token to identify the app instance, and once
  508. /// it's called, it periodically sends data to the Firebase backend. (see
  509. /// `Installations.authToken(completion:)`).
  510. /// To stop the periodic sync, call `Installations.delete(completion:)`
  511. /// and avoid calling this method again.
  512. ///
  513. /// - Parameter completionHandler: Fetch operation callback with status and error parameters.
  514. @objc public func fetchAndActivate(completionHandler:
  515. (@Sendable (RemoteConfigFetchAndActivateStatus, Error?) -> Void)? = nil) {
  516. fetch { [weak self] fetchStatus, error in
  517. guard let self else { return }
  518. // Fetch completed. We are being called on the main queue.
  519. // If fetch is successful, try to activate the fetched config
  520. if fetchStatus == .success, error == nil {
  521. self.activate { changed, error in
  522. if let completionHandler {
  523. DispatchQueue.main.async {
  524. let status: RemoteConfigFetchAndActivateStatus = error == nil ?
  525. .successFetchedFromRemote : .successUsingPreFetchedData
  526. completionHandler(status, nil)
  527. }
  528. }
  529. }
  530. } else if let completionHandler {
  531. DispatchQueue.main.async {
  532. let status: RemoteConfigFetchAndActivateStatus = fetchStatus == .success ?
  533. .successFetchedFromRemote : .error
  534. completionHandler(status, error)
  535. }
  536. }
  537. }
  538. }
  539. // MARK: - Activate
  540. /// Applies Fetched Config data to the Active Config, causing updates to the behavior and
  541. /// appearance of the app to take effect (depending on how config data is used in the app).
  542. /// - Returns A Bool indicating whether or not a change occurred.
  543. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  544. @discardableResult
  545. public func activate() async throws -> Bool {
  546. return try await withUnsafeThrowingContinuation { continuation in
  547. self.activate { updated, error in
  548. if let error {
  549. continuation.resume(throwing: error)
  550. } else {
  551. continuation.resume(returning: updated)
  552. }
  553. }
  554. }
  555. }
  556. /// Applies Fetched Config data to the Active Config, causing updates to the behavior and
  557. /// appearance of the app to take effect (depending on how config data is used in the app).
  558. /// - Parameter completion: Activate operation callback with changed and error parameters.
  559. @objc public func activate(completion: (@Sendable (Bool, Error?) -> Void)? = nil) {
  560. queue.async { [weak self] in
  561. guard let self else {
  562. let error = NSError(
  563. domain: ConfigConstants.remoteConfigErrorDomain,
  564. code: RemoteConfigError.internalError.rawValue,
  565. userInfo: ["ActivationFailureReason": "Internal Error."]
  566. )
  567. if let completion {
  568. DispatchQueue.main.async {
  569. completion(false, error)
  570. }
  571. }
  572. RCLog.error("I-RCN000068", "Internal error activating config.")
  573. return
  574. }
  575. // Check if the last fetched config has already been activated. Fetches with no data change
  576. // are ignored.
  577. if self.settings.lastETagUpdateTime == 0 ||
  578. self.settings.lastETagUpdateTime <= self.settings.lastApplyTimeInterval {
  579. RCLog.debug("I-RCN000069", "Most recently fetched config is already activated.")
  580. if let completion {
  581. DispatchQueue.main.async {
  582. completion(false, nil)
  583. }
  584. }
  585. return
  586. }
  587. self.configContent.copy(fromDictionary: self.configContent.fetchedConfig(),
  588. toSource: .active, forNamespace: self.FIRNamespace)
  589. self.settings.lastApplyTimeInterval = Date().timeIntervalSince1970
  590. // New config has been activated at this point
  591. RCLog.debug("I-RCN000069", "Config activated.")
  592. self.configContent.activatePersonalization()
  593. // Update last active template version number in setting and userDefaults.
  594. self.settings.updateLastActiveTemplateVersion()
  595. // Update activeRolloutMetadata
  596. self.configContent.activateRolloutMetadata { success in
  597. if success {
  598. self.notifyRolloutsStateChange(self.configContent.activeRolloutMetadata(),
  599. versionNumber: self.settings.lastActiveTemplateVersion)
  600. }
  601. }
  602. // Update experiments only for 3p namespace
  603. let namespace = self.FIRNamespace.split(separator: ":").first.map(String.init)
  604. if namespace == NamespaceGoogleMobilePlatform {
  605. DispatchQueue.main.async {
  606. self.notifyConfigHasActivated()
  607. }
  608. self.configExperiment.updateExperiments { _ in
  609. DispatchQueue.main.async {
  610. completion?(true, nil)
  611. }
  612. }
  613. } else {
  614. DispatchQueue.main.async {
  615. completion?(true, nil)
  616. }
  617. }
  618. }
  619. }
  620. private func notifyConfigHasActivated() {
  621. guard !appName.isEmpty else { return }
  622. // The Remote Config Swift SDK will be listening for this notification so it can tell SwiftUI
  623. // to update the UI.
  624. NotificationCenter.default.post(
  625. name: remoteConfigActivateNotification, object: self,
  626. userInfo: ["FIRAppNameKey": appName]
  627. )
  628. }
  629. // MARK: - Helpers
  630. private func fullyQualifiedNamespace(_ namespace: String) -> String {
  631. if namespace.contains(":") { return namespace } // Already fully qualified
  632. return "\(namespace):\(appName)"
  633. }
  634. private func defaultValue(forFullyQualifiedNamespace namespace: String, key: String)
  635. -> RemoteConfigValue {
  636. if let value = configContent.defaultConfig()[namespace]?[key] {
  637. return value
  638. }
  639. return RemoteConfigValue(data: Data(), source: .static)
  640. }
  641. // MARK: Get Config Result
  642. /// Gets the config value.
  643. /// - Parameter key: Config key.
  644. @objc public func configValue(forKey key: String) -> RemoteConfigValue {
  645. guard !key.isEmpty else {
  646. return RemoteConfigValue(data: Data(), source: .static)
  647. }
  648. let fullyQualifiedNamespace = fullyQualifiedNamespace(FIRNamespace)
  649. return queue.sync {
  650. guard let value = configContent.activeConfig()[fullyQualifiedNamespace]?[key] else {
  651. return defaultValue(forFullyQualifiedNamespace: fullyQualifiedNamespace, key: key)
  652. }
  653. if value.source != .remote {
  654. RCLog.error("I-RCN000001",
  655. "Key \(key) should come from source: \(RemoteConfigSource.remote.rawValue)" +
  656. "instead coming from source: \(value.source.rawValue)")
  657. }
  658. if let config = configContent.getConfigAndMetadata(forNamespace: fullyQualifiedNamespace)
  659. as? [String: RemoteConfigValue] {
  660. callListeners(key: key, config: config)
  661. }
  662. return value
  663. }
  664. }
  665. /// Gets the config value of a given source from the default namespace.
  666. /// - Parameter key: Config key.
  667. /// - Parameter source: Config value source.
  668. @objc public func configValue(forKey key: String, source: RemoteConfigSource) ->
  669. RemoteConfigValue {
  670. guard !key.isEmpty else {
  671. return RemoteConfigValue(data: Data(), source: .static)
  672. }
  673. let fullyQualifiedNamespace = self.fullyQualifiedNamespace(FIRNamespace)
  674. return queue.sync {
  675. let remoteConfigValue = switch source {
  676. case .remote:
  677. configContent.activeConfig()[fullyQualifiedNamespace]?[key]
  678. case .default:
  679. configContent.defaultConfig()[fullyQualifiedNamespace]?[key]
  680. case .static:
  681. RemoteConfigValue(data: Data(), source: .static)
  682. }
  683. return remoteConfigValue ?? RemoteConfigValue(data: Data(), source: source)
  684. }
  685. }
  686. @objc(allKeysFromSource:)
  687. public func allKeys(from source: RemoteConfigSource) -> [String] {
  688. queue.sync {
  689. let fullyQualifiedNamespace = self.fullyQualifiedNamespace(FIRNamespace)
  690. switch source {
  691. case .default:
  692. if let values = configContent.defaultConfig()[fullyQualifiedNamespace] {
  693. return Array(values.keys)
  694. }
  695. case .remote:
  696. if let values = configContent.activeConfig()[fullyQualifiedNamespace] {
  697. return Array(values.keys)
  698. }
  699. case .static: break
  700. }
  701. return []
  702. }
  703. }
  704. @objc public func keys(withPrefix prefix: String?) -> Set<String> {
  705. queue.sync {
  706. let fullyQualifiedNamespace = self.fullyQualifiedNamespace(FIRNamespace)
  707. if let config = configContent.activeConfig()[fullyQualifiedNamespace] {
  708. if let prefix = prefix, !prefix.isEmpty {
  709. return Set(config.keys.filter { $0.hasPrefix(prefix) })
  710. } else {
  711. return Set(config.keys)
  712. }
  713. }
  714. return Set<String>()
  715. }
  716. }
  717. public func countByEnumerating(with state: UnsafeMutablePointer<NSFastEnumerationState>,
  718. objects buffer: AutoreleasingUnsafeMutablePointer<AnyObject?>,
  719. count len: Int) -> Int {
  720. queue.sync {
  721. let fullyQualifiedNamespace = self.fullyQualifiedNamespace(FIRNamespace)
  722. if let config = configContent.activeConfig()[fullyQualifiedNamespace] as? NSDictionary {
  723. return config.countByEnumerating(with: state, objects: buffer, count: len)
  724. }
  725. return 0
  726. }
  727. }
  728. // MARK: - Defaults
  729. /// Sets config defaults for parameter keys and values in the default namespace config.
  730. /// - Parameter defaults: A dictionary mapping a NSString * key to a NSObject * value.
  731. @objc public func setDefaults(_ defaults: [String: Any]?) {
  732. let defaults = defaults ?? [String: Any]()
  733. let fullyQualifiedNamespace = self.fullyQualifiedNamespace(FIRNamespace)
  734. queue.async { [weak self] in
  735. guard let self else { return }
  736. self.configContent.copy(fromDictionary: [fullyQualifiedNamespace: defaults],
  737. toSource: .default,
  738. forNamespace: fullyQualifiedNamespace)
  739. self.settings.lastSetDefaultsTimeInterval = Date().timeIntervalSince1970
  740. }
  741. }
  742. /// Sets default configs from plist for default namespace.
  743. ///
  744. /// - Parameter fileName: The plist file name, with no file name extension. For example, if the
  745. /// plist file is named `defaultSamples.plist`:
  746. /// `RemoteConfig.remoteConfig().setDefaults(fromPlist: "defaultSamples")`
  747. @objc(setDefaultsFromPlistFileName:)
  748. public func setDefaults(fromPlist fileName: String?) {
  749. guard let fileName, !fileName.isEmpty else {
  750. RCLog.warning("I-RCN000037",
  751. "The plist file name cannot be nil or empty.")
  752. return
  753. }
  754. for bundle in [Bundle.main, Bundle(for: type(of: self))] {
  755. if let path = bundle.path(forResource: fileName, ofType: "plist"),
  756. let config = NSDictionary(contentsOfFile: path) as? [String: Any] {
  757. setDefaults(config)
  758. return
  759. }
  760. }
  761. RCLog.warning("I-RCN000037",
  762. "The plist file '\(fileName)' could not be found by Remote Config.")
  763. }
  764. /// Returns the default value of a given key from the default config.
  765. ///
  766. /// - Parameter key: The parameter key of default config.
  767. /// - Returns The default value of the specified key if the key exists; otherwise, nil.
  768. @objc public func defaultValue(forKey key: String) -> RemoteConfigValue? {
  769. queue.sync {
  770. let fullyQualifiedNamespace = self.fullyQualifiedNamespace(FIRNamespace)
  771. var value: RemoteConfigValue?
  772. if let config = configContent.defaultConfig()[fullyQualifiedNamespace] {
  773. value = config[key]
  774. if let value, value.source != .default {
  775. RCLog.error("I-RCN000002",
  776. "Key \(key) should come from source: \(RemoteConfigSource.default.rawValue)" +
  777. "instead coming from source: \(value.source.rawValue)")
  778. }
  779. }
  780. return value
  781. }
  782. }
  783. // MARK: Realtime
  784. /// Start listening for real-time config updates from the Remote Config backend and
  785. /// automatically fetch updates when they're available.
  786. ///
  787. /// If a connection to the Remote Config backend is not already open, calling this method will
  788. /// open it. Multiple listeners can be added by calling this method again, but subsequent calls
  789. /// re-use the same connection to the backend.
  790. ///
  791. /// Note: Real-time Remote Config requires the Firebase Remote Config Realtime API. See Get
  792. /// started with Firebase Remote Config at
  793. /// https://firebase.google.com/docs/remote-config/get-started
  794. /// for more information.
  795. ///
  796. /// - Parameter listener: The configured listener that is called for every config
  797. /// update.
  798. /// - Returns A registration representing the listener. The registration
  799. /// contains a remove method, which can be used to stop receiving updates for the provided
  800. /// listener.
  801. @discardableResult
  802. @objc(addOnConfigUpdateListener:)
  803. public func addOnConfigUpdateListener(remoteConfigUpdateCompletion listener: @Sendable @escaping (RemoteConfigUpdate?,
  804. Error?)
  805. -> Void)
  806. -> ConfigUpdateListenerRegistration {
  807. return configRealtime.addConfigUpdateListener(listener)
  808. }
  809. // MARK: Rollout
  810. @objc public func addRemoteConfigInteropSubscriber(_ subscriber: RolloutsStateSubscriber) {
  811. NotificationCenter.default.addObserver(
  812. forName: .rolloutsStateDidChange, object: self, queue: nil
  813. ) { notification in
  814. if let rolloutsState =
  815. notification.userInfo?[Notification.Name.rolloutsStateDidChange.rawValue]
  816. as? RolloutsState {
  817. subscriber.rolloutsStateDidChange(rolloutsState)
  818. }
  819. }
  820. // Send active rollout metadata stored in persistence while app launched if there is
  821. // an activeConfig
  822. let fullyQualifiedNamespace = fullyQualifiedNamespace(FIRNamespace)
  823. if let activeConfig = configContent.activeConfig()[fullyQualifiedNamespace],
  824. activeConfig.isEmpty == false {
  825. notifyRolloutsStateChange(configContent.activeRolloutMetadata(),
  826. versionNumber: settings.lastActiveTemplateVersion)
  827. }
  828. }
  829. private func notifyRolloutsStateChange(_ rolloutMetadata: [[String: Any]],
  830. versionNumber: String) {
  831. let rolloutsAssignments =
  832. rolloutsAssignments(with: rolloutMetadata, versionNumber: versionNumber)
  833. let rolloutsState = RolloutsState(assignmentList: rolloutsAssignments)
  834. RCLog.debug("I-RCN000069",
  835. "Send rollouts state notification with name " +
  836. "\(Notification.Name.rolloutsStateDidChange.rawValue) to RemoteConfigInterop.")
  837. NotificationCenter.default.post(
  838. name: .rolloutsStateDidChange,
  839. object: self,
  840. userInfo: [Notification.Name.rolloutsStateDidChange.rawValue: rolloutsState]
  841. )
  842. }
  843. private func rolloutsAssignments(with rolloutMetadata: [[String: Any]], versionNumber: String)
  844. -> [RolloutAssignment] {
  845. var rolloutsAssignments = [RolloutAssignment]()
  846. let fullyQualifiedNamespace = fullyQualifiedNamespace(FIRNamespace)
  847. for metadata in rolloutMetadata {
  848. if let rolloutID = metadata[ConfigConstants.fetchResponseKeyRolloutID] as? String,
  849. let variantID = metadata[ConfigConstants.fetchResponseKeyVariantID] as? String,
  850. let affectedParameterKeys =
  851. metadata[ConfigConstants.fetchResponseKeyAffectedParameterKeys] as? [String] {
  852. for key in affectedParameterKeys {
  853. let value = configContent.activeConfig()[fullyQualifiedNamespace]?[key] ??
  854. defaultValue(forFullyQualifiedNamespace: fullyQualifiedNamespace, key: key)
  855. let assignment = RolloutAssignment(
  856. rolloutId: rolloutID,
  857. variantId: variantID,
  858. templateVersion: Int64(versionNumber) ?? 0,
  859. parameterKey: key,
  860. parameterValue: value.stringValue
  861. )
  862. rolloutsAssignments.append(assignment)
  863. }
  864. }
  865. }
  866. return rolloutsAssignments
  867. }
  868. let customSignalsMaxKeyLength = 250
  869. let customSignalsMaxStringValueLength = 500
  870. let customSignalsMaxCount = 100
  871. // MARK: - Custom Signals
  872. /// Sets custom signals for this Remote Config instance.
  873. /// - Parameter customSignals: A dictionary mapping string keys to custom
  874. /// signals to be set for the app instance.
  875. ///
  876. /// When a new key is provided, a new key-value pair is added to the custom signals.
  877. /// If an existing key is provided with a new value, the corresponding signal is updated.
  878. /// If the value for a key is `nil`, the signal associated with that key is removed.
  879. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  880. public
  881. func setCustomSignals(_ customSignals: [String: CustomSignalValue?]) async throws {
  882. return try await withUnsafeThrowingContinuation { continuation in
  883. let customSignals = customSignals.mapValues { $0?.toNSObject() ?? NSNull() }
  884. self.setCustomSignalsImpl(customSignals) { error in
  885. if let error {
  886. continuation.resume(throwing: error)
  887. } else {
  888. continuation.resume()
  889. }
  890. }
  891. }
  892. }
  893. @available(swift 1000.0) // Objective-C only API
  894. @objc(setCustomSignals:withCompletion:) public func __setCustomSignals(_ customSignals: [
  895. String: Any
  896. ]?,
  897. withCompletion completionHandler: (
  898. @Sendable (Error?) -> Void
  899. )?) {
  900. setCustomSignalsImpl(customSignals, withCompletion: completionHandler)
  901. }
  902. private func setCustomSignalsImpl(_ customSignals: [String: Any]?,
  903. withCompletion completionHandler: (
  904. @Sendable (Error?) -> Void
  905. )?) {
  906. queue.async { [weak self] in
  907. guard let self else { return }
  908. guard let customSignals = customSignals else {
  909. if let completionHandler {
  910. DispatchQueue.main.async {
  911. completionHandler(nil)
  912. }
  913. }
  914. return
  915. }
  916. // Validate value type, and key and value length
  917. for (key, value) in customSignals {
  918. if !(value is NSNull || value is NSString || value is NSNumber) {
  919. let error = NSError(
  920. domain: ConfigConstants.remoteConfigCustomSignalsErrorDomain,
  921. code: RemoteConfigCustomSignalsError.invalidValueType.rawValue,
  922. userInfo: [
  923. NSLocalizedDescriptionKey: "Invalid value type. Must be NSString, NSNumber, or NSNull.",
  924. ]
  925. )
  926. if let completionHandler {
  927. DispatchQueue.main.async {
  928. completionHandler(error)
  929. }
  930. }
  931. return
  932. }
  933. if key.count > customSignalsMaxKeyLength ||
  934. (value is NSString && (value as! NSString).length > customSignalsMaxStringValueLength) {
  935. if let completionHandler {
  936. let error = NSError(
  937. domain: ConfigConstants.remoteConfigCustomSignalsErrorDomain,
  938. code: RemoteConfigCustomSignalsError.limitExceeded.rawValue,
  939. userInfo: [
  940. NSLocalizedDescriptionKey:
  941. "Custom signal keys and string values must be " +
  942. "\(customSignalsMaxKeyLength) and " +
  943. "\(customSignalsMaxStringValueLength) " +
  944. "characters or less respectively.",
  945. ]
  946. )
  947. DispatchQueue.main.async {
  948. completionHandler(error)
  949. }
  950. }
  951. return
  952. }
  953. }
  954. // Merge new signals with existing ones, overwriting existing keys.
  955. // Also, remove entries where the new value is null.
  956. var newCustomSignals = self.settings.customSignals
  957. for (key, value) in customSignals {
  958. if !(value is NSNull) {
  959. let stringValue = value is NSNumber ? (value as! NSNumber).stringValue : value as! String
  960. newCustomSignals[key] = stringValue
  961. } else {
  962. newCustomSignals.removeValue(forKey: key)
  963. }
  964. }
  965. // Check the size limit.
  966. if newCustomSignals.count > customSignalsMaxCount {
  967. if let completionHandler {
  968. let error = NSError(
  969. domain: ConfigConstants.remoteConfigCustomSignalsErrorDomain,
  970. code: RemoteConfigCustomSignalsError.limitExceeded.rawValue,
  971. userInfo: [
  972. NSLocalizedDescriptionKey:
  973. "Custom signals count exceeds the limit of \(customSignalsMaxCount).",
  974. ]
  975. )
  976. DispatchQueue.main.async {
  977. completionHandler(error)
  978. }
  979. }
  980. return
  981. }
  982. // Update only if there are changes.
  983. if newCustomSignals != self.settings.customSignals {
  984. self.settings.customSignals = newCustomSignals
  985. }
  986. // Log the keys of the updated custom signals using RCLog.debug
  987. RCLog.debug("I-RCN000078",
  988. "Keys of updated custom signals: \(newCustomSignals.keys.sorted())")
  989. DispatchQueue.main.async {
  990. completionHandler?(nil)
  991. }
  992. }
  993. }
  994. }
  995. // MARK: - Rollout Notification
  996. extension Notification.Name {
  997. static let rolloutsStateDidChange = Notification.Name(rawValue:
  998. "FIRRolloutsStateDidChangeNotification")
  999. }