diff --git a/FirebaseABTesting/Sources/ABTExperimentPayload.m b/FirebaseABTesting/Sources/ABTExperimentPayload.m index 6bc2aaaa604..0033022a6cf 100644 --- a/FirebaseABTesting/Sources/ABTExperimentPayload.m +++ b/FirebaseABTesting/Sources/ABTExperimentPayload.m @@ -14,6 +14,8 @@ #import "FirebaseABTesting/Sources/Private/ABTExperimentPayload.h" +#import "FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRExperimentController.h" + static NSString *const kExperimentPayloadKeyExperimentID = @"experimentId"; static NSString *const kExperimentPayloadKeyVariantID = @"variantId"; diff --git a/FirebaseABTesting/Sources/Private/ABTExperimentPayload.h b/FirebaseABTesting/Sources/Private/ABTExperimentPayload.h index b2f2da05406..94a4af1fb37 100644 --- a/FirebaseABTesting/Sources/Private/ABTExperimentPayload.h +++ b/FirebaseABTesting/Sources/Private/ABTExperimentPayload.h @@ -14,16 +14,9 @@ #import -NS_ASSUME_NONNULL_BEGIN +#import "FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRExperimentController.h" -/// Policy for handling the case where there's an overflow of experiments for an installation -/// instance. -typedef NS_ENUM(int32_t, ABTExperimentPayloadExperimentOverflowPolicy) { - ABTExperimentPayloadExperimentOverflowPolicyUnrecognizedValue = 999, - ABTExperimentPayloadExperimentOverflowPolicyUnspecified = 0, - ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest = 1, - ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest = 2, -}; +NS_ASSUME_NONNULL_BEGIN @interface ABTExperimentLite : NSObject @property(nonatomic, readonly, copy) NSString *experimentId; diff --git a/FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRExperimentController.h b/FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRExperimentController.h index 3bd757d8823..7a74551284f 100644 --- a/FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRExperimentController.h +++ b/FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRExperimentController.h @@ -14,15 +14,21 @@ #import +#import "FIRLifecycleEvents.h" + @class ABTExperimentPayload; -// Forward declaration to avoid importing into the module header -typedef NS_ENUM(int32_t, ABTExperimentPayloadExperimentOverflowPolicy); +/// Policy for handling the case where there's an overflow of experiments for an installation +/// instance. +typedef NS_ENUM(int32_t, ABTExperimentPayloadExperimentOverflowPolicy) { + ABTExperimentPayloadExperimentOverflowPolicyUnrecognizedValue = 999, + ABTExperimentPayloadExperimentOverflowPolicyUnspecified = 0, + ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest = 1, + ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest = 2, +}; NS_ASSUME_NONNULL_BEGIN -@class FIRLifecycleEvents; - /// The default experiment overflow policy, that is to discard the experiment with the oldest start /// time when users start the experiment on the web console. extern const ABTExperimentPayloadExperimentOverflowPolicy FIRDefaultExperimentOverflowPolicy; diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index 2c825ce466b..ca38f0f8841 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -23,7 +23,6 @@ #import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h" #import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" -#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h" #import "FirebaseRemoteConfig/Sources/RCNConfigRealtime.h" #import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" #import "FirebaseRemoteConfig/Sources/RCNPersonalization.h" diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h deleted file mode 100644 index 79051faa87b..00000000000 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2019 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import - -@class FIRExperimentController; -@class RCNConfigDBManager; - -/// Handles experiment information update and persistence. -@interface RCNConfigExperiment : NSObject - -/// Designated initializer; -- (nonnull instancetype)initWithDBManager:(RCNConfigDBManager *_Nullable)DBManager - experimentController:(FIRExperimentController *_Nullable)controller - NS_DESIGNATED_INITIALIZER; - -/// Use `initWithDBManager:` instead. -- (nonnull instancetype)init NS_UNAVAILABLE; - -/// Update/Persist experiment information from config fetch response. -- (void)updateExperimentsWithResponse:(NSArray *> *_Nullable)response; - -/// Update experiments to Firebase Analytics when `activateWithCompletion:` happens. -- (void)updateExperimentsWithHandler:(nullable void (^)(NSError *_Nullable error))handler; -@end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m deleted file mode 100644 index a0a1b840e49..00000000000 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright 2019 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h" - -#import "FirebaseABTesting/Sources/Private/FirebaseABTestingInternal.h" -#import "FirebaseCore/Extension/FirebaseCoreInternal.h" -#import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h" - -#import "FirebaseRemoteConfig/FirebaseRemoteConfig-Swift.h" - -static NSString *const kExperimentMetadataKeyLastStartTime = @"last_experiment_start_time"; - -static NSString *const kServiceOrigin = @"frc"; -static NSString *const kMethodNameLatestStartTime = - @"latestExperimentStartTimestampBetweenTimestamp:andPayloads:"; - -@interface RCNConfigExperiment () -@property(nonatomic, strong) - NSMutableArray *experimentPayloads; ///< Experiment payloads. -@property(nonatomic, strong) - NSMutableDictionary *experimentMetadata; ///< Experiment metadata -@property(nonatomic, strong) - NSMutableArray *activeExperimentPayloads; ///< Activated experiment payloads. -@property(nonatomic, strong) RCNConfigDBManager *DBManager; ///< Database Manager. -@property(nonatomic, strong) FIRExperimentController *experimentController; -@property(nonatomic, strong) NSDateFormatter *experimentStartTimeDateFormatter; -@end - -@implementation RCNConfigExperiment -/// Designated initializer -- (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager - experimentController:(FIRExperimentController *)controller { - self = [super init]; - if (self) { - _experimentPayloads = [[NSMutableArray alloc] init]; - _experimentMetadata = [[NSMutableDictionary alloc] init]; - _activeExperimentPayloads = [[NSMutableArray alloc] init]; - _experimentStartTimeDateFormatter = [[NSDateFormatter alloc] init]; - [_experimentStartTimeDateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"]; - [_experimentStartTimeDateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; - // Locale needs to be hardcoded. See - // https://developer.apple.com/library/ios/#qa/qa1480/_index.html for more details. - [_experimentStartTimeDateFormatter - setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]; - [_experimentStartTimeDateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]]; - - _DBManager = DBManager; - _experimentController = controller; - [self loadExperimentFromTable]; - } - return self; -} - -typedef void (^RCNDBCompletion)(BOOL success, NSDictionary *result); -- (void)loadExperimentFromTable { - if (!_DBManager) { - return; - } - __weak RCNConfigExperiment *weakSelf = self; - RCNDBCompletion completionHandler = ^(BOOL success, NSDictionary *result) { - RCNConfigExperiment *strongSelf = weakSelf; - if (strongSelf == nil) { - return; - } - if (result[@RCNExperimentTableKeyPayload]) { - [strongSelf->_experimentPayloads removeAllObjects]; - for (NSData *experiment in result[@RCNExperimentTableKeyPayload]) { - NSError *error; - id experimentPayloadJSON = [NSJSONSerialization JSONObjectWithData:experiment - options:kNilOptions - error:&error]; - if (!experimentPayloadJSON || error) { - FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000031", - @"Experiment payload could not be parsed as JSON."); - } else { - [strongSelf->_experimentPayloads addObject:experiment]; - } - } - } - if (result[@RCNExperimentTableKeyMetadata]) { - strongSelf->_experimentMetadata = [result[@RCNExperimentTableKeyMetadata] mutableCopy]; - } - - /// Load activated experiments payload and metadata. - if (result[@RCNExperimentTableKeyActivePayload]) { - [strongSelf->_activeExperimentPayloads removeAllObjects]; - for (NSData *experiment in result[@RCNExperimentTableKeyActivePayload]) { - NSError *error; - id experimentPayloadJSON = [NSJSONSerialization JSONObjectWithData:experiment - options:kNilOptions - error:&error]; - if (!experimentPayloadJSON || error) { - FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000031", - @"Activated experiment payload could not be parsed as JSON."); - } else { - [strongSelf->_activeExperimentPayloads addObject:experiment]; - } - } - } - }; - [_DBManager loadExperimentWithCompletionHandler:completionHandler]; -} - -- (void)updateExperimentsWithResponse:(NSArray *> *)response { - // cache fetched experiment payloads. - [_experimentPayloads removeAllObjects]; - [_DBManager deleteExperimentTableForKey:@RCNExperimentTableKeyPayload]; - - for (NSDictionary *experiment in response) { - NSError *error; - NSData *JSONPayload = [NSJSONSerialization dataWithJSONObject:experiment - options:kNilOptions - error:&error]; - if (!JSONPayload || error) { - FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000030", - @"Invalid experiment payload to be serialized."); - } else { - [_experimentPayloads addObject:JSONPayload]; - [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload - value:JSONPayload - completionHandler:nil]; - } - } -} - -- (void)updateExperimentsWithHandler:(void (^)(NSError *_Nullable))handler { - FIRLifecycleEvents *lifecycleEvent = [[FIRLifecycleEvents alloc] init]; - - // Get the last experiment start time prior to the latest payload. - NSTimeInterval lastStartTime = - [_experimentMetadata[kExperimentMetadataKeyLastStartTime] doubleValue]; - - // Update the last experiment start time with the latest payload. - [self updateExperimentStartTime]; - [self.experimentController - updateExperimentsWithServiceOrigin:kServiceOrigin - events:lifecycleEvent - policy:ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest - lastStartTime:lastStartTime - payloads:_experimentPayloads - completionHandler:handler]; - - /// Update activated experiments payload and metadata in DB. - [self updateActiveExperimentsInDB]; -} - -- (void)updateExperimentStartTime { - NSTimeInterval existingLastStartTime = - [_experimentMetadata[kExperimentMetadataKeyLastStartTime] doubleValue]; - - NSTimeInterval latestStartTime = - [self latestStartTimeWithExistingLastStartTime:existingLastStartTime]; - - _experimentMetadata[kExperimentMetadataKeyLastStartTime] = @(latestStartTime); - - if (![NSJSONSerialization isValidJSONObject:_experimentMetadata]) { - FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000028", - @"Invalid fetched experiment metadata to be serialized."); - return; - } - NSError *error; - NSData *serializedExperimentMetadata = - [NSJSONSerialization dataWithJSONObject:_experimentMetadata - options:NSJSONWritingPrettyPrinted - error:&error]; - [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyMetadata - value:serializedExperimentMetadata - completionHandler:nil]; -} - -- (void)updateActiveExperimentsInDB { - /// Put current fetched experiment payloads into activated experiment DB. - [_activeExperimentPayloads removeAllObjects]; - [_DBManager deleteExperimentTableForKey:@RCNExperimentTableKeyActivePayload]; - for (NSData *experiment in _experimentPayloads) { - [_activeExperimentPayloads addObject:experiment]; - [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActivePayload - value:experiment - completionHandler:nil]; - } -} - -- (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)existingLastStartTime { - return [self.experimentController - latestExperimentStartTimestampBetweenTimestamp:existingLastStartTime - andPayloads:_experimentPayloads]; -} -@end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m index 39e5ecd5d0e..3c54afb8c85 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m @@ -22,7 +22,6 @@ #import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h" #import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" -#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h" #import "FirebaseRemoteConfig/FirebaseRemoteConfig-Swift.h" diff --git a/FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift b/FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift new file mode 100644 index 00000000000..b15edf7b70a --- /dev/null +++ b/FirebaseRemoteConfig/SwiftNew/ConfigExperiment.swift @@ -0,0 +1,193 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import FirebaseABTesting +import Foundation + +// TODO(ncooke3): Once everything is ported, the `@objc` and `public` access +// can be removed. + +/// Handles experiment information update and persistence. +@objc(RCNConfigExperiment) public final class ConfigExperiment: NSObject { + private static let experimentMetadataKeyLastStartTime = "last_experiment_start_time" + private static let serviceOrigin = "frc" + + @objc private var experimentPayloads: [Data] + @objc private var experimentMetadata: [String: Any] + @objc private var activeExperimentPayloads: [Data] + private let dbManager: ConfigDBManager + // TODO(ncooke3): This property could be made non-optional after ensuring the + // unit tests properly configure the default app. This is because the + // experiment controller comes from the ABTesting component. + private let experimentController: ExperimentController? + private let experimentStartTimeDateFormatter: DateFormatter + + /// Designated initializer; + @objc public init(DBManager: ConfigDBManager, + experimentController controller: ExperimentController?) { + experimentPayloads = [] + experimentMetadata = [:] + activeExperimentPayloads = [] + experimentStartTimeDateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + // Locale needs to be hardcoded. See + // https://developer.apple.com/library/ios/#qa/qa1480/_index.html for more details. + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.timeZone = TimeZone(abbreviation: "UTC") + return dateFormatter + }() + dbManager = DBManager + experimentController = controller + super.init() + loadExperimentFromTable() + } + + @objc private func loadExperimentFromTable() { + let completionHandler: (Bool, [String: Any]?) -> Void = { [weak self] _, result in + guard let self else { return } + + if result?[ConfigConstants.experimentTableKeyPayload] != nil { + self.experimentPayloads.removeAll() + if let experiments = result?[ConfigConstants.experimentTableKeyPayload] as? [Data] { + for experiment in experiments { + do { + try JSONSerialization.jsonObject(with: experiment) + self.experimentPayloads.append(experiment) + } catch { + RCLog.warning("I-RCN000031", "Experiment payload could not be parsed as JSON.") + } + } + } + } + + if let experimentTable = + result?[ConfigConstants.experimentTableKeyMetadata] as? [String: Any] { + self.experimentMetadata = experimentTable + } + + if result?[ConfigConstants.experimentTableKeyActivePayload] != nil { + self.activeExperimentPayloads.removeAll() + if let experiments = result?[ConfigConstants.experimentTableKeyActivePayload] as? [Data] { + for experiment in experiments { + do { + try JSONSerialization.jsonObject(with: experiment) + self.activeExperimentPayloads.append(experiment) + } catch { + RCLog.warning( + "I-RCN000031", + "Activated experiment payload could not be parsed as JSON." + ) + } + } + } + } + } + + dbManager.loadExperiment(completionHandler: completionHandler) + } + + /// Update/Persist experiment information from config fetch response. + @objc public func updateExperiments(withResponse response: [[String: Any]]?) { + // Cache fetched experiment payloads. + experimentPayloads.removeAll() + dbManager.deleteExperimentTable(forKey: ConfigConstants.experimentTableKeyPayload) + + if let response { + for experiment in response { + do { + let jsonData = try JSONSerialization.data(withJSONObject: experiment) + experimentPayloads.append(jsonData) + dbManager + .insertExperimentTable( + withKey: ConfigConstants.experimentTableKeyPayload, + value: jsonData + ) + } catch { + RCLog.error("I-RCN000030", "Invalid experiment payload to be serialized.") + } + } + } + } + + /// Update experiments to Firebase Analytics when `activateWithCompletion:` happens. + @objc public func updateExperiments(handler: (((any Error)?) -> Void)? = nil) { + let lifecycleEvent = LifecycleEvents() + + // Get the last experiment start time prior to the latest payload. + let lastStartTime = experimentMetadata[Self.experimentMetadataKeyLastStartTime] as? Double + + // Update the last experiment start time with the latest payload. + updateExperimentStartTime() + experimentController? + .updateExperiments( + withServiceOrigin: Self.serviceOrigin, + events: lifecycleEvent, + policy: .discardOldest, + lastStartTime: lastStartTime ?? 0, + payloads: experimentPayloads, + completionHandler: handler + ) + + // Update activated experiments payload and metadata in DB. + updateActiveExperimentsInDB() + } + + @objc private func updateExperimentStartTime() { + let existingLastStartTime = + experimentMetadata[Self.experimentMetadataKeyLastStartTime] as? Double + + let latestStartTime = latestStartTime(existingLastStartTime: existingLastStartTime ?? 0) + + experimentMetadata[Self.experimentMetadataKeyLastStartTime] = latestStartTime + + guard JSONSerialization.isValidJSONObject(experimentMetadata) else { + RCLog.error("I-RCN000028", "Invalid fetched experiment metadata to be serialized.") + return + } + + if let serializedExperimentMetadata = try? JSONSerialization.data( + withJSONObject: experimentMetadata, + options: .prettyPrinted + ) { + dbManager + .insertExperimentTable( + withKey: ConfigConstants.experimentTableKeyMetadata, + value: serializedExperimentMetadata + ) + } + } + + @objc private func updateActiveExperimentsInDB() { + // Put current fetched experiment payloads into activated experiment DB. + activeExperimentPayloads.removeAll() + dbManager.deleteExperimentTable(forKey: ConfigConstants.experimentTableKeyActivePayload) + for data in experimentPayloads { + activeExperimentPayloads.append(data) + dbManager + .insertExperimentTable( + withKey: ConfigConstants.experimentTableKeyActivePayload, + value: data + ) + } + } + + private func latestStartTime(existingLastStartTime: Double) -> TimeInterval { + experimentController? + .latestExperimentStartTimestampBetweenTimestamp( + existingLastStartTime, + andPayloads: experimentPayloads + ) ?? 0 + } +} diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m index 635aae34a98..2de42eb01cd 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m @@ -19,8 +19,6 @@ @import FirebaseRemoteConfig; -#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h" - #import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" #import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h" #import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h" @@ -31,6 +29,8 @@ #import "Interop/Analytics/Public/FIRAnalyticsInterop.h" +#import "FirebaseRemoteConfig/FirebaseRemoteConfig-Swift.h" + // Surface the internal FIRExperimentController initializer. @interface FIRExperimentController () - (instancetype)initWithAnalytics:(nullable id)analytics; @@ -40,7 +40,6 @@ @interface RCNConfigExperiment () @property(nonatomic, copy) NSMutableArray *experimentPayloads; @property(nonatomic, copy) NSMutableDictionary *experimentMetadata; @property(nonatomic, copy) NSMutableArray *activeExperimentPayloads; -@property(nonatomic, strong) RCNConfigDBManager *DBManager; - (NSTimeInterval)updateExperimentStartTime; - (void)loadExperimentFromTable; - (void)updateActiveExperimentsInDB; diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m index 02b19519515..677d6859d9c 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m @@ -25,7 +25,6 @@ #import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h" #import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h" #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" -#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h" #import "FirebaseRemoteConfig/Sources/RCNConfigRealtime.h" #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h"