RCNConfigFetch.swift 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. import Foundation
  2. import FirebaseCore
  3. import FirebaseInstallations // Required for FIS interaction
  4. // TODO: Import FIRAnalyticsInterop if it's defined in a separate module
  5. // --- Placeholder Types ---
  6. typealias RCNConfigDBManager = AnyObject // Keep placeholder
  7. typealias RCNConfigExperiment = AnyObject // Keep placeholder
  8. typealias FIRAnalyticsInterop = AnyObject // Keep placeholder
  9. typealias RCNDevice = AnyObject // Keep placeholder
  10. // Assume RCNConfigContent, RCNConfigSettingsInternal, RemoteConfigFetchStatus, RemoteConfigError, RemoteConfigUpdate, etc. are defined
  11. // --- Helper Types ---
  12. // Define Completion handler type definition used internally and by Realtime
  13. typealias RCNConfigFetchCompletion = (RemoteConfigFetchStatus, RemoteConfigUpdate?, Error?) -> Void
  14. // Define Key constant used in error dictionary
  15. let RemoteConfigThrottledEndTimeInSecondsKey = "error_throttled_end_time_seconds"
  16. // --- Constants ---
  17. // TODO: Move to central constants file
  18. private enum FetchConstants {
  19. #if RCN_STAGING_SERVER
  20. static let serverURLDomain = "https://staging-firebaseremoteconfig.sandbox.googleapis.com"
  21. #else
  22. static let serverURLDomain = "https://firebaseremoteconfig.googleapis.com"
  23. #endif
  24. static let serverURLVersion = "/v1"
  25. static let serverURLProjects = "/projects/"
  26. static let serverURLNamespaces = "/namespaces/"
  27. static let serverURLQuery = ":fetch?"
  28. static let serverURLKey = "key="
  29. static let httpMethodPost = "POST"
  30. static let contentTypeHeaderName = "Content-Type"
  31. static let contentEncodingHeaderName = "Content-Encoding"
  32. static let acceptEncodingHeaderName = "Accept-Encoding"
  33. static let eTagHeaderName = "etag"
  34. static let ifNoneMatchETagHeaderName = "if-none-match"
  35. static let installationsAuthTokenHeaderName = "x-goog-firebase-installations-auth"
  36. static let iOSBundleIdentifierHeaderName = "X-Ios-Bundle-Identifier"
  37. static let fetchTypeHeaderName = "X-Firebase-RC-Fetch-Type"
  38. static let baseFetchType = "BASE"
  39. static let realtimeFetchType = "REALTIME"
  40. static let contentTypeValueJSON = "application/json"
  41. static let contentEncodingGzip = "gzip"
  42. static let httpStatusOK = 200
  43. static let httpStatusNotModified = 304 // Added for clarity, though not an error
  44. static let httpStatusTooManyRequests = 429
  45. static let httpStatusInternalError = 500
  46. static let httpStatusServiceUnavailable = 503
  47. static let httpStatusGatewayTimeout = 504 // Not explicitly handled in ObjC retry logic? Added for completeness
  48. // Response Keys (assuming defined elsewhere, e.g., RCNConfigConstants)
  49. static let responseKeyError = "error"
  50. static let responseKeyErrorCode = "code"
  51. static let responseKeyErrorStatus = "status"
  52. static let responseKeyErrorMessage = "message"
  53. static let responseKeyExperimentDescriptions = "experimentDescriptions"
  54. static let responseKeyTemplateVersion = "templateVersionNumber" // Match UserDefault key?
  55. static let responseKeyState = "state"
  56. static let responseKeyEntries = "entries"
  57. static let responseKeyPersonalizationMetadata = "personalizationMetadata"
  58. static let responseKeyRolloutMetadata = "rolloutMetadata"
  59. // State Values
  60. static let responseKeyStateNoChange = "NO_CHANGE"
  61. static let responseKeyStateEmptyConfig = "EMPTY_CONFIG"
  62. static let responseKeyStateNoTemplate = "NO_TEMPLATE"
  63. static let responseKeyStateUpdate = "UPDATE_CONFIG"
  64. }
  65. /// Handles the fetching of Remote Config data from the backend server.
  66. class RCNConfigFetch {
  67. // Dependencies
  68. private let content: RCNConfigContent
  69. // DBManager is placeholder only used via Settings placeholder calls for now
  70. // private let dbManager: RCNConfigDBManager
  71. private let settings: RCNConfigSettingsInternal
  72. private let analytics: FIRAnalyticsInterop? // Placeholder
  73. private let experiment: RCNConfigExperiment? // Placeholder
  74. private let lockQueue: DispatchQueue // Serial queue for synchronization
  75. private let firebaseNamespace: String
  76. private let options: FirebaseOptions
  77. // Internal State
  78. // Making fetchSession internal(set) allows tests to replace it
  79. internal(set) var fetchSession: URLSession
  80. // Publicly readable property for Realtime
  81. var templateVersionNumber: String {
  82. // Read directly from settings (which reads from UserDefaults)
  83. return settings.lastFetchedTemplateVersion ?? "0"
  84. }
  85. // MARK: - Initialization
  86. init(content: RCNConfigContent,
  87. dbManager: RCNConfigDBManager, // Placeholder accepted
  88. settings: RCNConfigSettingsInternal,
  89. analytics: FIRAnalyticsInterop?, // Placeholder accepted
  90. experiment: RCNConfigExperiment?, // Placeholder accepted
  91. queue: DispatchQueue,
  92. firebaseNamespace: String,
  93. options: FirebaseOptions) {
  94. self.content = content
  95. // self.dbManager = dbManager // Not directly used by Fetch itself
  96. self.settings = settings
  97. self.analytics = analytics
  98. self.experiment = experiment
  99. self.lockQueue = queue
  100. self.firebaseNamespace = firebaseNamespace
  101. self.options = options
  102. self.fetchSession = RCNConfigFetch.newFetchSession(settings: settings) // Initial session
  103. // templateVersionNumber read dynamically from settings
  104. }
  105. deinit {
  106. fetchSession.invalidateAndCancel()
  107. }
  108. // MARK: - Session Management
  109. private static func newFetchSession(settings: RCNConfigSettingsInternal) -> URLSession {
  110. let config = URLSessionConfiguration.default
  111. config.timeoutIntervalForRequest = settings.fetchTimeout
  112. config.timeoutIntervalForResource = settings.fetchTimeout
  113. return URLSession(configuration: config)
  114. }
  115. /// Recreates the network session, typically after settings change.
  116. @objc func recreateNetworkSession() { // Needs @objc for selector call from RemoteConfig
  117. let oldSession = fetchSession
  118. lockQueue.async { // Ensure thread safety if called concurrently
  119. self.fetchSession = RCNConfigFetch.newFetchSession(settings: self.settings)
  120. oldSession.invalidateAndCancel() // Invalidate after new one is ready
  121. }
  122. }
  123. // MARK: - Public Fetch Methods
  124. /// Fetches config data, respecting expiration duration and throttling.
  125. /// Needs @objc for selector call from RemoteConfig
  126. @objc func fetchConfig(withExpirationDuration expirationDuration: TimeInterval,
  127. completionHandler: ((RemoteConfigFetchStatus, Error?) -> Void)?) {
  128. // Note: device context check requires RCNDevice translation
  129. // let hasDeviceContextChanged = RCNDevice.hasDeviceContextChanged(settings.deviceContext, options.googleAppID ?? "")
  130. let hasDeviceContextChanged = false // Placeholder
  131. lockQueue.async { [weak self] in
  132. guard let self = self else { return }
  133. // 1. Check Expiration/Interval
  134. if !self.settings.hasMinimumFetchIntervalElapsed(minimumInterval: expirationDuration), !hasDeviceContextChanged {
  135. // TODO: Log debug: FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000051", ...)
  136. self.reportCompletion(status: .success, update: nil, error: nil,
  137. baseHandler: completionHandler, realtimeHandler: nil)
  138. return
  139. }
  140. // 2. Check Throttling
  141. if self.settings.shouldThrottle(), !hasDeviceContextChanged {
  142. self.settings.lastFetchStatus = .throttled // Update status
  143. self.settings.lastFetchError = .throttled
  144. let throttledEndTime = self.settings.exponentialBackoffThrottleEndTime
  145. let error = NSError(domain: RemoteConfigConstants.errorDomain,
  146. code: RemoteConfigError.throttled.rawValue,
  147. userInfo: [RemoteConfigThrottledEndTimeInSecondsKey: throttledEndTime]) // Use actual key constant
  148. self.reportCompletion(status: .throttled, update: nil, error: error,
  149. baseHandler: completionHandler, realtimeHandler: nil)
  150. return
  151. }
  152. // 3. Check In Progress
  153. // Note: isFetchInProgress access needs external sync (lockQueue handles it here)
  154. if self.settings.isFetchInProgress {
  155. // TODO: Log appropriately based on whether previous data exists
  156. // FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000052", ...) or
  157. // FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000053", ...)
  158. // Report previous status or failure
  159. let status = self.settings.lastFetchTimeInterval > 0 ? self.settings.lastFetchStatus : .failure
  160. self.reportCompletion(status: status, update: nil, error: nil, // Report no error for "in progress"
  161. baseHandler: completionHandler, realtimeHandler: nil)
  162. return
  163. }
  164. // 4. Proceed with fetch
  165. self.settings.isFetchInProgress = true
  166. let fetchTypeHeader = "\(FetchConstants.baseFetchType)/1" // Simple count for now
  167. self.refreshInstallationsToken(fetchTypeHeader: fetchTypeHeader,
  168. baseHandler: completionHandler,
  169. realtimeHandler: nil)
  170. }
  171. }
  172. /// Fetches config immediately for Realtime, respecting throttling but not expiration.
  173. func realtimeFetchConfig(fetchAttemptNumber: Int,
  174. completionHandler: @escaping RCNConfigFetchCompletion) { // Note: Escaping closure
  175. // Note: device context check requires RCNDevice translation
  176. // let hasDeviceContextChanged = RCNDevice.hasDeviceContextChanged(settings.deviceContext, options.googleAppID ?? "")
  177. let hasDeviceContextChanged = false // Placeholder
  178. lockQueue.async { [weak self] in
  179. guard let self = self else { return }
  180. // 1. Check Throttling
  181. if self.settings.shouldThrottle(), !hasDeviceContextChanged {
  182. self.settings.lastFetchStatus = .throttled
  183. self.settings.lastFetchError = .throttled
  184. let throttledEndTime = self.settings.exponentialBackoffThrottleEndTime
  185. let error = NSError(domain: RemoteConfigConstants.errorDomain,
  186. code: RemoteConfigError.throttled.rawValue,
  187. userInfo: [RemoteConfigThrottledEndTimeInSecondsKey: throttledEndTime])
  188. self.reportCompletion(status: .throttled, update: nil, error: error,
  189. baseHandler: nil, realtimeHandler: completionHandler)
  190. return
  191. }
  192. // 2. Proceed with fetch (no in-progress check for Realtime?)
  193. // ObjC logic didn't explicitly check isFetchInProgress here, assuming Realtime manages its own calls.
  194. // Let's keep isFetchInProgress set for consistency in FIS calls.
  195. self.settings.isFetchInProgress = true
  196. let fetchTypeHeader = "\(FetchConstants.realtimeFetchType)/\(fetchAttemptNumber)"
  197. self.refreshInstallationsToken(fetchTypeHeader: fetchTypeHeader,
  198. baseHandler: nil,
  199. realtimeHandler: completionHandler)
  200. }
  201. }
  202. // MARK: - Private Fetch Flow
  203. private func getAppNameFromNamespace() -> String {
  204. return firebaseNamespace.components(separatedBy: ":").last ?? ""
  205. }
  206. private func refreshInstallationsToken(fetchTypeHeader: String,
  207. baseHandler: ((RemoteConfigFetchStatus, Error?) -> Void)?,
  208. realtimeHandler: RCNConfigFetchCompletion?) {
  209. guard let gcmSenderID = options.gcmSenderID, !gcmSenderID.isEmpty else {
  210. let errorDesc = "Failed to get GCMSenderID"
  211. // TODO: Log error: FIRLogError(...)
  212. self.settings.isFetchInProgress = false // Reset flag
  213. let error = NSError(domain: RemoteConfigConstants.errorDomain, code: RemoteConfigError.internalError.rawValue, userInfo: [NSLocalizedDescriptionKey: errorDesc])
  214. self.reportCompletion(status: .failure, update: nil, error: error, baseHandler: baseHandler, realtimeHandler: realtimeHandler)
  215. return
  216. }
  217. let appName = getAppNameFromNamespace()
  218. guard let app = FirebaseApp.app(name: appName), let installations = Installations.installations(app: app) else {
  219. let errorDesc = "Failed to get FirebaseApp or Installations instance for app: \(appName)"
  220. // TODO: Log error: FIRLogError(...)
  221. self.settings.isFetchInProgress = false // Reset flag
  222. let error = NSError(domain: RemoteConfigConstants.errorDomain, code: RemoteConfigError.internalError.rawValue, userInfo: [NSLocalizedDescriptionKey: errorDesc])
  223. self.reportCompletion(status: .failure, update: nil, error: error, baseHandler: baseHandler, realtimeHandler: realtimeHandler)
  224. return
  225. }
  226. // TODO: Log debug: FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000039", ...)
  227. installations.authToken { [weak self] tokenResult, error in
  228. guard let self = self else { return }
  229. guard let token = tokenResult?.authToken, error == nil else {
  230. let errorDesc = "Failed to get installations token. Error: \(error?.localizedDescription ?? "Unknown")"
  231. // TODO: Log error: FIRLogError(...)
  232. self.lockQueue.async { // Ensure state update is on queue
  233. self.settings.isFetchInProgress = false // Reset flag
  234. let wrappedError = NSError(domain: RemoteConfigConstants.errorDomain,
  235. code: RemoteConfigError.internalError.rawValue,
  236. userInfo: [NSLocalizedDescriptionKey: errorDesc, NSUnderlyingErrorKey: error as Any])
  237. self.reportCompletion(status: .failure, update: nil, error: wrappedError, baseHandler: baseHandler, realtimeHandler: realtimeHandler)
  238. }
  239. return
  240. }
  241. // Get Installation ID
  242. installations.installationID { [weak self] identifier, error in
  243. guard let self = self else { return }
  244. // Dispatch back to queue for settings update & next step
  245. self.lockQueue.async {
  246. guard let identifier = identifier, error == nil else {
  247. let errorDesc = "Error getting Installation ID: \(error?.localizedDescription ?? "Unknown")"
  248. // TODO: Log error: FIRLogError(...)
  249. self.settings.isFetchInProgress = false // Reset flag
  250. let wrappedError = NSError(domain: RemoteConfigConstants.errorDomain,
  251. code: RemoteConfigError.internalError.rawValue,
  252. userInfo: [NSLocalizedDescriptionKey: errorDesc, NSUnderlyingErrorKey: error as Any])
  253. self.reportCompletion(status: .failure, update: nil, error: wrappedError, baseHandler: baseHandler, realtimeHandler: realtimeHandler)
  254. return
  255. }
  256. // TODO: Log info: FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000022", ...)
  257. self.settings.configInstallationsToken = token
  258. self.settings.configInstallationsIdentifier = identifier
  259. // Proceed to get user properties and make fetch call
  260. self.doFetchCall(fetchTypeHeader: fetchTypeHeader, baseHandler: baseHandler, realtimeHandler: realtimeHandler)
  261. }
  262. }
  263. }
  264. }
  265. private func doFetchCall(fetchTypeHeader: String,
  266. baseHandler: ((RemoteConfigFetchStatus, Error?) -> Void)?,
  267. realtimeHandler: RCNConfigFetchCompletion?) {
  268. // Get Analytics User Properties (Placeholder interaction)
  269. getAnalyticsUserProperties { [weak self] userProperties in
  270. guard let self = self else { return }
  271. // Ensure next step is on the queue
  272. self.lockQueue.async {
  273. self.performFetch(userProperties: userProperties,
  274. fetchTypeHeader: fetchTypeHeader,
  275. baseHandler: baseHandler,
  276. realtimeHandler: realtimeHandler)
  277. }
  278. }
  279. }
  280. // Placeholder for Analytics interaction
  281. private func getAnalyticsUserProperties(completionHandler: @escaping ([String: Any]?) -> Void) {
  282. // TODO: Log Debug: FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000060", ...)
  283. if let analytics = self.analytics {
  284. // analytics.getUserProperties(callback: completionHandler) // Requires translated interop
  285. // Placeholder: Simulate async call returning empty properties
  286. DispatchQueue.global().asyncAfter(deadline: .now() + 0.01) { // Simulate delay
  287. completionHandler([:])
  288. }
  289. } else {
  290. completionHandler([:]) // No analytics, return empty immediately
  291. }
  292. }
  293. private func performFetch(userProperties: [String: Any]?,
  294. fetchTypeHeader: String,
  295. baseHandler: ((RemoteConfigFetchStatus, Error?) -> Void)?,
  296. realtimeHandler: RCNConfigFetchCompletion?) {
  297. // TODO: Log Debug: FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000061", ...)
  298. guard let postBodyString = settings.nextRequestWithUserProperties(userProperties) else {
  299. let errorDesc = "Failed to construct fetch request body."
  300. self.settings.isFetchInProgress = false // Reset flag
  301. let error = NSError(domain: RemoteConfigConstants.errorDomain, code: RemoteConfigError.internalError.rawValue, userInfo: [NSLocalizedDescriptionKey: errorDesc])
  302. self.reportCompletion(status: .failure, update: nil, error: error, baseHandler: baseHandler, realtimeHandler: realtimeHandler)
  303. return
  304. }
  305. guard let content = postBodyString.data(using: .utf8) else {
  306. let errorDesc = "Failed to encode fetch request body to UTF8."
  307. self.settings.isFetchInProgress = false // Reset flag
  308. let error = NSError(domain: RemoteConfigConstants.errorDomain, code: RemoteConfigError.internalError.rawValue, userInfo: [NSLocalizedDescriptionKey: errorDesc])
  309. self.reportCompletion(status: .failure, update: nil, error: error, baseHandler: baseHandler, realtimeHandler: realtimeHandler)
  310. return
  311. }
  312. // Compress data
  313. let compressedContent: Data?
  314. do {
  315. compressedContent = try (content as NSData).gzipped(withCompressionLevel: .defaultCompression) // Requires GULNSData+zlib logic port or library
  316. // Placeholder for gzipped:
  317. // compressedContent = content // Remove this line if gzip available
  318. } catch {
  319. let errorDesc = "Failed to compress the config request: \(error)"
  320. // TODO: Log warning: FIRLogWarning(...)
  321. self.settings.isFetchInProgress = false // Reset flag
  322. let wrappedError = NSError(domain: RemoteConfigConstants.errorDomain,
  323. code: RemoteConfigError.internalError.rawValue,
  324. userInfo: [NSLocalizedDescriptionKey: errorDesc, NSUnderlyingErrorKey: error])
  325. self.reportCompletion(status: .failure, update: nil, error: wrappedError, baseHandler: baseHandler, realtimeHandler: realtimeHandler)
  326. return
  327. }
  328. guard let finalContent = compressedContent else {
  329. let errorDesc = "Compressed content is nil."
  330. self.settings.isFetchInProgress = false // Reset flag
  331. let error = NSError(domain: RemoteConfigConstants.errorDomain, code: RemoteConfigError.internalError.rawValue, userInfo: [NSLocalizedDescriptionKey: errorDesc])
  332. self.reportCompletion(status: .failure, update: nil, error: error, baseHandler: baseHandler, realtimeHandler: realtimeHandler)
  333. return
  334. }
  335. // TODO: Log Debug: FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000040", ...)
  336. let task = createURLSessionDataTask(content: finalContent, fetchTypeHeader: fetchTypeHeader) {
  337. [weak self] data, response, error in
  338. // This completion handler runs on the URLSession's delegate queue (main by default)
  339. // Ensure subsequent processing happens on our lockQueue
  340. guard let self = self else { return }
  341. // TODO: Log Debug: FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000050", ...)
  342. self.lockQueue.async { // Dispatch processing to the lock queue
  343. self.settings.isFetchInProgress = false // Reset flag regardless of outcome
  344. self.handleFetchResponse(data: data, response: response, error: error,
  345. baseHandler: baseHandler, realtimeHandler: realtimeHandler)
  346. }
  347. }
  348. task.resume()
  349. }
  350. private func createURLSessionDataTask(content: Data,
  351. fetchTypeHeader: String,
  352. completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
  353. guard let url = constructServerURL() else {
  354. // Should not happen if options are valid
  355. fatalError("Could not construct server URL") // Or handle more gracefully
  356. }
  357. // TODO: Log Debug: FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000046", ...)
  358. var request = URLRequest(url: url,
  359. cachePolicy: .reloadIgnoringLocalCacheData,
  360. timeoutInterval: fetchSession.configuration.timeoutIntervalForRequest) // Use session timeout
  361. request.httpMethod = FetchConstants.httpMethodPost
  362. request.setValue(FetchConstants.contentTypeValueJSON, forHTTPHeaderField: FetchConstants.contentTypeHeaderName)
  363. request.setValue(settings.configInstallationsToken, forHTTPHeaderField: FetchConstants.installationsAuthTokenHeaderName)
  364. request.setValue(settings.bundleIdentifier, forHTTPHeaderField: FetchConstants.iOSBundleIdentifierHeaderName) // Use settings bundle ID
  365. request.setValue(FetchConstants.contentEncodingGzip, forHTTPHeaderField: FetchConstants.contentEncodingHeaderName)
  366. request.setValue(FetchConstants.contentEncodingGzip, forHTTPHeaderField: FetchConstants.acceptEncodingHeaderName)
  367. request.setValue(fetchTypeHeader, forHTTPHeaderField: FetchConstants.fetchTypeHeaderName)
  368. if let etag = settings.lastETag {
  369. request.setValue(etag, forHTTPHeaderField: FetchConstants.ifNoneMatchETagHeaderName)
  370. }
  371. request.httpBody = content
  372. return fetchSession.dataTask(with: request, completionHandler: completionHandler)
  373. }
  374. // MARK: - Response Handling (on lockQueue)
  375. private func handleFetchResponse(data: Data?, response: URLResponse?, error: Error?,
  376. baseHandler: ((RemoteConfigFetchStatus, Error?) -> Void)?,
  377. realtimeHandler: RCNConfigFetchCompletion?) {
  378. let httpResponse = response as? HTTPURLResponse
  379. let statusCode = httpResponse?.statusCode ?? -1 // Default to invalid status code
  380. // 1. Handle Client-Side or HTTP Errors
  381. if let error = error { // Client-side error (network, timeout, etc.)
  382. handleFetchError(error: error, statusCode: statusCode, baseHandler: baseHandler, realtimeHandler: realtimeHandler)
  383. return
  384. }
  385. if statusCode != FetchConstants.httpStatusOK {
  386. // Check for 304 Not Modified *before* treating other non-200 as errors
  387. if statusCode == FetchConstants.httpStatusNotModified {
  388. // TODO: Log info - Not Modified
  389. settings.updateMetadataWithFetchSuccessStatus(true, templateVersion: settings.lastFetchedTemplateVersion) // Keep old version
  390. let update = content.getConfigUpdate(forNamespace: firebaseNamespace) // Calculate diff anyway?
  391. self.reportCompletion(status: .success, update: update, error: nil, baseHandler: baseHandler, realtimeHandler: realtimeHandler)
  392. return
  393. }
  394. // Handle other non-200, non-304 statuses as errors
  395. handleFetchError(error: nil, statusCode: statusCode, baseHandler: baseHandler, realtimeHandler: realtimeHandler)
  396. return
  397. }
  398. // 2. Handle Successful Fetch (Status OK - 200)
  399. guard let responseData = data else {
  400. // TODO: Log info - No data in successful response
  401. let update = content.getConfigUpdate(forNamespace: firebaseNamespace) // Still calculate diff
  402. self.reportCompletion(status: .success, update: update, error: nil, baseHandler: baseHandler, realtimeHandler: realtimeHandler)
  403. return
  404. }
  405. // 3. Parse JSON Response
  406. let parsedResponse: [String: Any]?
  407. do {
  408. parsedResponse = try JSONSerialization.jsonObject(with: responseData, options: .mutableContainers) as? [String: Any]
  409. } catch let parseError {
  410. // TODO: Log error - JSON parsing failure
  411. let wrappedError = NSError(domain: RemoteConfigConstants.errorDomain, code: RemoteConfigError.internalError.rawValue,
  412. userInfo: [NSLocalizedDescriptionKey: "Failed to parse fetch response JSON.", NSUnderlyingErrorKey: parseError])
  413. self.reportCompletion(status: .failure, update: nil, error: wrappedError, baseHandler: baseHandler, realtimeHandler: realtimeHandler)
  414. return
  415. }
  416. // 4. Check for Server-Side Error in JSON Payload
  417. if let responseDict = parsedResponse,
  418. let serverError = responseDict[FetchConstants.responseKeyError] as? [String: Any] {
  419. let errorDesc = formatServerError(serverError)
  420. // TODO: Log error - Server returned error
  421. let wrappedError = NSError(domain: RemoteConfigConstants.errorDomain, code: RemoteConfigError.internalError.rawValue, userInfo: [NSLocalizedDescriptionKey: errorDesc])
  422. self.reportCompletion(status: .failure, update: nil, error: wrappedError, baseHandler: baseHandler, realtimeHandler: realtimeHandler)
  423. return
  424. }
  425. // 5. Process Successful Fetch Data
  426. if let fetchedData = parsedResponse {
  427. // Update content (triggers DB writes via selectors for now)
  428. content.updateConfigContentWithResponse(fetchedData, forNamespace: firebaseNamespace)
  429. // Update experiments (Placeholder interaction)
  430. if let experimentDescriptions = fetchedData[FetchConstants.responseKeyExperimentDescriptions] {
  431. // TODO: Ensure experimentDescriptions is correct type for experiment object
  432. experiment?.perform(#selector(RCNConfigExperiment.updateExperiments(response:)), with: experimentDescriptions)
  433. }
  434. // Update ETag if changed
  435. if let latestETag = httpResponse?.allHeaderFields[FetchConstants.eTagHeaderName] as? String {
  436. if settings.lastETag != latestETag {
  437. settings.setLastETag(latestETag) // Updates UserDefaults
  438. }
  439. } else {
  440. // No ETag received? Clear local ETag? ObjC didn't explicitly clear.
  441. // settings.setLastETag(nil)
  442. }
  443. // Update settings metadata (DB interaction via selector)
  444. let newVersion = getTemplateVersionNumber(from: fetchedData)
  445. settings.updateMetadataWithFetchSuccessStatus(true, templateVersion: newVersion)
  446. } else {
  447. // TODO: Log Debug - Empty response?
  448. // Still treat as success, but update metadata? ObjC didn't explicitly handle empty dict case here.
  449. settings.updateMetadataWithFetchSuccessStatus(true, templateVersion: settings.lastFetchedTemplateVersion) // Keep old version?
  450. }
  451. // 6. Report Success
  452. let update = content.getConfigUpdate(forNamespace: firebaseNamespace)
  453. self.reportCompletion(status: .success, update: update, error: nil, baseHandler: baseHandler, realtimeHandler: realtimeHandler)
  454. }
  455. private func handleFetchError(error: Error?, statusCode: Int,
  456. baseHandler: ((RemoteConfigFetchStatus, Error?) -> Void)?,
  457. realtimeHandler: RCNConfigFetchCompletion?) {
  458. // Update metadata (DB interaction via selector)
  459. settings.updateMetadataWithFetchSuccessStatus(false, templateVersion: nil)
  460. var reportedError = error
  461. var reportedStatus = RemoteConfigFetchStatus.failure
  462. var errorDomain = error?.domain ?? RemoteConfigConstants.errorDomain
  463. var errorCode = error?.code ?? RemoteConfigError.internalError.rawValue
  464. // Check for retryable HTTP status codes and update throttling/status/error
  465. let retryableStatusCodes = [
  466. FetchConstants.httpStatusTooManyRequests,
  467. FetchConstants.httpStatusInternalError, // 500
  468. FetchConstants.httpStatusServiceUnavailable // 503
  469. // Add 504? ObjC didn't include it in backoff trigger check.
  470. ]
  471. if retryableStatusCodes.contains(statusCode) {
  472. settings.updateExponentialBackoffTime() // Update backoff window
  473. if settings.shouldThrottle() { // Check if *now* we are throttled
  474. reportedStatus = .throttled
  475. errorCode = RemoteConfigError.throttled.rawValue
  476. errorDomain = RemoteConfigConstants.errorDomain // Ensure RC domain
  477. let throttledEndTime = settings.exponentialBackoffThrottleEndTime
  478. reportedError = NSError(domain: errorDomain, code: errorCode,
  479. userInfo: [NSLocalizedDescriptionKey: "Fetch throttled. Backoff interval has not passed.",
  480. RemoteConfigThrottledEndTimeInSecondsKey: throttledEndTime])
  481. }
  482. }
  483. // Ensure reported error is constructed if nil
  484. if reportedError == nil {
  485. // Handle 304 separately as it's not a true error in the same sense
  486. if statusCode == FetchConstants.httpStatusNotModified {
  487. // This path shouldn't be reached based on logic in handleFetchResponse.
  488. // Log a warning if we somehow get here.
  489. // TODO: Log warning
  490. reportedStatus = .success // Technically not an error, but no update happened.
  491. reportedError = nil // Clear any potential error if it was just 304.
  492. } else {
  493. let errorDesc = "Fetch failed with HTTP status code: \(statusCode)"
  494. // TODO: Log Error
  495. reportedError = NSError(domain: errorDomain, code: errorCode,
  496. userInfo: [NSLocalizedDescriptionKey: errorDesc])
  497. }
  498. }
  499. // Update settings status *after* potential throttling check
  500. settings.lastFetchStatus = reportedStatus
  501. settings.lastFetchError = RemoteConfigError(rawValue: errorCode) ?? .unknown
  502. self.reportCompletion(status: reportedStatus, update: nil, error: reportedError,
  503. baseHandler: baseHandler, realtimeHandler: realtimeHandler)
  504. }
  505. // MARK: - Helpers
  506. private func constructServerURL() -> URL? {
  507. guard let projectID = options.projectID, !projectID.isEmpty,
  508. let apiKey = options.apiKey, !apiKey.isEmpty else {
  509. // TODO: Log error - Missing projectID or apiKey
  510. return nil
  511. }
  512. // Extract namespace part from "namespace:appName"
  513. let namespacePart = firebaseNamespace.components(separatedBy: ":").first ?? firebaseNamespace
  514. let urlString = FetchConstants.serverURLDomain +
  515. FetchConstants.serverURLVersion +
  516. FetchConstants.serverURLProjects + projectID +
  517. FetchConstants.serverURLNamespaces + namespacePart +
  518. FetchConstants.serverURLQuery +
  519. FetchConstants.serverURLKey + apiKey
  520. return URL(string: urlString)
  521. }
  522. private func getTemplateVersionNumber(from fetchedConfig: [String: Any]?) -> String {
  523. return fetchedConfig?[FetchConstants.responseKeyTemplateVersion] as? String ?? "0"
  524. }
  525. private func formatServerError(_ errorDict: [String: Any]) -> String {
  526. var errStr = "Fetch Failure: Server returned error: "
  527. if let code = errorDict[FetchConstants.responseKeyErrorCode] { errStr += "Code: \(code). " }
  528. if let status = errorDict[FetchConstants.responseKeyErrorStatus] { errStr += "Status: \(status). " }
  529. if let message = errorDict[FetchConstants.responseKeyErrorMessage] { errStr += "Message: \(message)" }
  530. return errStr
  531. }
  532. /// Dispatches completion handlers to the main queue.
  533. private func reportCompletion(status: RemoteConfigFetchStatus,
  534. update: RemoteConfigUpdate?, // Included for realtime handler
  535. error: Error?,
  536. baseHandler: ((RemoteConfigFetchStatus, Error?) -> Void)?,
  537. realtimeHandler: RCNConfigFetchCompletion?) {
  538. DispatchQueue.main.async {
  539. baseHandler?(status, error)
  540. realtimeHandler?(status, update, error) // Pass update only to realtime handler
  541. }
  542. }
  543. // MARK: - Placeholder Selectors
  544. // Selectors needed for RemoteConfig interaction via perform(#selector(...))
  545. @objc private func fetchConfig(withExpirationDuration duration: TimeInterval, completionHandler handler: Any?) {}
  546. // Selectors for RCNConfigExperiment (placeholder)
  547. @objc private func updateExperiments(response: Any?) {}
  548. }
  549. // Define GULNSData+zlib methods or include a library
  550. extension NSData {
  551. @objc func gzipped(withCompressionLevel level: Int32 = -1) throws -> Data {
  552. // Placeholder - Requires porting or library
  553. print("Warning: gzipped compression not implemented.")
  554. return self as Data
  555. }
  556. }
  557. // Assume these types are defined elsewhere
  558. // @objc(FIRRemoteConfigFetchStatus) public enum RemoteConfigFetchStatus: Int { ... }
  559. // @objc(FIRRemoteConfigError) public enum RemoteConfigError: Int { ... }
  560. // @objc(FIRRemoteConfigUpdate) public class RemoteConfigUpdate: NSObject { ... }
  561. // class RCNConfigContent { ... }
  562. // class RCNConfigSettingsInternal { ... }
  563. // etc.