RemoteConfigComponent.swift 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. // Copyright 2024 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 Foundation
  15. import FirebaseCore
  16. import FirebaseCoreExtension
  17. import FirebaseRemoteConfigInterop
  18. // TODO(ncooke3): Once Obj-C tests are ported, all `public` access modifers can be removed.
  19. // TODO(ncooke3): Move to another pod.
  20. @objc(AnalyticsInterop) public protocol FIRAnalyticsInterop {
  21. func getUserProperties(callback: @escaping ([String: Any]) -> Void)
  22. func logEvent(withOrigin origin: String,
  23. name: String,
  24. parameters: [String: Any])
  25. }
  26. /// Provides and creates instances of Remote Config based on the namespace provided. Used in the
  27. /// interop registration process to keep track of RC instances for each `FIRApp` instance.
  28. @objc(FIRRemoteConfigProvider) public protocol RemoteConfigProvider {
  29. /// Cached instances of Remote Config objects.
  30. var instances: [String: RemoteConfig] { get set }
  31. /// Default method for retrieving a Remote Config instance, or creating one if it doesn't exist.
  32. func remoteConfig(forNamespace remoteConfigNamespace: String) -> RemoteConfig?
  33. }
  34. /// A concrete implementation for FIRRemoteConfigInterop to create Remote Config instances and
  35. /// register with Core's component system.
  36. @objc(FIRRemoteConfigComponent) public final class RemoteConfigComponent: NSObject {
  37. // Because Component now need to register two protocols (provider and interop), we need a way to
  38. // return the same component instance for both registered protocol, this singleton pattern allow
  39. // us
  40. // to return the same component object for both registration callback.
  41. static var componentInstances: [String: RemoteConfigComponent] = [:]
  42. static let componentInstancesLock = NSLock()
  43. /// The FIRApp that instances will be set up with.
  44. @objc public weak var app: FirebaseApp?
  45. /// Cached instances of Remote Config objects.
  46. public var instances: [String: RemoteConfig]
  47. let instancesLock = NSLock()
  48. /// Default initializer.
  49. @objc public init(app: FirebaseApp) {
  50. self.app = app
  51. instances = [:]
  52. super.init()
  53. }
  54. }
  55. extension RemoteConfigComponent: RemoteConfigProvider {
  56. /// Default method for retrieving a Remote Config instance, or creating one if it doesn't exist.
  57. @objc public func remoteConfig(forNamespace remoteConfigNamespace: String) -> RemoteConfig? {
  58. guard let app else {
  59. return nil
  60. }
  61. // Validate the required information is available.
  62. let errorPropertyName = if app.options.googleAppID.isEmpty {
  63. "googleAppID"
  64. } else if app.options.gcmSenderID.isEmpty {
  65. "GCMSenderID"
  66. } else if (app.options.projectID ?? "").isEmpty {
  67. "projectID"
  68. } else { nil as String? }
  69. if let errorPropertyName {
  70. // TODO(ncooke): The ObjC unit tests depend on this throwing an exception
  71. // (which can be caught in ObjC but not as easily in Swift). Once unit
  72. // tests are ported, move to fatalError and document behavior change in
  73. // release notes.
  74. // fatalError("Firebase Remote Config is missing the required \(errorPropertyName) property
  75. // from the " +
  76. // "configured FirebaseApp and will not be able to function properly. " +
  77. // "Please fix this issue to ensure that Firebase is correctly configured.")
  78. NSException.raise(
  79. NSExceptionName("com.firebase.config"),
  80. format: "Firebase Remote Config is missing the required %@ property from the " +
  81. "configured FirebaseApp and will not be able to function properly. " +
  82. "Please fix this issue to ensure that Firebase is correctly configured.",
  83. arguments: getVaList([errorPropertyName])
  84. )
  85. }
  86. instancesLock.lock()
  87. defer { instancesLock.unlock() }
  88. guard let cachedInstance = instances[remoteConfigNamespace] else {
  89. let analytics = app.isDefaultApp ? app.container.instance(for: FIRAnalyticsInterop.self) : nil
  90. let newInstance = RemoteConfig(
  91. appName: app.name,
  92. options: app.options,
  93. namespace: remoteConfigNamespace,
  94. dbManager: ConfigDBManager.sharedInstance,
  95. configContent: ConfigContent.sharedInstance,
  96. analytics: analytics as? FIRAnalyticsInterop
  97. )
  98. instances[remoteConfigNamespace] = newInstance
  99. return newInstance
  100. }
  101. return cachedInstance
  102. }
  103. }
  104. extension RemoteConfigComponent: Library {
  105. public static func componentsToRegister() -> [Component] {
  106. let rcProvider = Component(
  107. RemoteConfigProvider.self,
  108. instantiationTiming: .alwaysEager
  109. ) { container, isCacheable in
  110. // Cache the component so instances of Remote Config are cached.
  111. isCacheable.pointee = true
  112. return getComponent(forApp: container.app)
  113. }
  114. // Unlike provider needs to setup a hard dependency on remote config, interop allows an optional
  115. // dependency on RC
  116. let rcInterop = Component(
  117. RemoteConfigInterop.self,
  118. instantiationTiming: .alwaysEager
  119. ) { container, isCacheable in
  120. // Cache the component so instances of Remote Config are cached.
  121. isCacheable.pointee = true
  122. return getComponent(forApp: container.app)
  123. }
  124. return [rcProvider, rcInterop]
  125. }
  126. private static func getComponent(forApp app: FirebaseApp?) -> RemoteConfigComponent? {
  127. componentInstancesLock.withLock {
  128. guard let app else {
  129. return nil
  130. }
  131. if componentInstances[app.name] == nil {
  132. componentInstances[app.name] = .init(app: app)
  133. }
  134. return componentInstances[app.name]
  135. }
  136. }
  137. /// Clear all the component instances from the singleton which created previously, this is for
  138. /// testing only
  139. @objc public static func clearAllComponentInstances() {
  140. componentInstancesLock.withLock {
  141. componentInstances.removeAll()
  142. }
  143. }
  144. }
  145. extension RemoteConfigComponent: RemoteConfigInterop {
  146. public func registerRolloutsStateSubscriber(_ subscriber: any FirebaseRemoteConfigInterop
  147. .RolloutsStateSubscriber,
  148. for namespace: String) {
  149. if let instance = remoteConfig(forNamespace: namespace) {
  150. instance.addRemoteConfigInteropSubscriber(subscriber)
  151. }
  152. }
  153. }