diff --git a/CoreOnly/Tests/FirebasePodTest/Podfile b/CoreOnly/Tests/FirebasePodTest/Podfile index dfe7a0fa557..1e7dacbdb17 100644 --- a/CoreOnly/Tests/FirebasePodTest/Podfile +++ b/CoreOnly/Tests/FirebasePodTest/Podfile @@ -33,6 +33,7 @@ target 'FirebasePodTest' do pod 'FirebaseAppCheckInterop', :path => '../../../' pod 'FirebaseAuthInterop', :path => '../../../' pod 'FirebaseMessagingInterop', :path => '../../../' + pod 'FirebaseRemoteConfigInterop', :path => '../../../' pod 'FirebaseCoreInternal', :path => '../../../' pod 'FirebaseCoreExtension', :path => '../../../' pod 'FirebaseSessions', :path => '../../../' diff --git a/Crashlytics/CHANGELOG.md b/Crashlytics/CHANGELOG.md index 2c06843fcea..62f507a7c01 100644 --- a/Crashlytics/CHANGELOG.md +++ b/Crashlytics/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased - [added] Updated upload-symbols to 13.7 with VisionPro build phase support. (#12306) +- [changed] Added support for Crashlytics to report metadata about Remote Config keys and values. # 10.22.0 - [fixed] Force validation or rotation of FIDs for FirebaseSessions. diff --git a/Crashlytics/Crashlytics/Components/FIRCLSUserLogging.h b/Crashlytics/Crashlytics/Components/FIRCLSUserLogging.h index e0cadd483fb..0b2aa3922f8 100644 --- a/Crashlytics/Crashlytics/Components/FIRCLSUserLogging.h +++ b/Crashlytics/Crashlytics/Components/FIRCLSUserLogging.h @@ -81,7 +81,9 @@ void FIRCLSUserLoggingRecordUserKeysAndValues(NSDictionary* keysAndValues); void FIRCLSUserLoggingRecordInternalKeyValue(NSString* key, id value); void FIRCLSUserLoggingWriteInternalKeyValue(NSString* key, NSString* value); -void FIRCLSUserLoggingRecordError(NSError* error, NSDictionary* additionalUserInfo); +void FIRCLSUserLoggingRecordError(NSError* error, + NSDictionary* additionalUserInfo, + NSString* rolloutsInfoJSON); NSDictionary* FIRCLSUserLoggingGetCompactedKVEntries(FIRCLSUserLoggingKVStorage* storage, bool decodeHex); diff --git a/Crashlytics/Crashlytics/Components/FIRCLSUserLogging.m b/Crashlytics/Crashlytics/Components/FIRCLSUserLogging.m index 31b4deef1e9..4da93b43450 100644 --- a/Crashlytics/Crashlytics/Components/FIRCLSUserLogging.m +++ b/Crashlytics/Crashlytics/Components/FIRCLSUserLogging.m @@ -355,7 +355,8 @@ static void FIRCLSUserLoggingWriteError(FIRCLSFile *file, NSError *error, NSDictionary *additionalUserInfo, NSArray *addresses, - uint64_t timestamp) { + uint64_t timestamp, + NSString *rolloutsInfoJSON) { FIRCLSFileWriteSectionStart(file, "error"); FIRCLSFileWriteHashStart(file); FIRCLSFileWriteHashEntryHexEncodedString(file, "domain", [[error domain] UTF8String]); @@ -374,12 +375,20 @@ static void FIRCLSUserLoggingWriteError(FIRCLSFile *file, FIRCLSUserLoggingRecordErrorUserInfo(file, "info", [error userInfo]); FIRCLSUserLoggingRecordErrorUserInfo(file, "extra_info", additionalUserInfo); + // rollouts + if (rolloutsInfoJSON) { + FIRCLSFileWriteHashKey(file, "rollouts"); + FIRCLSFileWriteStringUnquoted(file, [rolloutsInfoJSON UTF8String]); + FIRCLSFileWriteHashEnd(file); + } + FIRCLSFileWriteHashEnd(file); FIRCLSFileWriteSectionEnd(file); } void FIRCLSUserLoggingRecordError(NSError *error, - NSDictionary *additionalUserInfo) { + NSDictionary *additionalUserInfo, + NSString *rolloutsInfoJSON) { if (!error) { return; } @@ -396,7 +405,8 @@ void FIRCLSUserLoggingRecordError(NSError *error, FIRCLSUserLoggingWriteAndCheckABFiles( &_firclsContext.readonly->logging.errorStorage, &_firclsContext.writable->logging.activeErrorLogPath, ^(FIRCLSFile *file) { - FIRCLSUserLoggingWriteError(file, error, additionalUserInfo, addresses, timestamp); + FIRCLSUserLoggingWriteError(file, error, additionalUserInfo, addresses, timestamp, + rolloutsInfoJSON); }); } diff --git a/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h new file mode 100644 index 00000000000..bda6eabbf5e --- /dev/null +++ b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h @@ -0,0 +1,30 @@ +// 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. + +#if SWIFT_PACKAGE +@import FirebaseCrashlyticsSwift; +#else // Swift Package Manager +#import +#endif // CocoaPods + +@interface FIRCLSRolloutsPersistenceManager : NSObject + +- (instancetype _Nullable)initWithFileManager:(FIRCLSFileManager *_Nonnull)fileManager; +- (instancetype _Nonnull)init NS_UNAVAILABLE; ++ (instancetype _Nonnull)new NS_UNAVAILABLE; + +- (void)updateRolloutsStateToPersistenceWithRollouts:(NSData *_Nonnull)rollouts + reportID:(NSString *_Nonnull)reportID; +- (void)debugLogWithMessage:(NSString *_Nonnull)message; +@end diff --git a/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m new file mode 100644 index 00000000000..3e7867dab76 --- /dev/null +++ b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m @@ -0,0 +1,67 @@ +// 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 +#include "Crashlytics/Crashlytics/Components/FIRCLSGlobals.h" +#include "Crashlytics/Crashlytics/Components/FIRCLSUserLogging.h" +#import "Crashlytics/Crashlytics/Helpers/FIRCLSLogger.h" +#import "Crashlytics/Crashlytics/Models/FIRCLSFileManager.h" +#import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h" + +#if SWIFT_PACKAGE +@import FirebaseCrashlyticsSwift; +#else // Swift Package Manager +#import +#endif // CocoaPods + +@interface FIRCLSRolloutsPersistenceManager : NSObject +@property(nonatomic, readonly) FIRCLSFileManager *fileManager; +@end + +@implementation FIRCLSRolloutsPersistenceManager +- (instancetype)initWithFileManager:(FIRCLSFileManager *)fileManager { + self = [super init]; + if (!self) { + return nil; + } + _fileManager = fileManager; + return self; +} + +- (void)updateRolloutsStateToPersistenceWithRollouts:(NSData *_Nonnull)rollouts + reportID:(NSString *_Nonnull)reportID { + NSString *rolloutsPath = [[[_fileManager activePath] stringByAppendingPathComponent:reportID] + stringByAppendingPathComponent:FIRCLSReportRolloutsFile]; + if (![_fileManager fileExistsAtPath:rolloutsPath]) { + if (![_fileManager createFileAtPath:rolloutsPath contents:nil attributes:nil]) { + FIRCLSDebugLog(@"Could not create rollouts.clsrecord file. Error was code: %d - message: %s", + errno, strerror(errno)); + } + } + + NSFileHandle *rolloutsFile = [NSFileHandle fileHandleForUpdatingAtPath:rolloutsPath]; + + dispatch_sync(FIRCLSGetLoggingQueue(), ^{ + [rolloutsFile seekToEndOfFile]; + [rolloutsFile writeData:rollouts]; + NSData *newLineData = [@"\n" dataUsingEncoding:NSUTF8StringEncoding]; + [rolloutsFile writeData:newLineData]; + }); +} + +- (void)debugLogWithMessage:(NSString *_Nonnull)message { + FIRCLSDebugLog(message); +} + +@end diff --git a/Crashlytics/Crashlytics/FIRCrashlytics.m b/Crashlytics/Crashlytics/FIRCrashlytics.m index 4d112cddad4..85502b2a9d9 100644 --- a/Crashlytics/Crashlytics/FIRCrashlytics.m +++ b/Crashlytics/Crashlytics/FIRCrashlytics.m @@ -31,6 +31,7 @@ #import "Crashlytics/Crashlytics/Helpers/FIRCLSDefines.h" #include "Crashlytics/Crashlytics/Helpers/FIRCLSProfiling.h" #include "Crashlytics/Crashlytics/Helpers/FIRCLSUtility.h" +#import "Crashlytics/Crashlytics/Models/FIRCLSExecutionIdentifierModel.h" #import "Crashlytics/Crashlytics/Models/FIRCLSFileManager.h" #import "Crashlytics/Crashlytics/Models/FIRCLSSettings.h" #import "Crashlytics/Crashlytics/Settings/Models/FIRCLSApplicationIdentifierModel.h" @@ -47,6 +48,7 @@ #import "Crashlytics/Crashlytics/Controllers/FIRCLSNotificationManager.h" #import "Crashlytics/Crashlytics/Controllers/FIRCLSReportManager.h" #import "Crashlytics/Crashlytics/Controllers/FIRCLSReportUploader.h" +#import "Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h" #import "Crashlytics/Crashlytics/Private/FIRCLSExistingReportManager_Private.h" #import "Crashlytics/Crashlytics/Private/FIRCLSOnDemandModel_Private.h" #import "Crashlytics/Crashlytics/Private/FIRExceptionModel_Private.h" @@ -58,6 +60,12 @@ #import @import FirebaseSessions; +@import FirebaseRemoteConfigInterop; +#if SWIFT_PACKAGE +@import FirebaseCrashlyticsSwift; +#else // Swift Package Manager +#import +#endif // CocoaPods #if TARGET_OS_IPHONE #import @@ -76,7 +84,10 @@ @protocol FIRCrashlyticsInstanceProvider @end -@interface FIRCrashlytics () +@interface FIRCrashlytics () @property(nonatomic) BOOL didPreviouslyCrash; @property(nonatomic, copy) NSString *googleAppID; @@ -91,6 +102,8 @@ @interface FIRCrashlytics () )analytics - sessions:(id)sessions { + sessions:(id)sessions + remoteConfig:(id)remoteConfig { self = [super init]; if (self) { @@ -189,8 +203,19 @@ - (instancetype)initWithApp:(FIRApp *)app }] catch:^void(NSError *error) { FIRCLSErrorLog(@"Crash reporting failed to initialize with error: %@", error); }]; - } + // RemoteConfig subscription should be made after session report directory created. + if (remoteConfig) { + FIRCLSDebugLog(@"Registering RemoteConfig SDK subscription for rollouts data"); + + FIRCLSRolloutsPersistenceManager *persistenceManager = + [[FIRCLSRolloutsPersistenceManager alloc] initWithFileManager:_fileManager]; + _remoteConfigManager = + [[FIRCLSRemoteConfigManager alloc] initWithRemoteConfig:remoteConfig + persistenceDelegate:persistenceManager]; + [remoteConfig registerRolloutsStateSubscriber:self for:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform]; + } + } return self; } @@ -215,6 +240,7 @@ + (void)load { id analytics = FIR_COMPONENT(FIRAnalyticsInterop, container); id sessions = FIR_COMPONENT(FIRSessionsProvider, container); + id remoteConfig = FIR_COMPONENT(FIRRemoteConfigInterop, container); FIRInstallations *installations = [FIRInstallations installationsWithApp:container.app]; @@ -224,7 +250,8 @@ + (void)load { appInfo:NSBundle.mainBundle.infoDictionary installations:installations analytics:analytics - sessions:sessions]; + sessions:sessions + remoteConfig:remoteConfig]; }; FIRComponent *component = @@ -377,11 +404,13 @@ - (void)recordError:(NSError *)error { } - (void)recordError:(NSError *)error userInfo:(NSDictionary *)userInfo { - FIRCLSUserLoggingRecordError(error, userInfo); + NSString *rolloutsInfoJSON = [_remoteConfigManager getRolloutAssignmentsEncodedJsonString]; + FIRCLSUserLoggingRecordError(error, userInfo, rolloutsInfoJSON); } - (void)recordExceptionModel:(FIRExceptionModel *)exceptionModel { - FIRCLSExceptionRecordModel(exceptionModel); + NSString *rolloutsInfoJSON = [_remoteConfigManager getRolloutAssignmentsEncodedJsonString]; + FIRCLSExceptionRecordModel(exceptionModel, rolloutsInfoJSON); } - (void)recordOnDemandExceptionModel:(FIRExceptionModel *)exceptionModel { @@ -407,4 +436,14 @@ - (FIRSessionsSubscriberName)sessionsSubscriberName { return FIRSessionsSubscriberNameCrashlytics; } +#pragma mark - FIRRolloutsStateSubscriber +- (void)rolloutsStateDidChange:(FIRRolloutsState *_Nonnull)rolloutsState { + if (!_remoteConfigManager) { + FIRCLSDebugLog(@"rolloutsStateDidChange gets called without init the rc manager."); + return; + } + NSString *currentReportID = _managerData.executionIDModel.executionID; + [_remoteConfigManager updateRolloutsStateWithRolloutsState:rolloutsState + reportID:currentReportID]; +} @end diff --git a/Crashlytics/Crashlytics/Handlers/FIRCLSException.h b/Crashlytics/Crashlytics/Handlers/FIRCLSException.h index ae53b916f8b..65aae9bfd32 100644 --- a/Crashlytics/Crashlytics/Handlers/FIRCLSException.h +++ b/Crashlytics/Crashlytics/Handlers/FIRCLSException.h @@ -60,7 +60,7 @@ void FIRCLSExceptionRaiseTestObjCException(void) __attribute((noreturn)); void FIRCLSExceptionRaiseTestCppException(void) __attribute((noreturn)); #ifdef __OBJC__ -void FIRCLSExceptionRecordModel(FIRExceptionModel* exceptionModel); +void FIRCLSExceptionRecordModel(FIRExceptionModel* exceptionModel, NSString* rolloutsInfoJSON); NSString* FIRCLSExceptionRecordOnDemandModel(FIRExceptionModel* exceptionModel, int previousRecordedOnDemandExceptions, int previousDroppedOnDemandExceptions); @@ -68,7 +68,8 @@ void FIRCLSExceptionRecordNSException(NSException* exception); void FIRCLSExceptionRecord(FIRCLSExceptionType type, const char* name, const char* reason, - NSArray* frames); + NSArray* frames, + NSString* rolloutsInfoJSON); NSString* FIRCLSExceptionRecordOnDemand(FIRCLSExceptionType type, const char* name, const char* reason, diff --git a/Crashlytics/Crashlytics/Handlers/FIRCLSException.mm b/Crashlytics/Crashlytics/Handlers/FIRCLSException.mm index 798a4548ded..b92cd9848dd 100644 --- a/Crashlytics/Crashlytics/Handlers/FIRCLSException.mm +++ b/Crashlytics/Crashlytics/Handlers/FIRCLSException.mm @@ -82,11 +82,11 @@ void FIRCLSExceptionInitialize(FIRCLSExceptionReadOnlyContext *roContext, rwContext->customExceptionCount = 0; } -void FIRCLSExceptionRecordModel(FIRExceptionModel *exceptionModel) { +void FIRCLSExceptionRecordModel(FIRExceptionModel *exceptionModel, NSString *rolloutsInfoJSON) { const char *name = [[exceptionModel.name copy] UTF8String]; const char *reason = [[exceptionModel.reason copy] UTF8String] ?: ""; - - FIRCLSExceptionRecord(FIRCLSExceptionTypeCustom, name, reason, [exceptionModel.stackTrace copy]); + FIRCLSExceptionRecord(FIRCLSExceptionTypeCustom, name, reason, [exceptionModel.stackTrace copy], + rolloutsInfoJSON); } NSString *FIRCLSExceptionRecordOnDemandModel(FIRExceptionModel *exceptionModel, @@ -122,7 +122,7 @@ void FIRCLSExceptionRecordNSException(NSException *exception) { } FIRCLSExceptionRecord(FIRCLSExceptionTypeObjectiveC, [name UTF8String], [reason UTF8String], - frames); + frames, nil); } static void FIRCLSExceptionRecordFrame(FIRCLSFile *file, FIRStackFrame *frame) { @@ -175,7 +175,8 @@ void FIRCLSExceptionWrite(FIRCLSFile *file, FIRCLSExceptionType type, const char *name, const char *reason, - NSArray *frames) { + NSArray *frames, + NSString *rolloutsInfoJSON) { FIRCLSFileWriteSectionStart(file, "exception"); FIRCLSFileWriteHashStart(file); @@ -196,6 +197,12 @@ void FIRCLSExceptionWrite(FIRCLSFile *file, FIRCLSFileWriteArrayEnd(file); } + if (rolloutsInfoJSON) { + FIRCLSFileWriteHashKey(file, "rollouts"); + FIRCLSFileWriteStringUnquoted(file, [rolloutsInfoJSON UTF8String]); + FIRCLSFileWriteHashEnd(file); + } + FIRCLSFileWriteHashEnd(file); FIRCLSFileWriteSectionEnd(file); @@ -204,7 +211,8 @@ void FIRCLSExceptionWrite(FIRCLSFile *file, void FIRCLSExceptionRecord(FIRCLSExceptionType type, const char *name, const char *reason, - NSArray *frames) { + NSArray *frames, + NSString *rolloutsInfoJSON) { if (!FIRCLSContextIsInitialized()) { return; } @@ -224,7 +232,7 @@ void FIRCLSExceptionRecord(FIRCLSExceptionType type, return; } - FIRCLSExceptionWrite(&file, type, name, reason, frames); + FIRCLSExceptionWrite(&file, type, name, reason, frames, nil); // We only want to do this work if we have the expectation that we'll actually crash FIRCLSHandler(&file, mach_thread_self(), NULL); @@ -235,7 +243,7 @@ void FIRCLSExceptionRecord(FIRCLSExceptionType type, FIRCLSUserLoggingWriteAndCheckABFiles( &_firclsContext.readonly->logging.customExceptionStorage, &_firclsContext.writable->logging.activeCustomExceptionPath, ^(FIRCLSFile *file) { - FIRCLSExceptionWrite(file, type, name, reason, frames); + FIRCLSExceptionWrite(file, type, name, reason, frames, rolloutsInfoJSON); }); } @@ -271,6 +279,7 @@ void FIRCLSExceptionRecord(FIRCLSExceptionType type, // Create new report and copy into it the current state of custom keys and log and the sdk.log, // binary_images.clsrecord, and metadata.clsrecord files. + // Also copy rollouts.clsrecord if applicable. NSError *error = nil; BOOL copied = [fileManager.underlyingFileManager copyItemAtPath:currentReportPath toPath:newReportPath @@ -343,7 +352,7 @@ void FIRCLSExceptionRecord(FIRCLSExceptionType type, FIRCLSSDKLog("Unable to open log file for on demand custom exception\n"); return nil; } - FIRCLSExceptionWrite(&file, type, name, reason, frames); + FIRCLSExceptionWrite(&file, type, name, reason, frames, nil); FIRCLSHandler(&file, mach_thread_self(), NULL); FIRCLSFileClose(&file); @@ -397,19 +406,21 @@ static void FIRCLSCatchAndRecordActiveException(std::type_info *typeInfo) { #endif } } catch (const char *exc) { - FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, "const char *", exc, nil); + FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, "const char *", exc, nil, nil); } catch (const std::string &exc) { - FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, "std::string", exc.c_str(), nil); + FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, "std::string", exc.c_str(), nil, nil); } catch (const std::exception &exc) { - FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, FIRCLSExceptionDemangle(name), exc.what(), nil); + FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, FIRCLSExceptionDemangle(name), exc.what(), nil, + nil); } catch (const std::exception *exc) { - FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, FIRCLSExceptionDemangle(name), exc->what(), nil); + FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, FIRCLSExceptionDemangle(name), exc->what(), nil, + nil); } catch (const std::bad_alloc &exc) { // it is especially important to avoid demangling in this case, because the expetation at this // point is that all allocations could fail - FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, "std::bad_alloc", exc.what(), nil); + FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, "std::bad_alloc", exc.what(), nil, nil); } catch (...) { - FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, FIRCLSExceptionDemangle(name), "", nil); + FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, FIRCLSExceptionDemangle(name), "", nil, nil); } } diff --git a/Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h b/Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h index 6303962c667..624c1990ae7 100644 --- a/Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h +++ b/Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h @@ -36,6 +36,7 @@ extern NSString *const FIRCLSReportInternalIncrementalKVFile; extern NSString *const FIRCLSReportInternalCompactedKVFile; extern NSString *const FIRCLSReportUserIncrementalKVFile; extern NSString *const FIRCLSReportUserCompactedKVFile; +extern NSString *const FIRCLSReportRolloutsFile; @class FIRCLSFileManager; diff --git a/Crashlytics/Crashlytics/Models/FIRCLSInternalReport.m b/Crashlytics/Crashlytics/Models/FIRCLSInternalReport.m index 61daf92f3e8..35160d1cbc1 100644 --- a/Crashlytics/Crashlytics/Models/FIRCLSInternalReport.m +++ b/Crashlytics/Crashlytics/Models/FIRCLSInternalReport.m @@ -41,6 +41,7 @@ NSString *const FIRCLSReportInternalCompactedKVFile = @"internal_compacted_kv.clsrecord"; NSString *const FIRCLSReportUserIncrementalKVFile = @"user_incremental_kv.clsrecord"; NSString *const FIRCLSReportUserCompactedKVFile = @"user_compacted_kv.clsrecord"; +NSString *const FIRCLSReportRolloutsFile = @"rollouts.clsrecord"; @interface FIRCLSInternalReport () { NSString *_identifier; diff --git a/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift b/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift new file mode 100644 index 00000000000..d6d5cb16b82 --- /dev/null +++ b/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift @@ -0,0 +1,141 @@ +// Copyright 2023 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 FirebaseRemoteConfigInterop +import Foundation + +@objc(FIRCLSPersistenceLog) +public protocol CrashlyticsPersistenceLog { + func updateRolloutsStateToPersistence(rollouts: Data, reportID: String) + func debugLog(message: String) +} + +@objc(FIRCLSRemoteConfigManager) +public class CrashlyticsRemoteConfigManager: NSObject { + public static let maxRolloutAssignments = 128 + public static let maxParameterValueLength = 256 + + private let lock = NSLock() + private var _rolloutAssignment: [RolloutAssignment] = [] + + var remoteConfig: RemoteConfigInterop + var persistenceDelegate: CrashlyticsPersistenceLog + + @objc public var rolloutAssignment: [RolloutAssignment] { + lock.lock() + defer { lock.unlock() } + let copy = _rolloutAssignment + return copy + } + + @objc public init(remoteConfig: RemoteConfigInterop, + persistenceDelegate: CrashlyticsPersistenceLog) { + self.remoteConfig = remoteConfig + self.persistenceDelegate = persistenceDelegate + } + + @objc public func updateRolloutsState(rolloutsState: RolloutsState, reportID: String) { + lock.lock() + _rolloutAssignment = normalizeRolloutAssignment(assignments: Array(rolloutsState.assignments)) + lock.unlock() + + // Writring to persistence + if let rolloutsData = + getRolloutsStateEncodedJsonData() { + persistenceDelegate.updateRolloutsStateToPersistence( + rollouts: rolloutsData, + reportID: reportID + ) + } + } + + /// Return string format: [{RolloutAssignment1}, {RolloutAssignment2}, {RolloutAssignment3}...] + /// This will get inserted into each clsrcord for non-fatal events. + /// Return a string type because later `FIRCLSFileWriteStringUnquoted` takes string as input + @objc public func getRolloutAssignmentsEncodedJsonString() -> String? { + let encodeData = getRolloutAssignmentsEncodedJsonData() + if let data = encodeData { + return String(data: data, encoding: .utf8) + } + + let debugInfo = encodeData?.debugDescription ?? "nil" + persistenceDelegate.debugLog(message: String( + format: "Failed to serialize rollouts: %@", + arguments: [debugInfo] + )) + + return nil + } +} + +private extension CrashlyticsRemoteConfigManager { + func normalizeRolloutAssignment(assignments: [RolloutAssignment]) -> [RolloutAssignment] { + var validatedAssignments = assignments + if assignments.count > CrashlyticsRemoteConfigManager.maxRolloutAssignments { + persistenceDelegate + .debugLog( + message: "Rollouts excess the maximum number of assignments can pass to Crashlytics" + ) + validatedAssignments = + Array(assignments[.. CrashlyticsRemoteConfigManager.maxParameterValueLength { + debugPrint( + "Rollouts excess the maximum length of parameter value can pass to Crashlytics", + assignment.parameterValue + ) + let upperBound = String.Index( + utf16Offset: CrashlyticsRemoteConfigManager.maxParameterValueLength, + in: assignment.parameterValue + ) + let slicedParameterValue = assignment.parameterValue[.. Data? { + let contentEncodedRolloutAssignments = rolloutAssignment.map { assignment in + EncodedRolloutAssignment(assignment: assignment) + } + + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + encoder.outputFormatting = .sortedKeys + let encodeData = try? encoder.encode(contentEncodedRolloutAssignments) + return encodeData + } + + /// Return string format: {"rollouts": [{RolloutAssignment1}, {RolloutAssignment2}, + /// {RolloutAssignment3}...]} + /// This will get stored in the separate rollouts.clsrecord + /// Return a data type because later `[NSFileHandler writeData:]` takes data as input + func getRolloutsStateEncodedJsonData() -> Data? { + let contentEncodedRolloutAssignments = rolloutAssignment.map { assignment in + EncodedRolloutAssignment(assignment: assignment) + } + + let state = EncodedRolloutsState(assignments: contentEncodedRolloutAssignments) + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + let encodeData = try? encoder.encode(state) + return encodeData + } +} diff --git a/Crashlytics/Crashlytics/Rollouts/EncodedRolloutAssignment.swift b/Crashlytics/Crashlytics/Rollouts/EncodedRolloutAssignment.swift new file mode 100644 index 00000000000..725b63050ec --- /dev/null +++ b/Crashlytics/Crashlytics/Rollouts/EncodedRolloutAssignment.swift @@ -0,0 +1,44 @@ +// 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 FirebaseRemoteConfigInterop +import Foundation + +@objc(FIRCLSEncodedRolloutsState) +class EncodedRolloutsState: NSObject, Codable { + @objc public private(set) var rollouts: [EncodedRolloutAssignment] + + @objc public init(assignments: [EncodedRolloutAssignment]) { + rollouts = assignments + super.init() + } +} + +@objc(FIRCLSEncodedRolloutAssignment) +class EncodedRolloutAssignment: NSObject, Codable { + @objc public private(set) var rolloutId: String + @objc public private(set) var variantId: String + @objc public private(set) var templateVersion: Int64 + @objc public private(set) var parameterKey: String + @objc public private(set) var parameterValue: String + + public init(assignment: RolloutAssignment) { + rolloutId = FileUtility.stringToHexConverter(for: assignment.rolloutId) + variantId = FileUtility.stringToHexConverter(for: assignment.variantId) + templateVersion = assignment.templateVersion + parameterKey = FileUtility.stringToHexConverter(for: assignment.parameterKey) + parameterValue = FileUtility.stringToHexConverter(for: assignment.parameterValue) + super.init() + } +} diff --git a/Crashlytics/Crashlytics/Rollouts/StringToHexConverter.swift b/Crashlytics/Crashlytics/Rollouts/StringToHexConverter.swift new file mode 100644 index 00000000000..9d4365db927 --- /dev/null +++ b/Crashlytics/Crashlytics/Rollouts/StringToHexConverter.swift @@ -0,0 +1,38 @@ +// 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 Foundation + +// This is a swift rewrite for the logic in FIRCLSFile for the function FIRCLSFileHexEncodeString() +@objc(FIRCLSwiftFileUtility) +public class FileUtility: NSObject { + @objc public static func stringToHexConverter(for string: String) -> String { + let hexMap = "0123456789abcdef" + + var processedString = "" + let utf8Array = string.utf8.map { UInt8($0) } + for c in utf8Array { + let index1 = String.Index( + utf16Offset: Int(c >> 4), + in: hexMap + ) + let index2 = String.Index( + utf16Offset: Int(c & 0x0F), + in: hexMap + ) + processedString = processedString + String(hexMap[index1]) + String(hexMap[index2]) + } + return processedString + } +} diff --git a/Crashlytics/UnitTests/FIRCLSFileTests.m b/Crashlytics/UnitTests/FIRCLSFileTests.m index 85ff6c36a57..b8895f68a67 100644 --- a/Crashlytics/UnitTests/FIRCLSFileTests.m +++ b/Crashlytics/UnitTests/FIRCLSFileTests.m @@ -14,6 +14,12 @@ #include "Crashlytics/Crashlytics/Helpers/FIRCLSFile.h" +#if SWIFT_PACKAGE +@import FirebaseCrashlyticsSwift; +#else // Swift Package Manager +#import +#endif // CocoaPods + #import @interface FIRCLSFileTests : XCTestCase @@ -169,6 +175,31 @@ - (void)hexEncodingStringWithFile:(FIRCLSFile *)file buffered ? @"" : @"un"); } +// This is the test to compare FIRCLSwiftFileUtility.stringToHexConverter(for:) and +// FIRCLSFileWriteHexEncodedString return the same hex encoding value +- (void)testHexEncodingStringObjcAndSwiftResultsSame { + NSString *testedValueString = @"是themis的测试数据,输入中文"; + + FIRCLSFile *unbufferedFile = &_unbufferedFile; + FIRCLSFileWriteHashStart(unbufferedFile); + FIRCLSFileWriteHashEntryHexEncodedString(unbufferedFile, "hex", [testedValueString UTF8String]); + FIRCLSFileWriteHashEnd(unbufferedFile); + NSString *contentsFromObjcHexEncoding = [self contentsOfFileAtPath:self.unbufferedPath]; + + FIRCLSFile *bufferedFile = &_bufferedFile; + NSString *encodedValue = [FIRCLSwiftFileUtility stringToHexConverterFor:testedValueString]; + FIRCLSFileWriteHashStart(bufferedFile); + FIRCLSFileWriteHashKey(bufferedFile, "hex"); + FIRCLSFileWriteStringUnquoted(bufferedFile, "\""); + FIRCLSFileWriteStringUnquoted(bufferedFile, [encodedValue UTF8String]); + FIRCLSFileWriteStringUnquoted(bufferedFile, "\""); + FIRCLSFileWriteHashEnd(bufferedFile); + FIRCLSFileFlushWriteBuffer(bufferedFile); + NSString *contentsFromSwiftHexEncoding = [self contentsOfFileAtPath:self.bufferedPath]; + + XCTAssertTrue([contentsFromObjcHexEncoding isEqualToString:contentsFromSwiftHexEncoding]); +} + #pragma mark - - (void)testHexEncodingLongString { diff --git a/Crashlytics/UnitTests/FIRCLSLoggingTests.m b/Crashlytics/UnitTests/FIRCLSLoggingTests.m index a5c72f5c73c..b79341fe06e 100644 --- a/Crashlytics/UnitTests/FIRCLSLoggingTests.m +++ b/Crashlytics/UnitTests/FIRCLSLoggingTests.m @@ -365,7 +365,7 @@ - (void)testLoggedError { code:-1 userInfo:@{@"key1" : @"value", @"key2" : @"value2"}]; - FIRCLSUserLoggingRecordError(error, @{@"additional" : @"key"}); + FIRCLSUserLoggingRecordError(error, @{@"additional" : @"key"}, nil); NSArray* errors = [self errorAContents]; @@ -405,7 +405,7 @@ - (void)testWritingMaximumNumberOfLoggedErrors { userInfo:@{@"key1" : @"value", @"key2" : @"value2"}]; for (size_t i = 0; i < _firclsContext.readonly->logging.errorStorage.maxEntries; ++i) { - FIRCLSUserLoggingRecordError(error, nil); + FIRCLSUserLoggingRecordError(error, nil, nil); } NSArray* errors = [self errorAContents]; @@ -414,7 +414,7 @@ - (void)testWritingMaximumNumberOfLoggedErrors { // at this point, if we log one more, we should expect a roll over to the next file - FIRCLSUserLoggingRecordError(error, nil); + FIRCLSUserLoggingRecordError(error, nil, nil); XCTAssertEqual([[self errorAContents] count], 8, @""); XCTAssertEqual([[self errorBContents] count], 1, @""); @@ -422,7 +422,7 @@ - (void)testWritingMaximumNumberOfLoggedErrors { // and our next entry should continue into the B file - FIRCLSUserLoggingRecordError(error, nil); + FIRCLSUserLoggingRecordError(error, nil, nil); XCTAssertEqual([[self errorAContents] count], 8, @""); XCTAssertEqual([[self errorBContents] count], 2, @""); @@ -432,7 +432,7 @@ - (void)testWritingMaximumNumberOfLoggedErrors { - (void)testLoggedErrorWithNullsInAdditionalInfo { NSError* error = [NSError errorWithDomain:@"Domain" code:-1 userInfo:nil]; - FIRCLSUserLoggingRecordError(error, @{@"null-key" : [NSNull null]}); + FIRCLSUserLoggingRecordError(error, @{@"null-key" : [NSNull null]}, nil); NSArray* errors = [self errorAContents]; diff --git a/Crashlytics/UnitTests/FIRCLSRolloutsPersistenceManagerTests.m b/Crashlytics/UnitTests/FIRCLSRolloutsPersistenceManagerTests.m new file mode 100644 index 00000000000..aec030d7538 --- /dev/null +++ b/Crashlytics/UnitTests/FIRCLSRolloutsPersistenceManagerTests.m @@ -0,0 +1,70 @@ +// Copyright 2024 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 +#import + +#import "Crashlytics/Crashlytics/Components/FIRCLSContext.h" +#import "Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h" +#import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h" +#import "Crashlytics/UnitTests/Mocks/FIRCLSTempMockFileManager.h" +#if SWIFT_PACKAGE +@import FirebaseCrashlyticsSwift; +#else // Swift Package Manager +#import +#endif // CocoaPods + +NSString *reportId = @"1234567"; + +@interface FIRCLSRolloutsPersistenceManagerTests : XCTestCase +@property(nonatomic, strong) FIRCLSTempMockFileManager *fileManager; +@property(nonatomic, strong) FIRCLSRolloutsPersistenceManager *rolloutsPersistenceManager; +@end + +@implementation FIRCLSRolloutsPersistenceManagerTests +- (void)setUp { + [super setUp]; + FIRCLSContextBaseInit(); + self.fileManager = [[FIRCLSTempMockFileManager alloc] init]; + [self.fileManager createReportDirectories]; + [self.fileManager setupNewPathForExecutionIdentifier:reportId]; + + self.rolloutsPersistenceManager = + [[FIRCLSRolloutsPersistenceManager alloc] initWithFileManager:self.fileManager]; +} + +- (void)tearDown { + [self.fileManager removeItemAtPath:_fileManager.rootPath]; + FIRCLSContextBaseDeinit(); + [super tearDown]; +} + +- (void)testUpdateRolloutsStateToPersistenceWithRollouts { + NSString *encodedStateString = + @"{rollouts:[{\"parameter_key\":\"6d795f66656174757265\",\"parameter_value\":" + @"\"e8bf99e698af7468656d6973e79a84e6b58be8af95e695b0e68daeefbc8ce8be93e585a5e4b8ade69687\"," + @"\"rollout_id\":\"726f6c6c6f75745f31\",\"template_version\":1,\"variant_id\":" + @"\"636f6e74726f6c\"}]}"; + + NSData *data = [encodedStateString dataUsingEncoding:NSUTF8StringEncoding]; + NSString *rolloutsFilePath = + [[[self.fileManager activePath] stringByAppendingPathComponent:reportId] + stringByAppendingPathComponent:FIRCLSReportRolloutsFile]; + + [self.rolloutsPersistenceManager updateRolloutsStateToPersistenceWithRollouts:data + reportID:reportId]; + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:rolloutsFilePath]); +} + +@end diff --git a/Crashlytics/UnitTests/FIRRecordExceptionModelTests.m b/Crashlytics/UnitTests/FIRRecordExceptionModelTests.m index 1908fea71db..e564614f8ae 100644 --- a/Crashlytics/UnitTests/FIRRecordExceptionModelTests.m +++ b/Crashlytics/UnitTests/FIRRecordExceptionModelTests.m @@ -75,7 +75,7 @@ - (void)testWrittenCLSRecordFile { FIRExceptionModel *exceptionModel = [FIRExceptionModel exceptionModelWithName:name reason:reason]; exceptionModel.stackTrace = stackTrace; - FIRCLSExceptionRecordModel(exceptionModel); + FIRCLSExceptionRecordModel(exceptionModel, nil); NSData *data = [NSData dataWithContentsOfFile:[self.reportPath diff --git a/Crashlytics/UnitTestsSwift/CrashlyticsRemoteConfigManagerTests.swift b/Crashlytics/UnitTestsSwift/CrashlyticsRemoteConfigManagerTests.swift new file mode 100644 index 00000000000..6c2e070e47f --- /dev/null +++ b/Crashlytics/UnitTestsSwift/CrashlyticsRemoteConfigManagerTests.swift @@ -0,0 +1,136 @@ +// Copyright 2023 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. +#if SWIFT_PACKAGE + @testable import FirebaseCrashlyticsSwift +#else + @testable import FirebaseCrashlytics +#endif +import FirebaseRemoteConfigInterop +import XCTest + +class RemoteConfigConfigMock: RemoteConfigInterop { + func registerRolloutsStateSubscriber(_ subscriber: FirebaseRemoteConfigInterop + .RolloutsStateSubscriber, + for namespace: String) {} +} + +class PersistanceManagerMock: CrashlyticsPersistenceLog { + func updateRolloutsStateToPersistence(rollouts: Data, reportID: String) {} + func debugLog(message: String) {} +} + +final class CrashlyticsRemoteConfigManagerTests: XCTestCase { + let rollouts: RolloutsState = { + let assignment1 = RolloutAssignment( + rolloutId: "rollout_1", + variantId: "control", + templateVersion: 1, + parameterKey: "my_feature", + parameterValue: "false" + ) + let assignment2 = RolloutAssignment( + rolloutId: "rollout_2", + variantId: "enabled", + templateVersion: 1, + parameterKey: "themis_big_feature", + parameterValue: "1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + ) + let rollouts = RolloutsState(assignmentList: [assignment1, assignment2]) + return rollouts + }() + + let singleRollout: RolloutsState = { + let assignment1 = RolloutAssignment( + rolloutId: "rollout_1", + variantId: "control", + templateVersion: 1, + parameterKey: "my_feature", + parameterValue: "这是themis的测试数据,输入中文" // check unicode + ) + let rollouts = RolloutsState(assignmentList: [assignment1]) + return rollouts + }() + + let rcInterop = RemoteConfigConfigMock() + + func testRemoteConfigManagerProperlyProcessRolloutsState() throws { + let rcManager = CrashlyticsRemoteConfigManager( + remoteConfig: rcInterop, + persistenceDelegate: PersistanceManagerMock() + ) + rcManager.updateRolloutsState(rolloutsState: rollouts, reportID: "12R") + XCTAssertEqual(rcManager.rolloutAssignment.count, 2) + + for assignment in rollouts.assignments { + if assignment.parameterKey == "themis_big_feature" { + XCTAssertEqual( + assignment.parameterValue.count, + CrashlyticsRemoteConfigManager.maxParameterValueLength + ) + } + } + } + + func testRemoteConfigManagerGenerateEncodedRolloutAssignmentsJson() throws { + let expectedString = + "[{\"parameter_key\":\"6d795f66656174757265\",\"parameter_value\":\"e8bf99e698af7468656d6973e79a84e6b58be8af95e695b0e68daeefbc8ce8be93e585a5e4b8ade69687\",\"rollout_id\":\"726f6c6c6f75745f31\",\"template_version\":1,\"variant_id\":\"636f6e74726f6c\"}]" + + let rcManager = CrashlyticsRemoteConfigManager( + remoteConfig: rcInterop, + persistenceDelegate: PersistanceManagerMock() + ) + rcManager.updateRolloutsState(rolloutsState: singleRollout, reportID: "456") + + let string = rcManager.getRolloutAssignmentsEncodedJsonString() + XCTAssertEqual(string, expectedString) + } + + func testMultiThreadsUpdateRolloutAssignments() throws { + let rcManager = CrashlyticsRemoteConfigManager( + remoteConfig: rcInterop, + persistenceDelegate: PersistanceManagerMock() + ) + DispatchQueue.main.async { [weak self] in + if let singleRollout = self?.singleRollout { + rcManager.updateRolloutsState(rolloutsState: singleRollout, reportID: "456") + XCTAssertEqual(rcManager.rolloutAssignment.count, 1) + } + } + + DispatchQueue.main.async { [weak self] in + if let rollouts = self?.rollouts { + rcManager.updateRolloutsState(rolloutsState: rollouts, reportID: "456") + XCTAssertEqual(rcManager.rolloutAssignment.count, 2) + } + } + } + + func testMultiThreadsReadAndWriteRolloutAssignments() throws { + let rcManager = CrashlyticsRemoteConfigManager( + remoteConfig: rcInterop, + persistenceDelegate: PersistanceManagerMock() + ) + rcManager.updateRolloutsState(rolloutsState: singleRollout, reportID: "456") + + DispatchQueue.main.async { [weak self] in + if let rollouts = self?.rollouts { + let oldAssignments = rcManager.rolloutAssignment + rcManager.updateRolloutsState(rolloutsState: rollouts, reportID: "456") + XCTAssertEqual(rcManager.rolloutAssignment.count, 2) + XCTAssertEqual(oldAssignments.count, 1) + } + } + XCTAssertEqual(rcManager.rolloutAssignment.count, 1) + } +} diff --git a/Example/watchOSSample/Podfile b/Example/watchOSSample/Podfile index 5dd5e804c13..2f862708597 100644 --- a/Example/watchOSSample/Podfile +++ b/Example/watchOSSample/Podfile @@ -19,6 +19,7 @@ target 'SampleWatchAppWatchKitExtension' do pod 'FirebaseDatabase', :path => '../../' pod 'FirebaseAppCheckInterop', :path => '../../' pod 'FirebaseAuthInterop', :path => '../../' + pod 'FirebaseRemoteConfigInterop', :path => '../../' pod 'Firebase/Messaging', :path => '../../' pod 'Firebase/Storage', :path => '../../' diff --git a/FirebaseCrashlytics.podspec b/FirebaseCrashlytics.podspec index 108645728e4..e67e886b399 100644 --- a/FirebaseCrashlytics.podspec +++ b/FirebaseCrashlytics.podspec @@ -27,7 +27,7 @@ Pod::Spec.new do |s| s.prefix_header_file = false s.source_files = [ - 'Crashlytics/Crashlytics/**/*.{c,h,m,mm}', + 'Crashlytics/Crashlytics/**/*.{c,h,m,mm,swift}', 'Crashlytics/Protogen/**/*.{c,h,m,mm}', 'Crashlytics/Shared/**/*.{c,h,m,mm}', 'Crashlytics/third_party/**/*.{c,h,m,mm}', @@ -62,6 +62,7 @@ Pod::Spec.new do |s| s.dependency 'FirebaseCore', '~> 10.5' s.dependency 'FirebaseInstallations', '~> 10.0' s.dependency 'FirebaseSessions', '~> 10.5' + s.dependency 'FirebaseRemoteConfigInterop', '~> 10.23' s.dependency 'PromisesObjC', '~> 2.1' s.dependency 'GoogleDataTransport', '~> 9.2' s.dependency 'GoogleUtilities/Environment', '~> 7.8' @@ -119,7 +120,8 @@ Pod::Spec.new do |s| :tvos => tvos_deployment_target } unit_tests.source_files = 'Crashlytics/UnitTests/*.[mh]', - 'Crashlytics/UnitTests/*/*.[mh]' + 'Crashlytics/UnitTests/*/*.[mh]', + 'Crashlytics/UnitTestsSwift/*.swift' unit_tests.resources = 'Crashlytics/UnitTests/Data/*', 'Crashlytics/UnitTests/*.clsrecord', 'Crashlytics/UnitTests/FIRCLSMachO/machO_data/*' diff --git a/FirebaseRemoteConfig.podspec b/FirebaseRemoteConfig.podspec index efef50fef50..9767a218a60 100644 --- a/FirebaseRemoteConfig.podspec +++ b/FirebaseRemoteConfig.podspec @@ -56,6 +56,7 @@ app update. s.dependency 'FirebaseInstallations', '~> 10.0' s.dependency 'GoogleUtilities/Environment', '~> 7.8' s.dependency 'GoogleUtilities/NSData+zlib', '~> 7.8' + s.dependency 'FirebaseRemoteConfigInterop', '~> 10.23' s.test_spec 'unit' do |unit_tests| unit_tests.scheme = { :code_coverage => true } @@ -80,7 +81,8 @@ app update. 'FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.m', 'FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m', 'FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h', - 'FirebaseRemoteConfig/Tests/Unit/RCNInstanceIDTest.m' + 'FirebaseRemoteConfig/Tests/Unit/RCNInstanceIDTest.m', + 'FirebaseRemoteConfig/Tests/SwiftUnit/*.swift' # Supply plist custom plist testing. unit_tests.resources = 'FirebaseRemoteConfig/Tests/Unit/Defaults-testInfo.plist', diff --git a/FirebaseRemoteConfig/CHANGELOG.md b/FirebaseRemoteConfig/CHANGELOG.md index 47e9aff902e..e56c9da1f2d 100644 --- a/FirebaseRemoteConfig/CHANGELOG.md +++ b/FirebaseRemoteConfig/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- [changed] Add support for other Firebase products to integrate with Remote Config. + # 10.17.0 - [feature] The `FirebaseRemoteConfig` module now contains Firebase Remote Config's Swift-only APIs that were previously only available via the diff --git a/FirebaseRemoteConfig/Interop/RemoteConfigConstants.swift b/FirebaseRemoteConfig/Interop/RemoteConfigConstants.swift new file mode 100644 index 00000000000..f9a10e409b7 --- /dev/null +++ b/FirebaseRemoteConfig/Interop/RemoteConfigConstants.swift @@ -0,0 +1,21 @@ +// 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 Foundation + +@objc(FIRRemoteConfigConstants) +public final class RemoteConfigConstants: NSObject { + @objc(FIRNamespaceGoogleMobilePlatform) public static let NamespaceGoogleMobilePlatform = + "firebase" +} diff --git a/FirebaseRemoteConfig/Interop/RemoteConfigInterop.swift b/FirebaseRemoteConfig/Interop/RemoteConfigInterop.swift new file mode 100644 index 00000000000..b7988efa389 --- /dev/null +++ b/FirebaseRemoteConfig/Interop/RemoteConfigInterop.swift @@ -0,0 +1,21 @@ +// Copyright 2023 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 Foundation + +@objc(FIRRemoteConfigInterop) +public protocol RemoteConfigInterop { + func registerRolloutsStateSubscriber(_ subscriber: RolloutsStateSubscriber, + for namespace: String) +} diff --git a/FirebaseRemoteConfig/Interop/RolloutAssignment.swift b/FirebaseRemoteConfig/Interop/RolloutAssignment.swift new file mode 100644 index 00000000000..715412bb4f1 --- /dev/null +++ b/FirebaseRemoteConfig/Interop/RolloutAssignment.swift @@ -0,0 +1,47 @@ +// Copyright 2023 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 Foundation + +@objc(FIRRolloutAssignment) +public class RolloutAssignment: NSObject { + @objc public var rolloutId: String + @objc public var variantId: String + @objc public var templateVersion: Int64 + @objc public var parameterKey: String + @objc public var parameterValue: String + + @objc public init(rolloutId: String, variantId: String, templateVersion: Int64, + parameterKey: String, + parameterValue: String) { + self.rolloutId = rolloutId + self.variantId = variantId + self.templateVersion = templateVersion + self.parameterKey = parameterKey + self.parameterValue = parameterValue + super.init() + } +} + +@objc(FIRRolloutsState) +public class RolloutsState: NSObject { + @objc public var assignments: Set = Set() + + @objc public init(assignmentList: [RolloutAssignment]) { + for assignment in assignmentList { + assignments.insert(assignment) + } + super.init() + } +} diff --git a/FirebaseRemoteConfig/Interop/RolloutsStateSubscriber.swift b/FirebaseRemoteConfig/Interop/RolloutsStateSubscriber.swift new file mode 100644 index 00000000000..88e5ba8772d --- /dev/null +++ b/FirebaseRemoteConfig/Interop/RolloutsStateSubscriber.swift @@ -0,0 +1,20 @@ +// Copyright 2023 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 Foundation + +@objc(FIRRolloutsStateSubscriber) +public protocol RolloutsStateSubscriber { + func rolloutsStateDidChange(_ rolloutsState: RolloutsState) +} diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index 4035d558707..561ada50693 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -45,6 +45,8 @@ /// Notification when config is successfully activated const NSNotificationName FIRRemoteConfigActivateNotification = @"FIRRemoteConfigActivateNotification"; +static NSNotificationName FIRRolloutsStateDidChangeNotificationName = + @"FIRRolloutsStateDidChangeNotification"; /// Listener for the get methods. typedef void (^FIRRemoteConfigListener)(NSString *_Nonnull, NSDictionary *_Nonnull); @@ -79,8 +81,9 @@ @implementation FIRRemoteConfig { *RCInstances; + (nonnull FIRRemoteConfig *)remoteConfigWithApp:(FIRApp *_Nonnull)firebaseApp { - return [FIRRemoteConfig remoteConfigWithFIRNamespace:FIRNamespaceGoogleMobilePlatform - app:firebaseApp]; + return [FIRRemoteConfig + remoteConfigWithFIRNamespace:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform + app:firebaseApp]; } + (nonnull FIRRemoteConfig *)remoteConfigWithFIRNamespace:(NSString *_Nonnull)firebaseNamespace { @@ -116,8 +119,9 @@ + (FIRRemoteConfig *)remoteConfig { @"initializer in SwiftUI."]; } - return [FIRRemoteConfig remoteConfigWithFIRNamespace:FIRNamespaceGoogleMobilePlatform - app:[FIRApp defaultApp]]; + return [FIRRemoteConfig + remoteConfigWithFIRNamespace:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform + app:[FIRApp defaultApp]]; } /// Singleton instance of serial queue for queuing all incoming RC calls. @@ -329,10 +333,20 @@ - (void)activateWithCompletion:(FIRRemoteConfigActivateChangeCompletion)completi // New config has been activated at this point FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000069", @"Config activated."); [strongSelf->_configContent activatePersonalization]; + // Update last active template version number in setting and userDefaults. + [strongSelf->_settings updateLastActiveTemplateVersion]; + // Update activeRolloutMetadata + [strongSelf->_configContent activateRolloutMetadata:^(BOOL success) { + if (success) { + [self notifyRolloutsStateChange:strongSelf->_configContent.activeRolloutMetadata + versionNumber:strongSelf->_settings.lastActiveTemplateVersion]; + } + }]; + // Update experiments only for 3p namespace NSString *namespace = [strongSelf->_FIRNamespace substringToIndex:[strongSelf->_FIRNamespace rangeOfString:@":"].location]; - if ([namespace isEqualToString:FIRNamespaceGoogleMobilePlatform]) { + if ([namespace isEqualToString:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform]) { dispatch_async(dispatch_get_main_queue(), ^{ [self notifyConfigHasActivated]; }); @@ -377,6 +391,17 @@ - (NSString *)fullyQualifiedNamespace:(NSString *)namespace { return fullyQualifiedNamespace; } +- (FIRRemoteConfigValue *)defaultValueForFullyQualifiedNamespace:(NSString *)namespace + key:(NSString *)key { + FIRRemoteConfigValue *value = self->_configContent.defaultConfig[namespace][key]; + if (!value) { + value = [[FIRRemoteConfigValue alloc] + initWithData:[NSData data] + source:(FIRRemoteConfigSource)FIRRemoteConfigSourceStatic]; + } + return value; +} + #pragma mark - Get Config Result - (FIRRemoteConfigValue *)objectForKeyedSubscript:(NSString *)key { @@ -402,13 +427,7 @@ - (FIRRemoteConfigValue *)configValueForKey:(NSString *)key { config:[self->_configContent getConfigAndMetadataForNamespace:FQNamespace]]; return; } - value = self->_configContent.defaultConfig[FQNamespace][key]; - if (value) { - return; - } - - value = [[FIRRemoteConfigValue alloc] initWithData:[NSData data] - source:FIRRemoteConfigSourceStatic]; + value = [self defaultValueForFullyQualifiedNamespace:FQNamespace key:key]; }); return value; } @@ -613,4 +632,67 @@ - (FIRConfigUpdateListenerRegistration *)addOnConfigUpdateListener: return [self->_configRealtime addConfigUpdateListener:listener]; } +#pragma mark - Rollout + +- (void)addRemoteConfigInteropSubscriber:(id)subscriber { + [[NSNotificationCenter defaultCenter] + addObserverForName:FIRRolloutsStateDidChangeNotificationName + object:self + queue:nil + usingBlock:^(NSNotification *_Nonnull notification) { + FIRRolloutsState *rolloutsState = + notification.userInfo[FIRRolloutsStateDidChangeNotificationName]; + [subscriber rolloutsStateDidChange:rolloutsState]; + }]; + // Send active rollout metadata stored in persistence while app launched if there is activeConfig + NSString *fullyQualifiedNamespace = [self fullyQualifiedNamespace:_FIRNamespace]; + NSDictionary *activeConfig = self->_configContent.activeConfig; + if (activeConfig[fullyQualifiedNamespace] && activeConfig[fullyQualifiedNamespace].count > 0) { + [self notifyRolloutsStateChange:self->_configContent.activeRolloutMetadata + versionNumber:self->_settings.lastActiveTemplateVersion]; + } +} + +- (void)notifyRolloutsStateChange:(NSArray *)rolloutMetadata + versionNumber:(NSString *)versionNumber { + NSArray *rolloutsAssignments = + [self rolloutsAssignmentsWith:rolloutMetadata versionNumber:versionNumber]; + FIRRolloutsState *rolloutsState = + [[FIRRolloutsState alloc] initWithAssignmentList:rolloutsAssignments]; + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000069", + @"Send rollouts state notification with name %@ to RemoteConfigInterop.", + FIRRolloutsStateDidChangeNotificationName); + [[NSNotificationCenter defaultCenter] + postNotificationName:FIRRolloutsStateDidChangeNotificationName + object:self + userInfo:@{FIRRolloutsStateDidChangeNotificationName : rolloutsState}]; +} + +- (NSArray *)rolloutsAssignmentsWith: + (NSArray *)rolloutMetadata + versionNumber:(NSString *)versionNumber { + NSMutableArray *rolloutsAssignments = [[NSMutableArray alloc] init]; + NSString *FQNamespace = [self fullyQualifiedNamespace:_FIRNamespace]; + for (NSDictionary *metadata in rolloutMetadata) { + NSString *rolloutId = metadata[RCNFetchResponseKeyRolloutID]; + NSString *variantID = metadata[RCNFetchResponseKeyVariantID]; + NSArray *affectedParameterKeys = metadata[RCNFetchResponseKeyAffectedParameterKeys]; + if (rolloutId && variantID && affectedParameterKeys) { + for (NSString *key in affectedParameterKeys) { + FIRRemoteConfigValue *value = self->_configContent.activeConfig[FQNamespace][key]; + if (!value) { + value = [self defaultValueForFullyQualifiedNamespace:FQNamespace key:key]; + } + FIRRolloutAssignment *assignment = + [[FIRRolloutAssignment alloc] initWithRolloutId:rolloutId + variantId:variantID + templateVersion:[versionNumber longLongValue] + parameterKey:key + parameterValue:value.stringValue]; + [rolloutsAssignments addObject:assignment]; + } + } + } + return rolloutsAssignments; +} @end diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h b/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h index f015ea14974..e8dda531a01 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h @@ -17,6 +17,7 @@ #import #import "FirebaseCore/Extension/FirebaseCoreInternal.h" +@import FirebaseRemoteConfigInterop; @class FIRApp; @class FIRRemoteConfig; @@ -37,7 +38,8 @@ NS_ASSUME_NONNULL_BEGIN /// A concrete implementation for FIRRemoteConfigInterop to create Remote Config instances and /// register with Core's component system. -@interface FIRRemoteConfigComponent : NSObject +@interface FIRRemoteConfigComponent + : NSObject /// The FIRApp that instances will be set up with. @property(nonatomic, weak, readonly) FIRApp *app; @@ -45,6 +47,10 @@ NS_ASSUME_NONNULL_BEGIN /// Cached instances of Remote Config objects. @property(nonatomic, strong) NSMutableDictionary *instances; +/// Clear all the component instances from the singleton which created previously, this is for +/// testing only ++ (void)clearAllComponentInstances; + /// Default method for retrieving a Remote Config instance, or creating one if it doesn't exist. - (FIRRemoteConfig *)remoteConfigForNamespace:(NSString *)remoteConfigNamespace; diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m index e0adc7bccbf..81055451ae4 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m @@ -24,6 +24,31 @@ @implementation FIRRemoteConfigComponent +// Because Component now need to register two protocols (provider and interop), we need a way to +// return the same component instance for both registered protocol, this singleton pattern allow us +// to return the same component object for both registration callback. +static NSMutableDictionary *_componentInstances = nil; + ++ (FIRRemoteConfigComponent *)getComponentForApp:(FIRApp *)app { + @synchronized(_componentInstances) { + // need to init the dictionary first + if (!_componentInstances) { + _componentInstances = [[NSMutableDictionary alloc] init]; + } + if (![_componentInstances objectForKey:app.name]) { + _componentInstances[app.name] = [[self alloc] initWithApp:app]; + } + return _componentInstances[app.name]; + } + return nil; +} + ++ (void)clearAllComponentInstances { + @synchronized(_componentInstances) { + [_componentInstances removeAllObjects]; + } +} + /// Default method for retrieving a Remote Config instance, or creating one if it doesn't exist. - (FIRRemoteConfig *)remoteConfigForNamespace:(NSString *)remoteConfigNamespace { if (!remoteConfigNamespace) { @@ -102,9 +127,29 @@ + (void)load { creationBlock:^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) { // Cache the component so instances of Remote Config are cached. *isCacheable = YES; - return [[FIRRemoteConfigComponent alloc] initWithApp:container.app]; + return [FIRRemoteConfigComponent getComponentForApp:container.app]; + }]; + + // Unlike provider needs to setup a hard dependency on remote config, interop allows an optional + // dependency on RC + FIRComponent *rcInterop = [FIRComponent + componentWithProtocol:@protocol(FIRRemoteConfigInterop) + instantiationTiming:FIRInstantiationTimingAlwaysEager + dependencies:@[] + creationBlock:^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) { + // Cache the component so instances of Remote Config are cached. + *isCacheable = YES; + return [FIRRemoteConfigComponent getComponentForApp:container.app]; }]; - return @[ rcProvider ]; + return @[ rcProvider, rcInterop ]; +} + +#pragma mark - Remote Config Interop Protocol + +- (void)registerRolloutsStateSubscriber:(id)subscriber + for:(NSString * _Nonnull)namespace { + FIRRemoteConfig *instance = [self remoteConfigForNamespace:namespace]; + [instance addRemoteConfigInteropSubscriber:subscriber]; } @end diff --git a/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h b/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h index ef7def6fd9d..4420dcb2679 100644 --- a/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h +++ b/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h @@ -23,6 +23,7 @@ @class RCNConfigFetch; @class RCNConfigRealtime; @protocol FIRAnalyticsInterop; +@protocol FIRRolloutsStateSubscriber; NS_ASSUME_NONNULL_BEGIN @@ -78,6 +79,9 @@ NS_ASSUME_NONNULL_BEGIN configContent:(RCNConfigContent *)configContent analytics:(nullable id)analytics; +/// Register RolloutsStateSubcriber to FIRRemoteConfig instance +- (void)addRemoteConfigInteropSubscriber:(id _Nonnull)subscriber; + @end NS_ASSUME_NONNULL_END diff --git a/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h b/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h index 987f3a98225..36fb8e7435f 100644 --- a/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h +++ b/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h @@ -79,8 +79,10 @@ @property(nonatomic, readwrite, assign) NSString *lastETag; /// The timestamp of the last eTag update. @property(nonatomic, readwrite, assign) NSTimeInterval lastETagUpdateTime; -// Last fetched template version. -@property(nonatomic, readwrite, assign) NSString *lastTemplateVersion; +/// Last fetched template version. +@property(nonatomic, readwrite, assign) NSString *lastFetchedTemplateVersion; +/// Last active template version. +@property(nonatomic, readwrite, assign) NSString *lastActiveTemplateVersion; #pragma mark Throttling properties @@ -134,6 +136,9 @@ /// indicates a server issue. - (void)updateRealtimeExponentialBackoffTime; +/// Update last active template version from last fetched template version. +- (void)updateLastActiveTemplateVersion; + /// Returns the difference between the Realtime backoff end time and the current time in a /// NSTimeInterval format. - (NSTimeInterval)getRealtimeBackoffInterval; diff --git a/FirebaseRemoteConfig/Sources/RCNConfigConstants.h b/FirebaseRemoteConfig/Sources/RCNConfigConstants.h index db0e0213ae1..51d248c4106 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigConstants.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigConstants.h @@ -37,6 +37,14 @@ static NSString *const RCNFetchResponseKeyEntries = @"entries"; static NSString *const RCNFetchResponseKeyExperimentDescriptions = @"experimentDescriptions"; /// Key that includes data for Personalization metadata. static NSString *const RCNFetchResponseKeyPersonalizationMetadata = @"personalizationMetadata"; +/// Key that includes data for Rollout metadata. +static NSString *const RCNFetchResponseKeyRolloutMetadata = @"rolloutMetadata"; +/// Key that indicates rollout id in Rollout metadata. +static NSString *const RCNFetchResponseKeyRolloutID = @"rolloutId"; +/// Key that indicates variant id in Rollout metadata. +static NSString *const RCNFetchResponseKeyVariantID = @"variantId"; +/// Key that indicates affected parameter keys in Rollout Metadata. +static NSString *const RCNFetchResponseKeyAffectedParameterKeys = @"affectedParameterKeys"; /// Error key. static NSString *const RCNFetchResponseKeyError = @"error"; /// Error code. @@ -58,5 +66,7 @@ static NSString *const RCNFetchResponseKeyStateNoTemplate = @"NO_TEMPLATE"; static NSString *const RCNFetchResponseKeyStateNoChange = @"NO_CHANGE"; /// Template found, but evaluates to empty (e.g. all keys omitted). static NSString *const RCNFetchResponseKeyStateEmptyConfig = @"EMPTY_CONFIG"; -/// Template Version key +/// Fetched Template Version key static NSString *const RCNFetchResponseKeyTemplateVersion = @"templateVersion"; +/// Active Template Version key +static NSString *const RCNActiveKeyTemplateVersion = @"activeTemplateVersion"; diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.h b/FirebaseRemoteConfig/Sources/RCNConfigContent.h index 34d0895243a..e8410074b30 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.h @@ -39,6 +39,8 @@ typedef NS_ENUM(NSInteger, RCNDBSource) { @property(nonatomic, readonly, copy) NSDictionary *activeConfig; /// Local default config that is provided by external users; @property(nonatomic, readonly, copy) NSDictionary *defaultConfig; +/// Active Rollout metadata that is currently used. +@property(nonatomic, readonly, copy) NSArray *activeRolloutMetadata; - (instancetype)init NS_UNAVAILABLE; @@ -65,6 +67,9 @@ typedef NS_ENUM(NSInteger, RCNDBSource) { /// Gets the active config and Personalization metadata. - (NSDictionary *)getConfigAndMetadataForNamespace:(NSString *)FIRNamespace; +/// Sets the fetched rollout metadata to active with a success completion handler. +- (void)activateRolloutMetadata:(void (^)(BOOL success))completionHandler; + /// Returns the updated parameters between fetched and active config. - (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace; diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.m b/FirebaseRemoteConfig/Sources/RCNConfigContent.m index 4f55a2e9274..1c266734c40 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.m @@ -38,6 +38,10 @@ @implementation RCNConfigContent { /// Pending Personalization metadata that is latest data from server that might or might not be /// applied. NSDictionary *_fetchedPersonalization; + /// Active Rollout metadata that is currently used. + NSArray *_activeRolloutMetadata; + /// Pending Rollout metadata that is latest data from server that might or might not be applied. + NSArray *_fetchedRolloutMetadata; /// DBManager RCNConfigDBManager *_DBManager; /// Current bundle identifier; @@ -80,6 +84,8 @@ - (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager { _defaultConfig = [[NSMutableDictionary alloc] init]; _activePersonalization = [[NSDictionary alloc] init]; _fetchedPersonalization = [[NSDictionary alloc] init]; + _activeRolloutMetadata = [[NSArray alloc] init]; + _fetchedRolloutMetadata = [[NSArray alloc] init]; _bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; if (!_bundleIdentifier) { FIRLogNotice(kFIRLoggerRemoteConfig, @"I-RCN000038", @@ -115,25 +121,30 @@ - (void)loadConfigFromMainTable { _isDatabaseLoadAlreadyInitiated = true; dispatch_group_enter(_dispatch_group); - [_DBManager - loadMainWithBundleIdentifier:_bundleIdentifier - completionHandler:^(BOOL success, NSDictionary *fetchedConfig, - NSDictionary *activeConfig, NSDictionary *defaultConfig) { - self->_fetchedConfig = [fetchedConfig mutableCopy]; - self->_activeConfig = [activeConfig mutableCopy]; - self->_defaultConfig = [defaultConfig mutableCopy]; - dispatch_group_leave(self->_dispatch_group); - }]; + [_DBManager loadMainWithBundleIdentifier:_bundleIdentifier + completionHandler:^( + BOOL success, NSDictionary *fetchedConfig, NSDictionary *activeConfig, + NSDictionary *defaultConfig, NSDictionary *rolloutMetadata) { + self->_fetchedConfig = [fetchedConfig mutableCopy]; + self->_activeConfig = [activeConfig mutableCopy]; + self->_defaultConfig = [defaultConfig mutableCopy]; + self->_fetchedRolloutMetadata = + [rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata] copy]; + self->_activeRolloutMetadata = + [rolloutMetadata[@RCNRolloutTableKeyActiveMetadata] copy]; + dispatch_group_leave(self->_dispatch_group); + }]; // TODO(karenzeng): Refactor personalization to be returned in loadMainWithBundleIdentifier above dispatch_group_enter(_dispatch_group); - [_DBManager loadPersonalizationWithCompletionHandler:^( - BOOL success, NSDictionary *fetchedPersonalization, - NSDictionary *activePersonalization, NSDictionary *defaultConfig) { - self->_fetchedPersonalization = [fetchedPersonalization copy]; - self->_activePersonalization = [activePersonalization copy]; - dispatch_group_leave(self->_dispatch_group); - }]; + [_DBManager + loadPersonalizationWithCompletionHandler:^( + BOOL success, NSDictionary *fetchedPersonalization, NSDictionary *activePersonalization, + NSDictionary *defaultConfig, NSDictionary *rolloutMetadata) { + self->_fetchedPersonalization = [fetchedPersonalization copy]; + self->_activePersonalization = [activePersonalization copy]; + dispatch_group_leave(self->_dispatch_group); + }]; } /// Update the current config result to main table. @@ -269,6 +280,7 @@ - (void)updateConfigContentWithResponse:(NSDictionary *)response [self handleUpdateStateForConfigNamespace:currentNamespace withEntries:response[RCNFetchResponseKeyEntries]]; [self handleUpdatePersonalization:response[RCNFetchResponseKeyPersonalizationMetadata]]; + [self handleUpdateRolloutFetchedMetadata:response[RCNFetchResponseKeyRolloutMetadata]]; return; } } @@ -279,6 +291,15 @@ - (void)activatePersonalization { fromSource:RCNDBSourceActive]; } +- (void)activateRolloutMetadata:(void (^)(BOOL success))completionHandler { + _activeRolloutMetadata = _fetchedRolloutMetadata; + [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyActiveMetadata + value:_activeRolloutMetadata + completionHandler:^(BOOL success, NSDictionary *result) { + completionHandler(success); + }]; +} + #pragma mark State handling - (void)handleNoChangeStateForConfigNamespace:(NSString *)currentNamespace { if (!_fetchedConfig[currentNamespace]) { @@ -342,6 +363,16 @@ - (void)handleUpdatePersonalization:(NSDictionary *)metadata { [_DBManager insertOrUpdatePersonalizationConfig:metadata fromSource:RCNDBSourceFetched]; } +- (void)handleUpdateRolloutFetchedMetadata:(NSArray *)metadata { + if (!metadata) { + metadata = [[NSArray alloc] init]; + } + _fetchedRolloutMetadata = metadata; + [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyFetchedMetadata + value:metadata + completionHandler:nil]; +} + #pragma mark - getter/setter - (NSDictionary *)fetchedConfig { /// If this is the first time reading the fetchedConfig, we might still be reading it from the @@ -369,6 +400,11 @@ - (NSDictionary *)activePersonalization { return _activePersonalization; } +- (NSArray *)activeRolloutMetadata { + [self checkAndWaitForInitialDatabaseLoad]; + return _activeRolloutMetadata; +} + - (NSDictionary *)getConfigAndMetadataForNamespace:(NSString *)FIRNamespace { /// If this is the first time reading the active metadata, we might still be reading it from the /// database. @@ -411,6 +447,8 @@ - (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace _activeConfig[FIRNamespace] ? _activeConfig[FIRNamespace] : [[NSDictionary alloc] init]; NSDictionary *fetchedP13n = _fetchedPersonalization; NSDictionary *activeP13n = _activePersonalization; + NSArray *fetchedRolloutMetadata = _fetchedRolloutMetadata; + NSArray *activeRolloutMetadata = _activeRolloutMetadata; // add new/updated params for (NSString *key in [fetchedConfig allKeys]) { @@ -439,8 +477,50 @@ - (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace } } + NSDictionary *fetchedRollouts = + [self getParameterKeyToRolloutMetadata:fetchedRolloutMetadata]; + NSDictionary *activeRollouts = + [self getParameterKeyToRolloutMetadata:activeRolloutMetadata]; + + // add params with new/updated rollout metadata + for (NSString *key in [fetchedRollouts allKeys]) { + if (activeRollouts[key] == nil || + ![activeRollouts[key] isEqualToDictionary:fetchedRollouts[key]]) { + [updatedKeys addObject:key]; + } + } + // add params with deleted rollout metadata + for (NSString *key in [activeRollouts allKeys]) { + if (fetchedRollouts[key] == nil) { + [updatedKeys addObject:key]; + } + } + configUpdate = [[FIRRemoteConfigUpdate alloc] initWithUpdatedKeys:updatedKeys]; return configUpdate; } +- (NSDictionary *)getParameterKeyToRolloutMetadata: + (NSArray *)rolloutMetadata { + NSMutableDictionary *result = + [[NSMutableDictionary alloc] init]; + for (NSDictionary *metadata in rolloutMetadata) { + NSString *rolloutId = metadata[RCNFetchResponseKeyRolloutID]; + NSString *variantId = metadata[RCNFetchResponseKeyVariantID]; + NSArray *affectedKeys = metadata[RCNFetchResponseKeyAffectedParameterKeys]; + if (rolloutId && variantId && affectedKeys) { + for (NSString *key in affectedKeys) { + if (result[key]) { + NSMutableDictionary *rolloutIdToVariantId = result[key]; + [rolloutIdToVariantId setValue:variantId forKey:rolloutId]; + } else { + NSMutableDictionary *rolloutIdToVariantId = [@{rolloutId : variantId} mutableCopy]; + [result setValue:rolloutIdToVariantId forKey:key]; + } + } + } + } + return [result copy]; +} + @end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h index 39c3e213b73..fba094624ca 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h @@ -53,10 +53,12 @@ typedef void (^RCNDBCompletion)(BOOL success, NSDictionary *result); /// @param fetchedConfig Return fetchedConfig loaded from DB /// @param activeConfig Return activeConfig loaded from DB /// @param defaultConfig Return defaultConfig loaded from DB +/// @param rolloutMetadata Return fetched and active RolloutMetadata loaded from DB typedef void (^RCNDBLoadCompletion)(BOOL success, NSDictionary *fetchedConfig, NSDictionary *activeConfig, - NSDictionary *defaultConfig); + NSDictionary *defaultConfig, + NSDictionary *rolloutMetadata); /// Returns the current version of the Remote Config database. + (NSString *)remoteConfigPathForDatabase; @@ -78,7 +80,6 @@ typedef void (^RCNDBLoadCompletion)(BOOL success, /// Load Personalization from table. /// @param handler The callback when reading from DB is complete. - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler; - /// Insert a record in metadata table. /// @param columnNameToValue The column name and its value to be inserted in metadata table. /// @param handler The callback. @@ -110,6 +111,15 @@ typedef void (^RCNDBLoadCompletion)(BOOL success, /// Insert or update the data in Personalization config. - (BOOL)insertOrUpdatePersonalizationConfig:(NSDictionary *)metadata fromSource:(RCNDBSource)source; +/// Insert rollout metadata in rollout table. +/// @param key Key indicating whether rollout metadata is fetched or active and defined in +/// RCNConfigDefines.h. +/// @param metadataList The metadata info for each rollout entry . +/// @param handler The callback. +- (void)insertOrUpdateRolloutTableWithKey:(NSString *)key + value:(NSArray *)metadataList + completionHandler:(RCNDBCompletion)handler; + /// Clear the record of given namespace and package name /// before updating the table. - (void)deleteRecordFromMainTableWithNamespace:(NSString *)namespace_p diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m index 6550760c16b..5b21306a85a 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m @@ -31,6 +31,7 @@ #define RCNTableNameInternalMetadata "internal_metadata" #define RCNTableNameExperiment "experiment" #define RCNTableNamePersonalization "personalization" +#define RCNTableNameRollout "rollout" static BOOL gIsNewDatabase; /// SQLite file name in versions 0, 1 and 2. @@ -284,11 +285,14 @@ - (BOOL)createTableSchema { "create TABLE IF NOT EXISTS " RCNTableNamePersonalization " (_id INTEGER PRIMARY KEY, key INTEGER, value BLOB)"; + static const char *createTableRollout = "create TABLE IF NOT EXISTS " RCNTableNameRollout + " (_id INTEGER PRIMARY KEY, key TEXT, value BLOB)"; + return [self executeQuery:createTableMain] && [self executeQuery:createTableMainActive] && [self executeQuery:createTableMainDefault] && [self executeQuery:createTableMetadata] && [self executeQuery:createTableInternalMetadata] && [self executeQuery:createTableExperiment] && - [self executeQuery:createTablePersonalization]; + [self executeQuery:createTablePersonalization] && [self executeQuery:createTableRollout]; } - (void)removeDatabaseOnDatabaseQueueAtPath:(NSString *)path { @@ -618,6 +622,52 @@ - (BOOL)insertOrUpdatePersonalizationConfig:(NSDictionary *)dataValue return YES; } +- (void)insertOrUpdateRolloutTableWithKey:(NSString *)key + value:(NSArray *)metadataList + completionHandler:(RCNDBCompletion)handler { + dispatch_async(_databaseOperationQueue, ^{ + BOOL success = [self insertOrUpdateRolloutTableWithKey:key value:metadataList]; + if (handler) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + handler(success, nil); + }); + } + }); +} + +- (BOOL)insertOrUpdateRolloutTableWithKey:(NSString *)key + value:(NSArray *)arrayValue { + RCN_MUST_NOT_BE_MAIN_THREAD(); + NSError *error; + NSData *dataValue = [NSJSONSerialization dataWithJSONObject:arrayValue + options:NSJSONWritingPrettyPrinted + error:&error]; + const char *SQL = + "INSERT OR REPLACE INTO " RCNTableNameRollout + " (_id, key, value) values ((SELECT _id from " RCNTableNameRollout " WHERE key = ?), ?, ?)"; + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return NO; + } + if (![self bindStringToStatement:statement index:1 string:key]) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + + if (![self bindStringToStatement:statement index:2 string:key]) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + + if (sqlite3_bind_blob(statement, 3, dataValue.bytes, (int)dataValue.length, NULL) != SQLITE_OK) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + + if (sqlite3_step(statement) != SQLITE_DONE) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + sqlite3_finalize(statement); + return YES; +} + #pragma mark - update - (void)updateMetadataWithOption:(RCNUpdateOption)option @@ -852,7 +902,6 @@ - (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler { - (NSMutableArray *)loadExperimentTableFromKey:(NSString *)key { RCN_MUST_NOT_BE_MAIN_THREAD(); - NSMutableArray *results = [[NSMutableArray alloc] init]; const char *SQL = "SELECT value FROM " RCNTableNameExperiment " WHERE key = ?"; sqlite3_stmt *statement = [self prepareSQL:SQL]; if (!statement) { @@ -861,12 +910,49 @@ - (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler { NSArray *params = @[ key ]; [self bindStringsToStatement:statement stringArray:params]; - NSData *experimentData; + NSMutableArray *results = [self loadValuesFromStatement:statement]; + return results; +} + +- (NSArray *)loadRolloutTableFromKey:(NSString *)key { + RCN_MUST_NOT_BE_MAIN_THREAD(); + const char *SQL = "SELECT value FROM " RCNTableNameRollout " WHERE key = ?"; + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return nil; + } + NSArray *params = @[ key ]; + [self bindStringsToStatement:statement stringArray:params]; + NSMutableArray *results = [self loadValuesFromStatement:statement]; + // There should be only one entry in this table. + if (results.count != 1) { + return nil; + } + NSArray *rollout; + // Convert from NSData to NSArray + if (results[0]) { + NSError *error; + rollout = [NSJSONSerialization JSONObjectWithData:results[0] options:0 error:&error]; + if (!rollout) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000011", + @"Failed to convert NSData to NSAarry for Rollout Metadata with error %@.", + error); + } + } + if (!rollout) { + rollout = [[NSArray alloc] init]; + } + return rollout; +} + +- (NSMutableArray *)loadValuesFromStatement:(sqlite3_stmt *)statement { + NSMutableArray *results = [[NSMutableArray alloc] init]; + NSData *value; while (sqlite3_step(statement) == SQLITE_ROW) { - experimentData = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 0) - length:sqlite3_column_bytes(statement, 0)]; - if (experimentData) { - [results addObject:experimentData]; + value = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 0) + length:sqlite3_column_bytes(statement, 0)]; + if (value) { + [results addObject:value]; } } @@ -880,7 +966,7 @@ - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler { RCNConfigDBManager *strongSelf = weakSelf; if (!strongSelf) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - handler(NO, [NSMutableDictionary new], [NSMutableDictionary new], nil); + handler(NO, [NSMutableDictionary new], [NSMutableDictionary new], nil, nil); }); return; } @@ -913,7 +999,7 @@ - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler { if (handler) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - handler(YES, fetchedPersonalization, activePersonalization, nil); + handler(YES, fetchedPersonalization, activePersonalization, nil, nil); }); } }); @@ -987,7 +1073,7 @@ - (void)loadMainWithBundleIdentifier:(NSString *)bundleIdentifier RCNConfigDBManager *strongSelf = weakSelf; if (!strongSelf) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - handler(NO, [NSDictionary new], [NSDictionary new], [NSDictionary new]); + handler(NO, [NSDictionary new], [NSDictionary new], [NSDictionary new], [NSDictionary new]); }); return; } @@ -1000,12 +1086,26 @@ - (void)loadMainWithBundleIdentifier:(NSString *)bundleIdentifier __block NSDictionary *defaultConfig = [strongSelf loadMainTableWithBundleIdentifier:bundleIdentifier fromSource:RCNDBSourceDefault]; + + __block NSArray *fetchedRolloutMetadata = + [strongSelf loadRolloutTableFromKey:@RCNRolloutTableKeyFetchedMetadata]; + __block NSArray *activeRolloutMetadata = + [strongSelf loadRolloutTableFromKey:@RCNRolloutTableKeyActiveMetadata]; + if (handler) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ fetchedConfig = fetchedConfig ? fetchedConfig : [[NSDictionary alloc] init]; activeConfig = activeConfig ? activeConfig : [[NSDictionary alloc] init]; defaultConfig = defaultConfig ? defaultConfig : [[NSDictionary alloc] init]; - handler(YES, fetchedConfig, activeConfig, defaultConfig); + fetchedRolloutMetadata = + fetchedRolloutMetadata ? fetchedRolloutMetadata : [[NSArray alloc] init]; + activeRolloutMetadata = + activeRolloutMetadata ? activeRolloutMetadata : [[NSArray alloc] init]; + NSDictionary *rolloutMetadata = @{ + @RCNRolloutTableKeyActiveMetadata : [activeRolloutMetadata copy], + @RCNRolloutTableKeyFetchedMetadata : [fetchedRolloutMetadata copy] + }; + handler(YES, fetchedConfig, activeConfig, defaultConfig, rolloutMetadata); }); } }); diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDefines.h b/FirebaseRemoteConfig/Sources/RCNConfigDefines.h index cf08f738105..1e95373541b 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDefines.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigDefines.h @@ -31,5 +31,7 @@ #define RCNExperimentTableKeyPayload "experiment_payload" #define RCNExperimentTableKeyMetadata "experiment_metadata" #define RCNExperimentTableKeyActivePayload "experiment_active_payload" +#define RCNRolloutTableKeyActiveMetadata "active_rollout_metadata" +#define RCNRolloutTableKeyFetchedMetadata "fetched_rollout_metadata" #endif diff --git a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m index c3a0f16ddd8..dbc4b9bec56 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m @@ -25,6 +25,7 @@ #import "FirebaseRemoteConfig/Sources/RCNConfigContent.h" #import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h" #import "FirebaseRemoteConfig/Sources/RCNDevice.h" +@import FirebaseRemoteConfigInterop; #ifdef RCN_STAGING_SERVER static NSString *const kServerURLDomain = @@ -105,7 +106,7 @@ - (instancetype)initWithContent:(RCNConfigContent *)content _content = content; _fetchSession = [self newFetchSession]; _options = options; - _templateVersionNumber = [self->_settings lastTemplateVersion]; + _templateVersionNumber = [self->_settings lastFetchedTemplateVersion]; } return self; } @@ -572,7 +573,7 @@ - (void)fetchWithUserProperties:(NSDictionary *)userProperties // Update experiments only for 3p namespace NSString *namespace = [strongSelf->_FIRNamespace substringToIndex:[strongSelf->_FIRNamespace rangeOfString:@":"].location]; - if ([namespace isEqualToString:FIRNamespaceGoogleMobilePlatform]) { + if ([namespace isEqualToString:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform]) { [strongSelf->_experiment updateExperimentsWithResponse: fetchedConfig[RCNFetchResponseKeyExperimentDescriptions]]; } diff --git a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m index 0b3e3ad1164..e85a63f4873 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m @@ -110,7 +110,8 @@ - (instancetype)initWithDatabaseManager:(RCNConfigDBManager *)manager } _isFetchInProgress = NO; - _lastTemplateVersion = [_userDefaultsManager lastTemplateVersion]; + _lastFetchedTemplateVersion = [_userDefaultsManager lastFetchedTemplateVersion]; + _lastActiveTemplateVersion = [_userDefaultsManager lastActiveTemplateVersion]; _realtimeExponentialBackoffRetryInterval = [_userDefaultsManager currentRealtimeThrottlingRetryIntervalSeconds]; _realtimeExponentialBackoffThrottleEndTime = [_userDefaultsManager realtimeThrottleEndTime]; @@ -292,7 +293,8 @@ - (void)updateMetadataWithFetchSuccessStatus:(BOOL)fetchSuccess [self updateLastFetchTimeInterval:[[NSDate date] timeIntervalSince1970]]; // Note: We expect the googleAppID to always be available. _deviceContext = FIRRemoteConfigDeviceContextWithProjectIdentifier(_googleAppID); - [_userDefaultsManager setLastTemplateVersion:templateVersion]; + _lastFetchedTemplateVersion = templateVersion; + [_userDefaultsManager setLastFetchedTemplateVersion:templateVersion]; } [self updateMetadataTable]; @@ -377,6 +379,11 @@ - (void)updateMetadataTable { [_DBManager insertMetadataTableWithValues:columnNameToValue completionHandler:nil]; } +- (void)updateLastActiveTemplateVersion { + _lastActiveTemplateVersion = _lastFetchedTemplateVersion; + [_userDefaultsManager setLastActiveTemplateVersion:_lastActiveTemplateVersion]; +} + #pragma mark - fetch request /// Returns a fetch request with the latest device and config change. diff --git a/FirebaseRemoteConfig/Sources/RCNConstants3P.m b/FirebaseRemoteConfig/Sources/RCNConstants3P.m index 6bd5d78d094..e64295be62c 100644 --- a/FirebaseRemoteConfig/Sources/RCNConstants3P.m +++ b/FirebaseRemoteConfig/Sources/RCNConstants3P.m @@ -17,4 +17,5 @@ #import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h" /// Firebase Remote Config service default namespace. +/// TODO(doudounan): Change to use this namespace defined in RemoteConfigInterop. NSString *const FIRNamespaceGoogleMobilePlatform = @"firebase"; diff --git a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h index acbcd5842f4..b235f217d81 100644 --- a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h +++ b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h @@ -44,7 +44,9 @@ NS_ASSUME_NONNULL_BEGIN /// Realtime retry count. @property(nonatomic, assign) int realtimeRetryCount; /// Last fetched template version. -@property(nonatomic, assign) NSString *lastTemplateVersion; +@property(nonatomic, assign) NSString *lastFetchedTemplateVersion; +/// Last active template version. +@property(nonatomic, assign) NSString *lastActiveTemplateVersion; /// Designated initializer. - (instancetype)initWithAppName:(NSString *)appName diff --git a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m index 29ec2e87a06..880a2157fe1 100644 --- a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m +++ b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m @@ -111,7 +111,7 @@ - (void)setLastETag:(NSString *)lastETag { } } -- (NSString *)lastTemplateVersion { +- (NSString *)lastFetchedTemplateVersion { NSDictionary *userDefaults = [self instanceUserDefaults]; if ([userDefaults objectForKey:RCNFetchResponseKeyTemplateVersion]) { return [userDefaults objectForKey:RCNFetchResponseKeyTemplateVersion]; @@ -120,12 +120,27 @@ - (NSString *)lastTemplateVersion { return @"0"; } -- (void)setLastTemplateVersion:(NSString *)templateVersion { +- (void)setLastFetchedTemplateVersion:(NSString *)templateVersion { if (templateVersion) { [self setInstanceUserDefaultsValue:templateVersion forKey:RCNFetchResponseKeyTemplateVersion]; } } +- (NSString *)lastActiveTemplateVersion { + NSDictionary *userDefaults = [self instanceUserDefaults]; + if ([userDefaults objectForKey:RCNActiveKeyTemplateVersion]) { + return [userDefaults objectForKey:RCNActiveKeyTemplateVersion]; + } + + return @"0"; +} + +- (void)setLastActiveTemplateVersion:(NSString *)templateVersion { + if (templateVersion) { + [self setInstanceUserDefaultsValue:templateVersion forKey:RCNActiveKeyTemplateVersion]; + } +} + - (NSTimeInterval)lastETagUpdateTime { NSNumber *lastETagUpdateTime = [[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNamelastETagUpdateTime]; diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp.xcodeproj/project.pbxproj b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..d7f955d7c07 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp.xcodeproj/project.pbxproj @@ -0,0 +1,1059 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 61A5654706089C41A7398CF3 /* Pods_FeatureRolloutsTestApp_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FE5945D035CAAB3297D2CAC /* Pods_FeatureRolloutsTestApp_iOS.framework */; }; + 848D345C8969AF72BCC0E2E4 /* Pods_FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1729B0ED2CACB9C5A62A6F8C /* Pods_FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.framework */; }; + 951D70152B71AD9B00BE7EED /* RemoteConfigButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951D70142B71AD9A00BE7EED /* RemoteConfigButtonView.swift */; }; + 951D70162B71AD9B00BE7EED /* RemoteConfigButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951D70142B71AD9A00BE7EED /* RemoteConfigButtonView.swift */; }; + AD11C57C978D52894BFDC47F /* Pods_FeatureRolloutsTestApp_RemoteConfig_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E60230146BDE14D306856CB /* Pods_FeatureRolloutsTestApp_RemoteConfig_iOS.framework */; }; + C427C4A32B4603F60088A488 /* FeatureRolloutsTestAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C427C4A22B4603F60088A488 /* FeatureRolloutsTestAppApp.swift */; }; + C427C4A52B4603F60088A488 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C427C4A42B4603F60088A488 /* ContentView.swift */; }; + C49C486C2B4704D900BC1456 /* FeatureRolloutsTestAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C427C4A22B4603F60088A488 /* FeatureRolloutsTestAppApp.swift */; }; + C49C48702B4704F300BC1456 /* FeatureRolloutsTestAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C427C4A22B4603F60088A488 /* FeatureRolloutsTestAppApp.swift */; }; + C49C487A2B4704F500BC1456 /* FeatureRolloutsTestAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C427C4A22B4603F60088A488 /* FeatureRolloutsTestAppApp.swift */; }; + C49C48832B47074400BC1456 /* FirebaseCrashlytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C49C48822B47074400BC1456 /* FirebaseCrashlytics.framework */; }; + C49C48872B47075600BC1456 /* FirebaseRemoteConfig.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C49C48862B47075600BC1456 /* FirebaseRemoteConfig.framework */; }; + C49C488B2B47075C00BC1456 /* FirebaseCrashlytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C49C488A2B47075C00BC1456 /* FirebaseCrashlytics.framework */; }; + C49C488F2B47076200BC1456 /* FirebaseRemoteConfig.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C49C488E2B47076200BC1456 /* FirebaseRemoteConfig.framework */; }; + C49C48952B47207200BC1456 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C48942B47207200BC1456 /* ContentView.swift */; }; + C49C48992B4720AE00BC1456 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C48982B4720AE00BC1456 /* ContentView.swift */; }; + C49C489C2B4720DD00BC1456 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C489B2B4720DD00BC1456 /* ContentView.swift */; }; + C49C489E2B4722C100BC1456 /* CrashButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C489D2B4722C100BC1456 /* CrashButtonView.swift */; }; + C49C489F2B47233000BC1456 /* CrashButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C489D2B4722C100BC1456 /* CrashButtonView.swift */; }; + C49C48A12B47261000BC1456 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C49C48A02B47261000BC1456 /* GoogleService-Info.plist */; }; + C49C48A22B47261000BC1456 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C49C48A02B47261000BC1456 /* GoogleService-Info.plist */; }; + C49C48A32B47261000BC1456 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C49C48A02B47261000BC1456 /* GoogleService-Info.plist */; }; + C49C48A42B47261000BC1456 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C49C48A02B47261000BC1456 /* GoogleService-Info.plist */; }; + F07A9478976524A8264259F0 /* Pods_FeatureRolloutsTestApp_Crashlytics_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5472955122D0CE0A8A3CE4D5 /* Pods_FeatureRolloutsTestApp_Crashlytics_iOS.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + C49C48852B47074400BC1456 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + C49C48892B47075600BC1456 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + C49C488D2B47075C00BC1456 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 025F972344BB0B489CC052D6 /* Pods-FeatureRolloutsTestApp_Crashlytics_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FeatureRolloutsTestApp_Crashlytics_iOS.debug.xcconfig"; path = "Target Support Files/Pods-FeatureRolloutsTestApp_Crashlytics_iOS/Pods-FeatureRolloutsTestApp_Crashlytics_iOS.debug.xcconfig"; sourceTree = ""; }; + 10710CAF870FA7E8D1ABF94C /* Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.release.xcconfig"; path = "Target Support Files/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.release.xcconfig"; sourceTree = ""; }; + 1729B0ED2CACB9C5A62A6F8C /* Pods_FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2842D338F32EE531C752262E /* Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.debug.xcconfig"; path = "Target Support Files/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.debug.xcconfig"; sourceTree = ""; }; + 2D15DD53784CDDE94D00AB02 /* Pods-FeatureRolloutsTestApp_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FeatureRolloutsTestApp_iOS.release.xcconfig"; path = "Target Support Files/Pods-FeatureRolloutsTestApp_iOS/Pods-FeatureRolloutsTestApp_iOS.release.xcconfig"; sourceTree = ""; }; + 2FE5945D035CAAB3297D2CAC /* Pods_FeatureRolloutsTestApp_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FeatureRolloutsTestApp_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 30BB126FCB8D5F53B5795500 /* Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.release.xcconfig"; path = "Target Support Files/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.release.xcconfig"; sourceTree = ""; }; + 4E60230146BDE14D306856CB /* Pods_FeatureRolloutsTestApp_RemoteConfig_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FeatureRolloutsTestApp_RemoteConfig_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5472955122D0CE0A8A3CE4D5 /* Pods_FeatureRolloutsTestApp_Crashlytics_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FeatureRolloutsTestApp_Crashlytics_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6D30A6D1F2CE622B6D5D563F /* Pods-FeatureRolloutsTestApp_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FeatureRolloutsTestApp_iOS.debug.xcconfig"; path = "Target Support Files/Pods-FeatureRolloutsTestApp_iOS/Pods-FeatureRolloutsTestApp_iOS.debug.xcconfig"; sourceTree = ""; }; + 8BA72854B19D7A9D9BE15E1D /* Pods-FeatureRolloutsTestApp_Crashlytics_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FeatureRolloutsTestApp_Crashlytics_iOS.release.xcconfig"; path = "Target Support Files/Pods-FeatureRolloutsTestApp_Crashlytics_iOS/Pods-FeatureRolloutsTestApp_Crashlytics_iOS.release.xcconfig"; sourceTree = ""; }; + 951D70142B71AD9A00BE7EED /* RemoteConfigButtonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteConfigButtonView.swift; sourceTree = ""; }; + AF260B513E38B2528E7B13CC /* Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.debug.xcconfig"; path = "Target Support Files/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.debug.xcconfig"; sourceTree = ""; }; + C427C49F2B4603F60088A488 /* FeatureRolloutsTestApp_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FeatureRolloutsTestApp_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + C427C4A22B4603F60088A488 /* FeatureRolloutsTestAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureRolloutsTestAppApp.swift; sourceTree = ""; }; + C427C4A42B4603F60088A488 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + C427C4A82B4603F80088A488 /* FeatureRolloutsTestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FeatureRolloutsTestApp.entitlements; sourceTree = ""; }; + C49C48412B460FC600BC1456 /* FeatureRolloutsTestApp_Crashlytics_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FeatureRolloutsTestApp_Crashlytics_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + C49C48772B4704F300BC1456 /* FeatureRolloutsTestApp_RemoteConfig_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FeatureRolloutsTestApp_RemoteConfig_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + C49C48812B4704F500BC1456 /* FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + C49C48822B47074400BC1456 /* FirebaseCrashlytics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FirebaseCrashlytics.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C49C48862B47075600BC1456 /* FirebaseRemoteConfig.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FirebaseRemoteConfig.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C49C488A2B47075C00BC1456 /* FirebaseCrashlytics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FirebaseCrashlytics.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C49C488E2B47076200BC1456 /* FirebaseRemoteConfig.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FirebaseRemoteConfig.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C49C48942B47207200BC1456 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + C49C48982B4720AE00BC1456 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + C49C489B2B4720DD00BC1456 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + C49C489D2B4722C100BC1456 /* CrashButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashButtonView.swift; sourceTree = ""; }; + C49C48A02B47261000BC1456 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + C427C49C2B4603F60088A488 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 61A5654706089C41A7398CF3 /* Pods_FeatureRolloutsTestApp_iOS.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C49C483E2B460FC600BC1456 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F07A9478976524A8264259F0 /* Pods_FeatureRolloutsTestApp_Crashlytics_iOS.framework in Frameworks */, + C49C48832B47074400BC1456 /* FirebaseCrashlytics.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C49C48722B4704F300BC1456 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AD11C57C978D52894BFDC47F /* Pods_FeatureRolloutsTestApp_RemoteConfig_iOS.framework in Frameworks */, + C49C48872B47075600BC1456 /* FirebaseRemoteConfig.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C49C487C2B4704F500BC1456 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C49C488F2B47076200BC1456 /* FirebaseRemoteConfig.framework in Frameworks */, + 848D345C8969AF72BCC0E2E4 /* Pods_FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.framework in Frameworks */, + C49C488B2B47075C00BC1456 /* FirebaseCrashlytics.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 29E7B4F9D5112B2AFBA1C6F8 /* Pods */ = { + isa = PBXGroup; + children = ( + 2842D338F32EE531C752262E /* Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.debug.xcconfig */, + 10710CAF870FA7E8D1ABF94C /* Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.release.xcconfig */, + 025F972344BB0B489CC052D6 /* Pods-FeatureRolloutsTestApp_Crashlytics_iOS.debug.xcconfig */, + 8BA72854B19D7A9D9BE15E1D /* Pods-FeatureRolloutsTestApp_Crashlytics_iOS.release.xcconfig */, + AF260B513E38B2528E7B13CC /* Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.debug.xcconfig */, + 30BB126FCB8D5F53B5795500 /* Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.release.xcconfig */, + 6D30A6D1F2CE622B6D5D563F /* Pods-FeatureRolloutsTestApp_iOS.debug.xcconfig */, + 2D15DD53784CDDE94D00AB02 /* Pods-FeatureRolloutsTestApp_iOS.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 4D9F4C8E7175D4479AD28BAC /* Frameworks */ = { + isa = PBXGroup; + children = ( + C49C488E2B47076200BC1456 /* FirebaseRemoteConfig.framework */, + C49C488A2B47075C00BC1456 /* FirebaseCrashlytics.framework */, + C49C48862B47075600BC1456 /* FirebaseRemoteConfig.framework */, + C49C48822B47074400BC1456 /* FirebaseCrashlytics.framework */, + 1729B0ED2CACB9C5A62A6F8C /* Pods_FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.framework */, + 5472955122D0CE0A8A3CE4D5 /* Pods_FeatureRolloutsTestApp_Crashlytics_iOS.framework */, + 4E60230146BDE14D306856CB /* Pods_FeatureRolloutsTestApp_RemoteConfig_iOS.framework */, + 2FE5945D035CAAB3297D2CAC /* Pods_FeatureRolloutsTestApp_iOS.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + C427C4962B4603F60088A488 = { + isa = PBXGroup; + children = ( + C49C489A2B4720C700BC1456 /* FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS */, + C49C48972B47208B00BC1456 /* FeatureRolloutsTestApp_RemoteConfig_iOS */, + C49C48932B47205400BC1456 /* FeatureRolloutsTestApp_Crashlytics_iOS */, + C427C4A12B4603F60088A488 /* FeatureRolloutsTestApp */, + C49C486B2B47048000BC1456 /* Shared */, + C427C4A02B4603F60088A488 /* Products */, + 29E7B4F9D5112B2AFBA1C6F8 /* Pods */, + 4D9F4C8E7175D4479AD28BAC /* Frameworks */, + ); + sourceTree = ""; + }; + C427C4A02B4603F60088A488 /* Products */ = { + isa = PBXGroup; + children = ( + C427C49F2B4603F60088A488 /* FeatureRolloutsTestApp_iOS.app */, + C49C48412B460FC600BC1456 /* FeatureRolloutsTestApp_Crashlytics_iOS.app */, + C49C48772B4704F300BC1456 /* FeatureRolloutsTestApp_RemoteConfig_iOS.app */, + C49C48812B4704F500BC1456 /* FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.app */, + ); + name = Products; + sourceTree = ""; + }; + C427C4A12B4603F60088A488 /* FeatureRolloutsTestApp */ = { + isa = PBXGroup; + children = ( + C427C4A42B4603F60088A488 /* ContentView.swift */, + C427C4A82B4603F80088A488 /* FeatureRolloutsTestApp.entitlements */, + ); + path = FeatureRolloutsTestApp; + sourceTree = ""; + }; + C49C486B2B47048000BC1456 /* Shared */ = { + isa = PBXGroup; + children = ( + C49C48A02B47261000BC1456 /* GoogleService-Info.plist */, + C427C4A22B4603F60088A488 /* FeatureRolloutsTestAppApp.swift */, + 951D70142B71AD9A00BE7EED /* RemoteConfigButtonView.swift */, + C49C489D2B4722C100BC1456 /* CrashButtonView.swift */, + ); + path = Shared; + sourceTree = ""; + }; + C49C48932B47205400BC1456 /* FeatureRolloutsTestApp_Crashlytics_iOS */ = { + isa = PBXGroup; + children = ( + C49C48942B47207200BC1456 /* ContentView.swift */, + ); + path = FeatureRolloutsTestApp_Crashlytics_iOS; + sourceTree = ""; + }; + C49C48972B47208B00BC1456 /* FeatureRolloutsTestApp_RemoteConfig_iOS */ = { + isa = PBXGroup; + children = ( + C49C48982B4720AE00BC1456 /* ContentView.swift */, + ); + path = FeatureRolloutsTestApp_RemoteConfig_iOS; + sourceTree = ""; + }; + C49C489A2B4720C700BC1456 /* FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS */ = { + isa = PBXGroup; + children = ( + C49C489B2B4720DD00BC1456 /* ContentView.swift */, + ); + path = FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + C427C49E2B4603F60088A488 /* FeatureRolloutsTestApp_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = C427C4C42B4603F80088A488 /* Build configuration list for PBXNativeTarget "FeatureRolloutsTestApp_iOS" */; + buildPhases = ( + 1C5C78EEA66C3693218AA186 /* [CP] Check Pods Manifest.lock */, + C427C49B2B4603F60088A488 /* Sources */, + C427C49C2B4603F60088A488 /* Frameworks */, + C427C49D2B4603F60088A488 /* Resources */, + 3FCED6F36D70B363DD56F3FB /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FeatureRolloutsTestApp_iOS; + productName = FeatureRolloutsTestApp; + productReference = C427C49F2B4603F60088A488 /* FeatureRolloutsTestApp_iOS.app */; + productType = "com.apple.product-type.application"; + }; + C49C48402B460FC600BC1456 /* FeatureRolloutsTestApp_Crashlytics_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = C49C48622B460FC800BC1456 /* Build configuration list for PBXNativeTarget "FeatureRolloutsTestApp_Crashlytics_iOS" */; + buildPhases = ( + E894A1751ED4EBA12174B475 /* [CP] Check Pods Manifest.lock */, + C49C483D2B460FC600BC1456 /* Sources */, + C49C483E2B460FC600BC1456 /* Frameworks */, + C49C483F2B460FC600BC1456 /* Resources */, + 8809A9DD2155751AF47F697B /* [CP] Embed Pods Frameworks */, + C49C48852B47074400BC1456 /* Embed Frameworks */, + C49C48A72B47285600BC1456 /* Crashlytics run script */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FeatureRolloutsTestApp_Crashlytics_iOS; + productName = FeatureRolloutsTestApp_Crashlytics_iOS; + productReference = C49C48412B460FC600BC1456 /* FeatureRolloutsTestApp_Crashlytics_iOS.app */; + productType = "com.apple.product-type.application"; + }; + C49C486E2B4704F300BC1456 /* FeatureRolloutsTestApp_RemoteConfig_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = C49C48742B4704F300BC1456 /* Build configuration list for PBXNativeTarget "FeatureRolloutsTestApp_RemoteConfig_iOS" */; + buildPhases = ( + 2E2005BA5C63DC9C5F6B0B5C /* [CP] Check Pods Manifest.lock */, + C49C486F2B4704F300BC1456 /* Sources */, + C49C48722B4704F300BC1456 /* Frameworks */, + C49C48732B4704F300BC1456 /* Resources */, + AE72CFC82C05D96F24F22349 /* [CP] Embed Pods Frameworks */, + C49C48892B47075600BC1456 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FeatureRolloutsTestApp_RemoteConfig_iOS; + productName = FeatureRolloutsTestApp_Crashlytics_iOS; + productReference = C49C48772B4704F300BC1456 /* FeatureRolloutsTestApp_RemoteConfig_iOS.app */; + productType = "com.apple.product-type.application"; + }; + C49C48782B4704F500BC1456 /* FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = C49C487E2B4704F500BC1456 /* Build configuration list for PBXNativeTarget "FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS" */; + buildPhases = ( + 1D1A7736C1202CF5AE3E74DF /* [CP] Check Pods Manifest.lock */, + C49C48792B4704F500BC1456 /* Sources */, + C49C487C2B4704F500BC1456 /* Frameworks */, + C49C487D2B4704F500BC1456 /* Resources */, + 1AFC789ACC0369540ADCC334 /* [CP] Embed Pods Frameworks */, + C49C488D2B47075C00BC1456 /* Embed Frameworks */, + C49C48A52B47279000BC1456 /* Crashlytics run script */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS; + productName = FeatureRolloutsTestApp_Crashlytics_iOS; + productReference = C49C48812B4704F500BC1456 /* FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + C427C4972B4603F60088A488 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1500; + TargetAttributes = { + C427C49E2B4603F60088A488 = { + CreatedOnToolsVersion = 15.0; + }; + C49C48402B460FC600BC1456 = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = C427C49A2B4603F60088A488 /* Build configuration list for PBXProject "FeatureRolloutsTestApp" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = C427C4962B4603F60088A488; + productRefGroup = C427C4A02B4603F60088A488 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + C49C48782B4704F500BC1456 /* FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS */, + C49C48402B460FC600BC1456 /* FeatureRolloutsTestApp_Crashlytics_iOS */, + C49C486E2B4704F300BC1456 /* FeatureRolloutsTestApp_RemoteConfig_iOS */, + C427C49E2B4603F60088A488 /* FeatureRolloutsTestApp_iOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + C427C49D2B4603F60088A488 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C49C48A12B47261000BC1456 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C49C483F2B460FC600BC1456 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C49C48A22B47261000BC1456 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C49C48732B4704F300BC1456 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C49C48A32B47261000BC1456 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C49C487D2B4704F500BC1456 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C49C48A42B47261000BC1456 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1AFC789ACC0369540ADCC334 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 1C5C78EEA66C3693218AA186 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-FeatureRolloutsTestApp_iOS-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 1D1A7736C1202CF5AE3E74DF /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 2E2005BA5C63DC9C5F6B0B5C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3FCED6F36D70B363DD56F3FB /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_iOS/Pods-FeatureRolloutsTestApp_iOS-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_iOS/Pods-FeatureRolloutsTestApp_iOS-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_iOS/Pods-FeatureRolloutsTestApp_iOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 8809A9DD2155751AF47F697B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_Crashlytics_iOS/Pods-FeatureRolloutsTestApp_Crashlytics_iOS-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_Crashlytics_iOS/Pods-FeatureRolloutsTestApp_Crashlytics_iOS-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_Crashlytics_iOS/Pods-FeatureRolloutsTestApp_Crashlytics_iOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + AE72CFC82C05D96F24F22349 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + C49C48A52B47279000BC1456 /* Crashlytics run script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${BUILD_NAME}", + "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + ); + name = "Crashlytics run script"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n${PODS_ROOT}/../../../../Crashlytics/run\n"; + }; + C49C48A72B47285600BC1456 /* Crashlytics run script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${BUILD_DIR%Build/*}SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run", + "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + ); + name = "Crashlytics run script"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n${PODS_ROOT}/../../../../Crashlytics/run\n"; + }; + E894A1751ED4EBA12174B475 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-FeatureRolloutsTestApp_Crashlytics_iOS-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + C427C49B2B4603F60088A488 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C427C4A52B4603F60088A488 /* ContentView.swift in Sources */, + C427C4A32B4603F60088A488 /* FeatureRolloutsTestAppApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C49C483D2B460FC600BC1456 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C49C489F2B47233000BC1456 /* CrashButtonView.swift in Sources */, + C49C486C2B4704D900BC1456 /* FeatureRolloutsTestAppApp.swift in Sources */, + C49C48952B47207200BC1456 /* ContentView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C49C486F2B4704F300BC1456 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C49C48702B4704F300BC1456 /* FeatureRolloutsTestAppApp.swift in Sources */, + 951D70162B71AD9B00BE7EED /* RemoteConfigButtonView.swift in Sources */, + C49C48992B4720AE00BC1456 /* ContentView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C49C48792B4704F500BC1456 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 951D70152B71AD9B00BE7EED /* RemoteConfigButtonView.swift in Sources */, + C49C487A2B4704F500BC1456 /* FeatureRolloutsTestAppApp.swift in Sources */, + C49C489E2B4722C100BC1456 /* CrashButtonView.swift in Sources */, + C49C489C2B4720DD00BC1456 /* ContentView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + C427C4C22B4603F80088A488 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + C427C4C32B4603F80088A488 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + C427C4C52B4603F80088A488 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6D30A6D1F2CE622B6D5D563F /* Pods-FeatureRolloutsTestApp_iOS.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.firebase.config.featureRolloutsTestApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + C427C4C62B4603F80088A488 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2D15DD53784CDDE94D00AB02 /* Pods-FeatureRolloutsTestApp_iOS.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.firebase.config.featureRolloutsTestApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + C49C48632B460FC800BC1456 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 025F972344BB0B489CC052D6 /* Pods-FeatureRolloutsTestApp_Crashlytics_iOS.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.firebase.config.featureRolloutsTestApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + C49C48642B460FC800BC1456 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8BA72854B19D7A9D9BE15E1D /* Pods-FeatureRolloutsTestApp_Crashlytics_iOS.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.firebase.config.featureRolloutsTestApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + C49C48752B4704F300BC1456 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AF260B513E38B2528E7B13CC /* Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.firebase.config.featureRolloutsTestApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + C49C48762B4704F300BC1456 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 30BB126FCB8D5F53B5795500 /* Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.firebase.config.featureRolloutsTestApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + C49C487F2B4704F500BC1456 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2842D338F32EE531C752262E /* Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.firebase.config.featureRolloutsTestApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + C49C48802B4704F500BC1456 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 10710CAF870FA7E8D1ABF94C /* Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.firebase.config.featureRolloutsTestApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + C427C49A2B4603F60088A488 /* Build configuration list for PBXProject "FeatureRolloutsTestApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C427C4C22B4603F80088A488 /* Debug */, + C427C4C32B4603F80088A488 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C427C4C42B4603F80088A488 /* Build configuration list for PBXNativeTarget "FeatureRolloutsTestApp_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C427C4C52B4603F80088A488 /* Debug */, + C427C4C62B4603F80088A488 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C49C48622B460FC800BC1456 /* Build configuration list for PBXNativeTarget "FeatureRolloutsTestApp_Crashlytics_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C49C48632B460FC800BC1456 /* Debug */, + C49C48642B460FC800BC1456 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C49C48742B4704F300BC1456 /* Build configuration list for PBXNativeTarget "FeatureRolloutsTestApp_RemoteConfig_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C49C48752B4704F300BC1456 /* Debug */, + C49C48762B4704F300BC1456 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C49C487E2B4704F500BC1456 /* Build configuration list for PBXNativeTarget "FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C49C487F2B4704F500BC1456 /* Debug */, + C49C48802B4704F500BC1456 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = C427C4972B4603F60088A488 /* Project object */; +} diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp/ContentView.swift b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp/ContentView.swift new file mode 100644 index 00000000000..cd875f49230 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp/ContentView.swift @@ -0,0 +1,29 @@ +// +// 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 SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp/FeatureRolloutsTestApp.entitlements b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp/FeatureRolloutsTestApp.entitlements new file mode 100644 index 00000000000..f2ef3ae0265 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp/FeatureRolloutsTestApp.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/ContentView.swift b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/ContentView.swift new file mode 100644 index 00000000000..ac68e43b8a8 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/ContentView.swift @@ -0,0 +1,27 @@ +// +// 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 Foundation +import SwiftUI + +struct ContentView: View { + var body: some View { + CrashButtonView() + .padding() + RemoteConfigButtonView() + .padding() + } +} diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_Crashlytics_iOS/ContentView.swift b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_Crashlytics_iOS/ContentView.swift new file mode 100644 index 00000000000..acb951d35a1 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_Crashlytics_iOS/ContentView.swift @@ -0,0 +1,26 @@ +// +// 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 FirebaseCrashlytics +import Foundation +import SwiftUI + +struct ContentView: View { + var body: some View { + CrashButtonView() + .padding() + } +} diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_RemoteConfig_iOS/ContentView.swift b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_RemoteConfig_iOS/ContentView.swift new file mode 100644 index 00000000000..51e437b030a --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_RemoteConfig_iOS/ContentView.swift @@ -0,0 +1,25 @@ +// +// 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 Foundation +import SwiftUI + +struct ContentView: View { + var body: some View { + RemoteConfigButtonView() + .padding() + } +} diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Podfile b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Podfile new file mode 100644 index 00000000000..975c45eaa98 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Podfile @@ -0,0 +1,52 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +def shared_pods + pod 'FirebaseCore', :path => '../../../' + pod 'FirebaseInstallations', :path => '../../../' + pod 'FirebaseCoreInternal', :path => '../../../' + pod 'FirebaseCoreExtension', :path => '../../../' + pod 'FirebaseRemoteConfigInterop', :path => '../../../' + pod 'FirebasePerformance', :path => '../../../' +end + +target 'FeatureRolloutsTestApp_iOS' do + platform :ios, '11.0' + + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + shared_pods +end + +target 'FeatureRolloutsTestApp_Crashlytics_iOS' do + platform :ios, '11.0' + + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + shared_pods + pod 'FirebaseCrashlytics', :path => '../../../' +end + +target 'FeatureRolloutsTestApp_RemoteConfig_iOS' do + platform :ios, '11.0' + + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + shared_pods + pod 'FirebaseRemoteConfig', :path => '../../../' +end + +target 'FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS' do + platform :ios, '11.0' + + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + shared_pods + pod 'FirebaseCrashlytics', :path => '../../../' + pod 'FirebaseRemoteConfig', :path => '../../../' +end + diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/CrashButtonView.swift b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/CrashButtonView.swift new file mode 100644 index 00000000000..4fb004e196c --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/CrashButtonView.swift @@ -0,0 +1,62 @@ +// +// 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 FirebaseCrashlytics +import Foundation +import SwiftUI + +struct CrashButtonView: View { + var body: some View { + var counter = 0 + + NavigationView { + VStack( + alignment: .leading, + spacing: 10 + ) { + Button(action: { + Crashlytics.crashlytics().setUserID("ThisIsABot") + }) { + Text("Set User Id") + } + + Button(action: { + assertionFailure("Throw a Crash") + }) { + Text("Crash") + } + + Button(action: { + Crashlytics.crashlytics().record(error: NSError( + domain: "This is a test non-fatal", + code: 400 + )) + }) { + Text("Record Non-fatal event") + } + + Button(action: { + Crashlytics.crashlytics().setCustomValue(counter, forKey: "counter " + String(counter)) + let i = counter + counter = i + 1 + }) { + Text("Set custom key") + } + } + .navigationTitle("Crashlytics Example") + } + } +} diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/FeatureRolloutsTestAppApp.swift b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/FeatureRolloutsTestAppApp.swift new file mode 100644 index 00000000000..b00e9bc6e6b --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/FeatureRolloutsTestAppApp.swift @@ -0,0 +1,31 @@ +// +// 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 FirebaseCore +import SwiftUI + +@main +struct FeatureRolloutsTestAppApp: App { + init() { + FirebaseApp.configure() + } + + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/RemoteConfigButtonView.swift b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/RemoteConfigButtonView.swift new file mode 100644 index 00000000000..1391ad16e55 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/RemoteConfigButtonView.swift @@ -0,0 +1,51 @@ +// +// 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 FirebaseRemoteConfig +import Foundation +import SwiftUI + +struct RemoteConfigButtonView: View { + @State private var turnOnRealTimeRC = false + let rc = RemoteConfig.remoteConfig() + @RemoteConfigProperty(key: "ios_rollouts", fallback: "unfetched") var iosRollouts: String + + var body: some View { + NavigationView { + VStack( + alignment: .leading, + spacing: 10 + ) { + Button(action: { + rc.fetch() + }) { + Text("Fetch") + } + Button(action: { + rc.activate() + }) { + Text("Activate") + } + Text(iosRollouts) + Toggle("Turn on RealTime RC", isOn: $turnOnRealTimeRC).toggleStyle(.button).tint(.mint) + .onChange(of: self.turnOnRealTimeRC, perform: { value in + rc.addOnConfigUpdateListener { u, e in rc.activate() } + }) + } + .navigationTitle("Remote Config Example") + } + } +} diff --git a/FirebaseRemoteConfig/Tests/Sample/Podfile b/FirebaseRemoteConfig/Tests/Sample/Podfile index 961df70b58e..bfff53e6fea 100644 --- a/FirebaseRemoteConfig/Tests/Sample/Podfile +++ b/FirebaseRemoteConfig/Tests/Sample/Podfile @@ -14,6 +14,7 @@ target 'RemoteConfigSampleApp' do pod 'FirebaseInstallations', :path => '../../../' pod 'FirebaseRemoteConfig', :path => '../../../' pod 'FirebaseABTesting', :path => '../../..' + pod 'FirebaseRemoteConfigInterop', :path => '../../..' # Pods for RemoteConfigSampleApp diff --git a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/ViewController.m b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/ViewController.m index 57c766a9035..e57cc930ea2 100644 --- a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/ViewController.m +++ b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/ViewController.m @@ -21,6 +21,7 @@ #import #import "../../../Sources/Private/FIRRemoteConfig_Private.h" #import "FRCLog.h" +@import FirebaseRemoteConfigInterop; static NSString *const FIRPerfNamespace = @"fireperf"; static NSString *const FIRDefaultFIRAppName = @"__FIRAPP_DEFAULT"; @@ -81,7 +82,8 @@ - (void)viewDidLoad { // TODO(mandard): Add support for deleting and adding namespaces in the app. self.namespacePickerData = - [[NSArray alloc] initWithObjects:FIRNamespaceGoogleMobilePlatform, FIRPerfNamespace, nil]; + [[NSArray alloc] initWithObjects:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform, + FIRPerfNamespace, nil]; self.appPickerData = [[NSArray alloc] initWithObjects:FIRDefaultFIRAppName, FIRSecondFIRAppName, nil]; self.RCInstances = [[NSMutableDictionary alloc] init]; @@ -91,7 +93,8 @@ - (void)viewDidLoad { if (!self.RCInstances[namespaceString]) { self.RCInstances[namespaceString] = [[NSMutableDictionary alloc] init]; } - if ([namespaceString isEqualToString:FIRNamespaceGoogleMobilePlatform] && + if ([namespaceString + isEqualToString:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform] && [appString isEqualToString:FIRDefaultFIRAppName]) { self.RCInstances[namespaceString][appString] = [FIRRemoteConfig remoteConfig]; } else { @@ -120,7 +123,7 @@ - (void)viewDidLoad { [alert addAction:defaultAction]; // Add realtime listener for firebase namespace - [self.RCInstances[FIRNamespaceGoogleMobilePlatform][FIRDefaultFIRAppName] + [self.RCInstances[FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform][FIRDefaultFIRAppName] addOnConfigUpdateListener:^(FIRRemoteConfigUpdate *_Nullable update, NSError *_Nullable error) { if (error != nil) { diff --git a/FirebaseRemoteConfig/Tests/SwiftUnit/RemoteConfigInteropTests.swift b/FirebaseRemoteConfig/Tests/SwiftUnit/RemoteConfigInteropTests.swift new file mode 100644 index 00000000000..d4610a03d65 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/SwiftUnit/RemoteConfigInteropTests.swift @@ -0,0 +1,65 @@ +// Copyright 2023 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 FirebaseRemoteConfigInterop +import XCTest + +class MockRCInterop: RemoteConfigInterop { + weak var subscriber: FirebaseRemoteConfigInterop.RolloutsStateSubscriber? + func registerRolloutsStateSubscriber(_ subscriber: FirebaseRemoteConfigInterop + .RolloutsStateSubscriber, + for namespace: String) { + self.subscriber = subscriber + } +} + +class MockRolloutSubscriber: RolloutsStateSubscriber { + var isSubscriberCalled = false + var rolloutsState: RolloutsState? + func rolloutsStateDidChange(_ rolloutsState: FirebaseRemoteConfigInterop.RolloutsState) { + isSubscriberCalled = true + self.rolloutsState = rolloutsState + } +} + +final class RemoteConfigInteropTests: XCTestCase { + let rollouts: RolloutsState = { + let assignment1 = RolloutAssignment( + rolloutId: "rollout_1", + variantId: "control", + templateVersion: 1, + parameterKey: "my_feature", + parameterValue: "false" + ) + let assignment2 = RolloutAssignment( + rolloutId: "rollout_2", + variantId: "enabled", + templateVersion: 123, + parameterKey: "themis_big_feature", + parameterValue: "1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + ) + let rollouts = RolloutsState(assignmentList: [assignment1, assignment2]) + return rollouts + }() + + func testRemoteConfigIntegration() throws { + let rcSubscriber = MockRolloutSubscriber() + let rcInterop = MockRCInterop() + rcInterop.registerRolloutsStateSubscriber(rcSubscriber, for: "namespace") + rcInterop.subscriber?.rolloutsStateDidChange(rollouts) + + XCTAssertTrue(rcSubscriber.isSubscriberCalled) + XCTAssertEqual(rcSubscriber.rolloutsState?.assignments.count, 2) + } +} diff --git a/FirebaseRemoteConfig/Tests/Unit/FIRRemoteConfigComponentTest.m b/FirebaseRemoteConfig/Tests/Unit/FIRRemoteConfigComponentTest.m index 077702b7b19..52d56bb3852 100644 --- a/FirebaseRemoteConfig/Tests/Unit/FIRRemoteConfigComponentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/FIRRemoteConfigComponentTest.m @@ -20,6 +20,7 @@ #import "FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h" #import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h" #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" +@import FirebaseRemoteConfigInterop; @interface FIRRemoteConfigComponentTest : XCTestCase @end @@ -31,6 +32,7 @@ - (void)tearDown { // Clear out any apps that were called with `configure`. [FIRApp resetApps]; + [FIRRemoteConfigComponent clearAllComponentInstances]; } - (void)testRCInstanceCreationAndCaching { @@ -92,7 +94,8 @@ - (void)testInitialization { } - (void)testRegistersAsLibrary { - XCTAssertEqual([FIRRemoteConfigComponent componentsToRegister].count, 1); + // Now component has two register, one is provider and another one is Interop + XCTAssertEqual([FIRRemoteConfigComponent componentsToRegister].count, 2); // Configure a test FIRApp for fetching an instance of the FIRRemoteConfigProvider. NSString *appName = [self generatedTestAppName]; @@ -101,12 +104,50 @@ - (void)testRegistersAsLibrary { // Attempt to fetch the component and verify it's a valid instance. id provider = FIR_COMPONENT(FIRRemoteConfigProvider, app.container); + id interop = FIR_COMPONENT(FIRRemoteConfigInterop, app.container); XCTAssertNotNil(provider); + XCTAssertNotNil(interop); // Ensure that the instance that comes from the container is cached. id sameProvider = FIR_COMPONENT(FIRRemoteConfigProvider, app.container); + id sameInterop = FIR_COMPONENT(FIRRemoteConfigInterop, app.container); XCTAssertNotNil(sameProvider); + XCTAssertNotNil(sameInterop); XCTAssertEqual(provider, sameProvider); + XCTAssertEqual(interop, sameInterop); + + // Dynamic typing, both prototols are refering to the same component instance + id providerID = provider; + id interopID = interop; + XCTAssertEqualObjects(providerID, interopID); +} + +- (void)testTwoAppsCreateTwoComponents { + NSString *appName = [self generatedTestAppName]; + [FIRApp configureWithName:appName options:[self fakeOptions]]; + FIRApp *app = [FIRApp appNamed:appName]; + + [FIRApp configureWithOptions:[self fakeOptions]]; + FIRApp *defaultApp = [FIRApp defaultApp]; + XCTAssertNotNil(defaultApp); + XCTAssertNotEqualObjects(app, defaultApp); + + id provider = FIR_COMPONENT(FIRRemoteConfigProvider, app.container); + id interop = FIR_COMPONENT(FIRRemoteConfigInterop, app.container); + id defaultAppProvider = + FIR_COMPONENT(FIRRemoteConfigProvider, defaultApp.container); + id defaultAppInterop = + FIR_COMPONENT(FIRRemoteConfigInterop, defaultApp.container); + + id providerID = provider; + id interopID = interop; + id defaultAppProviderID = defaultAppProvider; + id defaultAppInteropID = defaultAppInterop; + + XCTAssertEqualObjects(providerID, interopID); + XCTAssertEqualObjects(defaultAppProviderID, defaultAppInteropID); + // Check two apps get their own component to register + XCTAssertNotEqualObjects(interopID, defaultAppInteropID); } - (void)testThrowsWithEmptyGoogleAppID { diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m index d4f33bf0f71..e02f22f2454 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m @@ -24,6 +24,7 @@ #import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h" #import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" +@import FirebaseRemoteConfigInterop; @interface RCNConfigContent (Testing) - (BOOL)checkAndWaitForInitialDatabaseLoad; @@ -44,7 +45,7 @@ - (void)loadMainWithBundleIdentifier:(NSString *)bundleIdentifier dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(justSmallDelay * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ self.isLoadMainCompleted = YES; - handler(YES, nil, nil, nil); + handler(YES, nil, nil, nil, nil); }); } - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler { @@ -53,7 +54,7 @@ - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(justOtherSmallDelay * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ self.isLoadPersonalizationCompleted = YES; - handler(YES, nil, nil, nil); + handler(YES, nil, nil, nil, nil); }); } @end @@ -62,6 +63,7 @@ @interface RCNConfigContentTest : XCTestCase { NSTimeInterval _expectationTimeout; RCNConfigContent *_configContent; NSString *namespaceApp1, *namespaceApp2; + NSString *_namespaceGoogleMobilePlatform; } @end @@ -70,11 +72,12 @@ @implementation RCNConfigContentTest - (void)setUp { [super setUp]; _expectationTimeout = 1.0; + _namespaceGoogleMobilePlatform = FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform; namespaceApp1 = [NSString - stringWithFormat:@"%@:%@", FIRNamespaceGoogleMobilePlatform, RCNTestsDefaultFIRAppName]; + stringWithFormat:@"%@:%@", _namespaceGoogleMobilePlatform, RCNTestsDefaultFIRAppName]; namespaceApp2 = [NSString - stringWithFormat:@"%@:%@", FIRNamespaceGoogleMobilePlatform, RCNTestsSecondFIRAppName]; + stringWithFormat:@"%@:%@", _namespaceGoogleMobilePlatform, RCNTestsSecondFIRAppName]; _configContent = [[RCNConfigContent alloc] initWithDBManager:nil]; @@ -129,14 +132,14 @@ - (void)testUpdateConfigContentWithResponse { NSDictionary *entries = @{@"key1" : @"value1", @"key2" : @"value2"}; [configToSet setValue:entries forKey:@"entries"]; [_configContent updateConfigContentWithResponse:configToSet - forNamespace:FIRNamespaceGoogleMobilePlatform]; + forNamespace:_namespaceGoogleMobilePlatform]; NSDictionary *fetchedConfig = _configContent.fetchedConfig; - XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key1"]); - XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key1"] stringValue], + XCTAssertNotNil(fetchedConfig[_namespaceGoogleMobilePlatform][@"key1"]); + XCTAssertEqualObjects([fetchedConfig[_namespaceGoogleMobilePlatform][@"key1"] stringValue], @"value1"); - XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"]); - XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"] stringValue], + XCTAssertNotNil(fetchedConfig[_namespaceGoogleMobilePlatform][@"key2"]); + XCTAssertEqualObjects([fetchedConfig[_namespaceGoogleMobilePlatform][@"key2"] stringValue], @"value2"); } @@ -147,20 +150,20 @@ - (void)testUpdateConfigContentWithStatusUpdateWithDifferentKeys { NSDictionary *entries = @{@"key1" : @"value1"}; [configToSet setValue:entries forKey:@"entries"]; [_configContent updateConfigContentWithResponse:configToSet - forNamespace:FIRNamespaceGoogleMobilePlatform]; + forNamespace:_namespaceGoogleMobilePlatform]; configToSet = [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil]; entries = @{@"key2" : @"value2", @"key3" : @"value3"}; [configToSet setValue:entries forKey:@"entries"]; [_configContent updateConfigContentWithResponse:configToSet - forNamespace:FIRNamespaceGoogleMobilePlatform]; + forNamespace:_namespaceGoogleMobilePlatform]; NSDictionary *fetchedConfig = _configContent.fetchedConfig; - XCTAssertNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key1"]); - XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"]); - XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"] stringValue], + XCTAssertNil(fetchedConfig[_namespaceGoogleMobilePlatform][@"key1"]); + XCTAssertNotNil(fetchedConfig[_namespaceGoogleMobilePlatform][@"key2"]); + XCTAssertEqualObjects([fetchedConfig[_namespaceGoogleMobilePlatform][@"key2"] stringValue], @"value2"); - XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key3"]); - XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key3"] stringValue], + XCTAssertNotNil(fetchedConfig[_namespaceGoogleMobilePlatform][@"key3"]); + XCTAssertEqualObjects([fetchedConfig[_namespaceGoogleMobilePlatform][@"key3"] stringValue], @"value3"); } @@ -332,7 +335,9 @@ - (void)testConfigUpdate_noChange_emptyResponse { // populate fetched config NSMutableDictionary *fetchResponse = - [self createFetchResponseWithConfigEntries:@{@"key1" : @"value1"} p13nMetadata:nil]; + [self createFetchResponseWithConfigEntries:@{@"key1" : @"value1"} + p13nMetadata:nil + rolloutMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; // active config is the same as fetched config @@ -365,7 +370,8 @@ - (void)testConfigUpdate_paramAdded_returnsNewKey { // fetch response has new param NSMutableDictionary *fetchResponse = [self createFetchResponseWithConfigEntries:@{@"key1" : @"value1", newParam : @"value2"} - p13nMetadata:nil]; + p13nMetadata:nil + rolloutMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; @@ -391,7 +397,9 @@ - (void)testConfigUpdate_paramValueChanged_returnsUpdatedKey { // fetch response contains updated value NSMutableDictionary *fetchResponse = - [self createFetchResponseWithConfigEntries:@{existingParam : updatedValue} p13nMetadata:nil]; + [self createFetchResponseWithConfigEntries:@{existingParam : updatedValue} + p13nMetadata:nil + rolloutMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; @@ -417,7 +425,9 @@ - (void)testConfigUpdate_paramDeleted_returnsDeletedKey { // fetch response does not contain existing param NSMutableDictionary *fetchResponse = - [self createFetchResponseWithConfigEntries:@{newParam : value1} p13nMetadata:nil]; + [self createFetchResponseWithConfigEntries:@{newParam : value1} + p13nMetadata:nil + rolloutMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; @@ -437,7 +447,8 @@ - (void)testConfigUpdate_p13nMetadataUpdated_returnsKey { // popuate fetched config NSMutableDictionary *fetchResponse = [self createFetchResponseWithConfigEntries:@{existingParam : value1} - p13nMetadata:@{existingParam : oldMetadata}]; + p13nMetadata:@{existingParam : oldMetadata} + rolloutMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; // populate active config with the same content @@ -461,6 +472,148 @@ - (void)testConfigUpdate_p13nMetadataUpdated_returnsKey { XCTAssertTrue([[update updatedKeys] containsObject:existingParam]); } +- (void)testConfigUpdate_rolloutMetadataUpdated_returnsKey { + NSString *namespace = @"test_namespace"; + NSString *key1 = @"key1"; + NSString *key2 = @"kety2"; + NSString *value = @"value"; + NSString *rolloutId1 = @"1"; + NSString *rolloutId2 = @"2"; + NSString *variantId1 = @"A"; + NSString *variantId2 = @"B"; + NSArray *rolloutMetadata = @[ @{ + RCNFetchResponseKeyRolloutID : rolloutId1, + RCNFetchResponseKeyVariantID : variantId1, + RCNFetchResponseKeyAffectedParameterKeys : @[ key1 ] + } ]; + // Update rolltou metadata + NSArray *updatedRolloutMetadata = @[ + @{ + RCNFetchResponseKeyRolloutID : rolloutId1, + RCNFetchResponseKeyVariantID : variantId2, + RCNFetchResponseKeyAffectedParameterKeys : @[ key1 ] + }, + @{ + RCNFetchResponseKeyRolloutID : rolloutId2, + RCNFetchResponseKeyVariantID : variantId1, + RCNFetchResponseKeyAffectedParameterKeys : @[ key2 ] + }, + ]; + // Populate fetched config + NSMutableDictionary *fetchResponse = [self createFetchResponseWithConfigEntries:@{key1 : value} + p13nMetadata:nil + rolloutMetadata:rolloutMetadata]; + [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; + // populate active config with the same content + [_configContent activateRolloutMetadata:nil]; + XCTAssertEqualObjects(rolloutMetadata, _configContent.activeRolloutMetadata); + FIRRemoteConfigValue *rcValue = + [[FIRRemoteConfigValue alloc] initWithData:[value dataUsingEncoding:NSUTF8StringEncoding] + source:FIRRemoteConfigSourceRemote]; + + NSDictionary *namespaceToConfig = @{namespace : @{key1 : rcValue}}; + [_configContent copyFromDictionary:namespaceToConfig + toSource:RCNDBSourceActive + forNamespace:namespace]; + // New fetch response has updated rollout metadata + [fetchResponse setValue:updatedRolloutMetadata forKey:RCNFetchResponseKeyRolloutMetadata]; + [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; + + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; + + XCTAssertTrue([update updatedKeys].count == 2); + XCTAssertTrue([[update updatedKeys] containsObject:key1]); + XCTAssertTrue([[update updatedKeys] containsObject:key2]); +} + +- (void)testConfigUpdate_rolloutMetadataDeleted_returnsKey { + NSString *namespace = @"test_namespace"; + NSString *key1 = @"key1"; + NSString *key2 = @"key2"; + NSString *value = @"value"; + NSString *rolloutId1 = @"1"; + NSString *variantId1 = @"A"; + NSArray *rolloutMetadata = @[ @{ + RCNFetchResponseKeyRolloutID : rolloutId1, + RCNFetchResponseKeyVariantID : variantId1, + RCNFetchResponseKeyAffectedParameterKeys : @[ key1, key2 ] + } ]; + // Remove key2 from rollout metadata + NSArray *updatedRolloutMetadata = @[ @{ + RCNFetchResponseKeyRolloutID : rolloutId1, + RCNFetchResponseKeyVariantID : variantId1, + RCNFetchResponseKeyAffectedParameterKeys : @[ key1 ] + } ]; + // Populate fetched config + NSMutableDictionary *fetchResponse = + [self createFetchResponseWithConfigEntries:@{key1 : value, key2 : value} + p13nMetadata:nil + rolloutMetadata:rolloutMetadata]; + [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; + // populate active config with the same content + [_configContent activateRolloutMetadata:nil]; + XCTAssertEqualObjects(rolloutMetadata, _configContent.activeRolloutMetadata); + FIRRemoteConfigValue *rcValue = + [[FIRRemoteConfigValue alloc] initWithData:[value dataUsingEncoding:NSUTF8StringEncoding] + source:FIRRemoteConfigSourceRemote]; + + NSDictionary *namespaceToConfig = @{namespace : @{key1 : rcValue, key2 : rcValue}}; + [_configContent copyFromDictionary:namespaceToConfig + toSource:RCNDBSourceActive + forNamespace:namespace]; + // New fetch response has updated rollout metadata + [fetchResponse setValue:updatedRolloutMetadata forKey:RCNFetchResponseKeyRolloutMetadata]; + [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; + + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; + + XCTAssertTrue([update updatedKeys].count == 1); + XCTAssertTrue([[update updatedKeys] containsObject:key2]); +} + +- (void)testConfigUpdate_rolloutMetadataDeletedAll_returnsKey { + NSString *namespace = @"test_namespace"; + NSString *key = @"key"; + NSString *value = @"value"; + NSString *rolloutId1 = @"1"; + NSString *variantId1 = @"A"; + NSArray *rolloutMetadata = @[ @{ + RCNFetchResponseKeyRolloutID : rolloutId1, + RCNFetchResponseKeyVariantID : variantId1, + RCNFetchResponseKeyAffectedParameterKeys : @[ key ] + } ]; + // Populate fetched config + NSMutableDictionary *fetchResponse = [self createFetchResponseWithConfigEntries:@{key : value} + p13nMetadata:nil + rolloutMetadata:rolloutMetadata]; + [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; + // populate active config with the same content + [_configContent activateRolloutMetadata:nil]; + XCTAssertEqualObjects(rolloutMetadata, _configContent.activeRolloutMetadata); + FIRRemoteConfigValue *rcValue = + [[FIRRemoteConfigValue alloc] initWithData:[value dataUsingEncoding:NSUTF8StringEncoding] + source:FIRRemoteConfigSourceRemote]; + + NSDictionary *namespaceToConfig = @{namespace : @{key : rcValue}}; + [_configContent copyFromDictionary:namespaceToConfig + toSource:RCNDBSourceActive + forNamespace:namespace]; + + // New fetch response has updated rollout metadata + NSMutableDictionary *updateFetchResponse = + [self createFetchResponseWithConfigEntries:@{key : value} + p13nMetadata:nil + rolloutMetadata:nil]; + [_configContent updateConfigContentWithResponse:updateFetchResponse forNamespace:namespace]; + + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; + [_configContent activateRolloutMetadata:nil]; + + XCTAssertTrue([update updatedKeys].count == 1); + XCTAssertTrue([[update updatedKeys] containsObject:key]); + XCTAssertTrue(_configContent.activeRolloutMetadata.count == 0); +} + - (void)testConfigUpdate_valueSourceChanged_returnsKey { NSString *namespace = @"test_namespace"; NSString *existingParam = @"key1"; @@ -477,7 +630,9 @@ - (void)testConfigUpdate_valueSourceChanged_returnsKey { // fetch response contains same key->value NSMutableDictionary *fetchResponse = - [self createFetchResponseWithConfigEntries:@{existingParam : value1} p13nMetadata:nil]; + [self createFetchResponseWithConfigEntries:@{existingParam : value1} + p13nMetadata:nil + rolloutMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; @@ -489,14 +644,18 @@ - (void)testConfigUpdate_valueSourceChanged_returnsKey { #pragma mark - Test Helpers - (NSMutableDictionary *)createFetchResponseWithConfigEntries:(NSDictionary *)config - p13nMetadata:(NSDictionary *)metadata { + p13nMetadata:(NSDictionary *)p13nMetadata + rolloutMetadata:(NSArray *)rolloutMetadata { NSMutableDictionary *fetchResponse = [[NSMutableDictionary alloc] initWithObjectsAndKeys:RCNFetchResponseKeyStateUpdate, RCNFetchResponseKeyState, nil]; if (config) { [fetchResponse setValue:config forKey:RCNFetchResponseKeyEntries]; } - if (metadata) { - [fetchResponse setValue:metadata forKey:RCNFetchResponseKeyPersonalizationMetadata]; + if (p13nMetadata) { + [fetchResponse setValue:p13nMetadata forKey:RCNFetchResponseKeyPersonalizationMetadata]; + } + if (rolloutMetadata) { + [fetchResponse setValue:rolloutMetadata forKey:RCNFetchResponseKeyRolloutMetadata]; } return fetchResponse; } diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m index 23705be1abf..773af690935 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m @@ -83,8 +83,8 @@ - (void)testV1NamespaceMigrationToV2Namespace { BOOL loadSuccess, NSDictionary *> *fetchedConfig, NSDictionary *> *activeConfig, - NSDictionary *> - *defaultConfig) { + NSDictionary *> *defaultConfig, + NSDictionary *unusedRolloutMetadata) { XCTAssertTrue(loadSuccess); NSString *fullyQualifiedNamespace = [NSString stringWithFormat:@"%@:%@", namespace_p, kFIRDefaultAppName]; @@ -125,18 +125,19 @@ - (void)testWriteAndLoadMainTableResult { XCTAssertTrue(success); if (count == 100) { // check DB read correctly - [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier - completionHandler:^(BOOL success, NSDictionary *fetchedConfig, - NSDictionary *activeConfig, - NSDictionary *defaultConfig) { - NSMutableDictionary *res = [fetchedConfig mutableCopy]; - XCTAssertTrue(success); - FIRRemoteConfigValue *value = res[namespace_p][@"key100"]; - XCTAssertEqualObjects(value.stringValue, @"value100"); - if (success) { - [loadConfigContentExpectation fulfill]; - } - }]; + [self->_DBManager + loadMainWithBundleIdentifier:bundleIdentifier + completionHandler:^(BOOL success, NSDictionary *fetchedConfig, + NSDictionary *activeConfig, NSDictionary *defaultConfig, + NSDictionary *unusedRolloutMetadata) { + NSMutableDictionary *res = [fetchedConfig mutableCopy]; + XCTAssertTrue(success); + FIRRemoteConfigValue *value = res[namespace_p][@"key100"]; + XCTAssertEqualObjects(value.stringValue, @"value100"); + if (success) { + [loadConfigContentExpectation fulfill]; + } + }]; } }; NSString *value = [NSString stringWithFormat:@"value%d", i]; @@ -382,7 +383,8 @@ - (void)testDeleteParamAndLoadMainTable { [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier completionHandler:^(BOOL success, NSDictionary *fetchedConfig, - NSDictionary *activeConfig, NSDictionary *defaultConfig) { + NSDictionary *activeConfig, NSDictionary *defaultConfig, + NSDictionary *unusedRolloutMetadata) { NSMutableDictionary *res = [activeConfig mutableCopy]; XCTAssertTrue(success); FIRRemoteConfigValue *value = res[namespaceToDelete][@"keyToDelete"]; @@ -403,7 +405,8 @@ - (void)testDeleteParamAndLoadMainTable { [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier completionHandler:^(BOOL success, NSDictionary *fetchedConfig, - NSDictionary *activeConfig, NSDictionary *defaultConfig) { + NSDictionary *activeConfig, NSDictionary *defaultConfig, + NSDictionary *unusedRolloutMetadata) { NSMutableDictionary *res = [activeConfig mutableCopy]; XCTAssertTrue(success); FIRRemoteConfigValue *value2 = res[namespaceToKeep][@"keyToRetain"]; @@ -587,6 +590,136 @@ - (void)testWriteAndLoadMetadataMultipleTimes { [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; } +- (void)testWriteAndLoadFetchedAndActiveRollout { + XCTestExpectation *writeAndLoadFetchedRolloutExpectation = + [self expectationWithDescription:@"Write and load rollout in database successfully"]; + + NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier; + + NSArray *fetchedRollout = @[ + @{ + @"rollout_id" : @"1", + @"variant_id" : @"B", + @"affected_parameter_keys" : @[ @"key_1", @"key_2" ] + }, + @{ + @"rollout_id" : @"2", + @"variant_id" : @"1", + @"affected_parameter_keys" : @[ @"key_1", @"key_3" ] + } + ]; + + NSArray *activeRollout = @[ + @{ + @"rollout_id" : @"1", + @"variant_id" : @"B", + @"affected_parameter_keys" : @[ @"key_1", @"key_2" ] + }, + @{ + @"rollout_id" : @"3", + @"variant_id" : @"a", + @"affected_parameter_keys" : @[ @"key_1", @"key_3" ] + } + ]; + + RCNDBCompletion writeRolloutCompletion = ^(BOOL success, NSDictionary *result) { + XCTAssertTrue(success); + RCNDBLoadCompletion loadCompletion = ^( + BOOL success, NSDictionary *unusedFetchedConfig, NSDictionary *unusedActiveConfig, + NSDictionary *unusedDefaultConfig, NSDictionary *rolloutMetadata) { + XCTAssertTrue(success); + XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]); + XCTAssertEqualObjects(fetchedRollout, rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]); + XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyActiveMetadata]); + XCTAssertEqualObjects(activeRollout, rolloutMetadata[@RCNRolloutTableKeyActiveMetadata]); + + [writeAndLoadFetchedRolloutExpectation fulfill]; + }; + [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier + completionHandler:loadCompletion]; + }; + [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyFetchedMetadata + value:fetchedRollout + completionHandler:nil]; + [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyActiveMetadata + value:activeRollout + completionHandler:writeRolloutCompletion]; + + [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; +} + +- (void)testUpdateAndLoadRollout { + XCTestExpectation *updateAndLoadFetchedRolloutExpectation = + [self expectationWithDescription:@"Update and load rollout in database successfully"]; + + NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier; + + NSArray *fetchedRollout = @[ @{ + @"rollout_id" : @"1", + @"variant_id" : @"B", + @"affected_parameter_keys" : @[ @"key_1", @"key_2" ] + } ]; + + NSArray *updatedFetchedRollout = @[ + @{ + @"rollout_id" : @"1", + @"variant_id" : @"B", + @"affected_parameter_keys" : @[ @"key_1", @"key_2" ] + }, + @{ + @"rollout_id" : @"2", + @"variant_id" : @"1", + @"affected_parameter_keys" : @[ @"key_1", @"key_3" ] + } + ]; + + RCNDBCompletion writeRolloutCompletion = ^(BOOL success, NSDictionary *result) { + XCTAssertTrue(success); + RCNDBLoadCompletion loadCompletion = + ^(BOOL success, NSDictionary *unusedFetchedConfig, NSDictionary *unusedActiveConfig, + NSDictionary *unusedDefaultConfig, NSDictionary *rolloutMetadata) { + XCTAssertTrue(success); + XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]); + XCTAssertEqualObjects(updatedFetchedRollout, + rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]); + + [updateAndLoadFetchedRolloutExpectation fulfill]; + }; + [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier + completionHandler:loadCompletion]; + }; + [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyFetchedMetadata + value:fetchedRollout + completionHandler:nil]; + [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyFetchedMetadata + value:updatedFetchedRollout + completionHandler:writeRolloutCompletion]; + + [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; +} +- (void)testLoadEmptyRollout { + XCTestExpectation *updateAndLoadFetchedRolloutExpectation = + [self expectationWithDescription:@"Load empty rollout in database successfully"]; + + NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier; + + NSArray *emptyResult = [[NSArray alloc] init]; + + RCNDBLoadCompletion loadCompletion = + ^(BOOL success, NSDictionary *unusedFetchedConfig, NSDictionary *unusedActiveConfig, + NSDictionary *unusedDefaultConfig, NSDictionary *rolloutMetadata) { + XCTAssertTrue(success); + XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]); + XCTAssertEqualObjects(emptyResult, rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]); + XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyActiveMetadata]); + XCTAssertEqualObjects(emptyResult, rolloutMetadata[@RCNRolloutTableKeyActiveMetadata]); + + [updateAndLoadFetchedRolloutExpectation fulfill]; + }; + [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier completionHandler:loadCompletion]; + [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; +} + - (void)testUpdateAndloadLastFetchStatus { XCTestExpectation *updateAndLoadMetadataExpectation = [self expectationWithDescription:@"Update and load last fetch status in database successfully."]; diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigTest.m index 2a5bd7c67c9..9acb62e0717 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigTest.m @@ -23,6 +23,7 @@ #import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h" #import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" +@import FirebaseRemoteConfigInterop; static NSString *const RCNFakeSenderID = @"855865492447"; static NSString *const RCNFakeToken = @"ctToAh17Exk:" @@ -48,6 +49,7 @@ @interface RCNConfigTest : XCTestCase { RCNConfigExperiment *_experiment; RCNConfigFetch *_configFetch; dispatch_queue_t _queue; + NSString *_namespaceGoogleMobilePlatform; } @end @@ -66,9 +68,10 @@ - (void)setUp { experiment:_experiment queue:_queue]; _configFetch = OCMPartialMock(fetcher); + _namespaceGoogleMobilePlatform = FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform; // Fake a response with a default namespace and a custom namespace. NSDictionary *namespaceToConfig = @{ - FIRNamespaceGoogleMobilePlatform : @{@"key1" : @"value1", @"key2" : @"value2"}, + _namespaceGoogleMobilePlatform : @{@"key1" : @"value1", @"key2" : @"value2"}, FIRNamespaceGooglePlayPlatform : @{@"playerID" : @"36", @"gameLevel" : @"87"}, }; _response = @@ -149,19 +152,19 @@ - (void)testFetchAllConfigsSuccessfully { XCTAssertNotNil(result); [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key1" value:@"value1"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key2" value:@"value2"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGooglePlayPlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"playerID" value:@"36"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGooglePlayPlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"gameLevel" value:@"87"]; XCTAssertEqual(self->_settings.expirationInSeconds, 43200, @@ -200,11 +203,11 @@ - (void)testFetchConfigInCachedResults { NSDictionary *result = self->_configContent.fetchedConfig; XCTAssertNotNil(result); [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key1" value:@"value1"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key2" value:@"value2"]; @@ -246,19 +249,19 @@ - (void)testFetchFailedWithCachedResult { XCTAssertNotNil(result); [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key1" value:@"value1"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key2" value:@"value2"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGooglePlayPlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"playerID" value:@"36"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGooglePlayPlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"gameLevel" value:@"87"]; @@ -340,19 +343,19 @@ - (void)testFetchThrottledWithStaledCachedResult { NSDictionary *result = self->_configContent.fetchedConfig; XCTAssertNotNil(result); [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key1" value:@"value1"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key2" value:@"value2"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGooglePlayPlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"playerID" value:@"36"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGooglePlayPlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"gameLevel" value:@"87"]; XCTAssertEqual( diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNInstanceIDTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNInstanceIDTest.m index 5d1b28fb61d..cbbcd0a91bd 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNInstanceIDTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNInstanceIDTest.m @@ -29,6 +29,7 @@ #import #import "FirebaseCore/Extension/FirebaseCoreInternal.h" #import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h" +@import FirebaseRemoteConfigInterop; @interface RCNConfigFetch (ForTest) - (instancetype)initWithContent:(RCNConfigContent *)content @@ -136,7 +137,8 @@ - (void)setUpConfigMock { case RCNTestRCInstanceSecondApp: currentAppName = RCNTestsSecondFIRAppName; currentOptions = [self secondAppOptions]; - currentNamespace = FIRNamespaceGoogleMobilePlatform; + currentNamespace = FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform; + ; break; case RCNTestRCInstanceDefault: default: diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m index e1a23b5a695..e02b8ecaabf 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m @@ -18,6 +18,7 @@ #import #import +#import "FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h" #import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h" #import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h" #import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h" @@ -31,6 +32,9 @@ #import #import "FirebaseCore/Extension/FirebaseCoreInternal.h" +@import FirebaseRemoteConfigInterop; + +@protocol FIRRolloutsStateSubscriber; @interface RCNConfigFetch (ForTest) - (instancetype)initWithContent:(RCNConfigContent *)content @@ -130,6 +134,7 @@ @interface RCNRemoteConfigTest : XCTestCase { NSTimeInterval _checkCompletionTimeout; NSMutableArray *_configInstances; NSMutableArray *> *_entries; + NSArray *_rolloutMetadata; NSMutableArray *> *_response; NSMutableArray *_responseData; NSMutableArray *_URLResponse; @@ -145,6 +150,7 @@ @interface RCNRemoteConfigTest : XCTestCase { NSString *_fullyQualifiedNamespace; RCNConfigSettings *_settings; dispatch_queue_t _queue; + NSString *_namespaceGoogleMobilePlatform; } @end @@ -180,6 +186,7 @@ - (void)setUp { _URLResponse = [[NSMutableArray alloc] initWithCapacity:3]; _configFetch = [[NSMutableArray alloc] initWithCapacity:3]; _configRealtime = [[NSMutableArray alloc] initWithCapacity:3]; + _namespaceGoogleMobilePlatform = FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform; // Populate the default, second app, second namespace instances. for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { @@ -204,7 +211,7 @@ - (void)setUp { case RCNTestRCInstanceSecondApp: currentAppName = RCNTestsSecondFIRAppName; currentOptions = [self secondAppOptions]; - currentNamespace = FIRNamespaceGoogleMobilePlatform; + currentNamespace = _namespaceGoogleMobilePlatform; break; case RCNTestRCInstanceDefault: default: @@ -259,7 +266,17 @@ __unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status, updateCompletionHandler:nil]; }); - _response[i] = @{@"state" : @"UPDATE", @"entries" : _entries[i]}; + _rolloutMetadata = @[ @{ + RCNFetchResponseKeyRolloutID : @"1", + RCNFetchResponseKeyVariantID : @"0", + RCNFetchResponseKeyAffectedParameterKeys : @[ _entries[i].allKeys[0] ] + } ]; + + _response[i] = @{ + @"state" : @"UPDATE", + @"entries" : _entries[i], + RCNFetchResponseKeyRolloutMetadata : _rolloutMetadata + }; _responseData[i] = [NSJSONSerialization dataWithJSONObject:_response[i] options:0 error:nil]; @@ -286,6 +303,7 @@ __unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status, - (void)tearDown { [_DBManager removeDatabaseOnDatabaseQueueAtPath:_DBPath]; + [FIRRemoteConfigComponent clearAllComponentInstances]; [[NSUserDefaults standardUserDefaults] removePersistentDomainForName:_userDefaultsSuiteName]; [_DBManagerMock stopMocking]; _DBManagerMock = nil; @@ -594,7 +612,7 @@ - (void)testFetchConfigsFailed { case RCNTestRCInstanceSecondApp: currentAppName = RCNTestsSecondFIRAppName; currentOptions = [self secondAppOptions]; - currentNamespace = FIRNamespaceGoogleMobilePlatform; + currentNamespace = _namespaceGoogleMobilePlatform; break; case RCNTestRCInstanceDefault: default: @@ -707,7 +725,7 @@ - (void)testFetchConfigsFailedErrorNoNetwork { case RCNTestRCInstanceSecondApp: currentAppName = RCNTestsSecondFIRAppName; currentOptions = [self secondAppOptions]; - currentNamespace = FIRNamespaceGoogleMobilePlatform; + currentNamespace = _namespaceGoogleMobilePlatform; break; case RCNTestRCInstanceDefault: default: @@ -911,7 +929,7 @@ - (void)testActivateOnFetchNoChangeStatus { case RCNTestRCInstanceSecondApp: currentAppName = RCNTestsSecondFIRAppName; currentOptions = [self secondAppOptions]; - currentNamespace = FIRNamespaceGoogleMobilePlatform; + currentNamespace = _namespaceGoogleMobilePlatform; break; case RCNTestRCInstanceDefault: default: @@ -1782,6 +1800,43 @@ - (void)testRealtimeStreamRequestBody { XCTAssertTrue([strData containsString:@"appInstanceId:'iid'"]); } +- (void)testFetchAndActivateRolloutsNotifyInterop { + id mockNotificationCenter = [OCMockObject mockForClass:[NSNotificationCenter class]]; + [[mockNotificationCenter expect] postNotificationName:@"RolloutsStateDidChangeNotification" + object:[OCMArg any] + userInfo:[OCMArg any]]; + id mockSubscriber = [OCMockObject mockForProtocol:@protocol(FIRRolloutsStateSubscriber)]; + [[mockSubscriber expect] rolloutsStateDidChange:[OCMArg any]]; + + XCTestExpectation *expectation = [self + expectationWithDescription:[NSString + stringWithFormat:@"Test rollout update send notification"]]; + + XCTAssertEqual(_configInstances[RCNTestRCInstanceDefault].lastFetchStatus, + FIRRemoteConfigFetchStatusNoFetchYet); + + FIRRemoteConfigFetchAndActivateCompletion fetchAndActivateCompletion = + ^void(FIRRemoteConfigFetchAndActivateStatus status, NSError *error) { + XCTAssertEqual(status, FIRRemoteConfigFetchAndActivateStatusSuccessFetchedFromRemote); + XCTAssertNil(error); + + XCTAssertEqual(self->_configInstances[RCNTestRCInstanceDefault].lastFetchStatus, + FIRRemoteConfigFetchStatusSuccess); + XCTAssertNotNil(self->_configInstances[RCNTestRCInstanceDefault].lastFetchTime); + XCTAssertGreaterThan( + self->_configInstances[RCNTestRCInstanceDefault].lastFetchTime.timeIntervalSince1970, 0, + @"last fetch time interval should be set."); + [expectation fulfill]; + }; + + [_configInstances[RCNTestRCInstanceDefault] + fetchAndActivateWithCompletionHandler:fetchAndActivateCompletion]; + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + #pragma mark - Test Helpers - (FIROptions *)firstAppOptions { diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNThrottlingTests.m b/FirebaseRemoteConfig/Tests/Unit/RCNThrottlingTests.m index 8721463feb8..5429c61df1f 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNThrottlingTests.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNThrottlingTests.m @@ -25,6 +25,7 @@ #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" #import "FirebaseCore/Extension/FirebaseCoreInternal.h" +@import FirebaseRemoteConfigInterop; @interface RCNThrottlingTests : XCTestCase { RCNConfigContent *_configContentMock; @@ -53,20 +54,22 @@ - (void)setUp { RCNConfigDBManager *DBManager = [[RCNConfigDBManager alloc] init]; _configContentMock = OCMClassMock([RCNConfigContent class]); - _settings = [[RCNConfigSettings alloc] initWithDatabaseManager:DBManager - namespace:FIRNamespaceGoogleMobilePlatform - app:[FIRApp defaultApp]]; + _settings = [[RCNConfigSettings alloc] + initWithDatabaseManager:DBManager + namespace:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform + app:[FIRApp defaultApp]]; _experimentMock = OCMClassMock([RCNConfigExperiment class]); dispatch_queue_t _queue = dispatch_queue_create( "com.google.GoogleConfigService.FIRRemoteConfigTest", DISPATCH_QUEUE_SERIAL); - _configFetch = [[RCNConfigFetch alloc] initWithContent:_configContentMock - DBManager:DBManager - settings:_settings - experiment:_experimentMock - queue:_queue - namespace:FIRNamespaceGoogleMobilePlatform - app:[FIRApp defaultApp]]; + _configFetch = [[RCNConfigFetch alloc] + initWithContent:_configContentMock + DBManager:DBManager + settings:_settings + experiment:_experimentMock + queue:_queue + namespace:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform + app:[FIRApp defaultApp]]; } - (void)mockFetchResponseWithStatusCode:(NSInteger)statusCode { diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m b/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m index 0c3135e2edd..5f915d73632 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m @@ -129,8 +129,17 @@ - (void)testUserDefaultsTemplateVersionWriteAndRead { [[RCNUserDefaultsManager alloc] initWithAppName:AppName bundleID:[NSBundle mainBundle].bundleIdentifier namespace:FQNamespace1]; - [manager setLastTemplateVersion:@"1"]; - XCTAssertEqual([manager lastTemplateVersion], @"1"); + [manager setLastFetchedTemplateVersion:@"1"]; + XCTAssertEqual([manager lastFetchedTemplateVersion], @"1"); +} + +- (void)testUserDefaultsActiveTemplateVersionWriteAndRead { + RCNUserDefaultsManager* manager = + [[RCNUserDefaultsManager alloc] initWithAppName:AppName + bundleID:[NSBundle mainBundle].bundleIdentifier + namespace:FQNamespace1]; + [manager setLastActiveTemplateVersion:@"1"]; + XCTAssertEqual([manager lastActiveTemplateVersion], @"1"); } - (void)testUserDefaultsRealtimeThrottleEndTimeWriteAndRead { @@ -229,10 +238,16 @@ - (void)testUserDefaultsForMultipleNamespaces { XCTAssertEqual([manager2 realtimeRetryCount], 2); /// Fetch template version. - [manager1 setLastTemplateVersion:@"1"]; - [manager2 setLastTemplateVersion:@"2"]; - XCTAssertEqualObjects([manager1 lastTemplateVersion], @"1"); - XCTAssertEqualObjects([manager2 lastTemplateVersion], @"2"); + [manager1 setLastFetchedTemplateVersion:@"1"]; + [manager2 setLastFetchedTemplateVersion:@"2"]; + XCTAssertEqualObjects([manager1 lastFetchedTemplateVersion], @"1"); + XCTAssertEqualObjects([manager2 lastFetchedTemplateVersion], @"2"); + + /// Active template version. + [manager1 setLastActiveTemplateVersion:@"1"]; + [manager2 setLastActiveTemplateVersion:@"2"]; + XCTAssertEqualObjects([manager1 lastActiveTemplateVersion], @"1"); + XCTAssertEqualObjects([manager2 lastActiveTemplateVersion], @"2"); } - (void)testUserDefaultsReset { diff --git a/FirebaseRemoteConfig/generate_featureRolloutsTestApp.sh b/FirebaseRemoteConfig/generate_featureRolloutsTestApp.sh new file mode 100755 index 00000000000..1667fc0fe5a --- /dev/null +++ b/FirebaseRemoteConfig/generate_featureRolloutsTestApp.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# Copyright 2022 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. +# + +readonly DIR="$( git rev-parse --show-toplevel )" + +# +# This script attempts to copy the Google Services file from google3. If you are not a Google Employee, it will fail, so we'd recommend you create your own Firebase App and place the Google Services file in Tests/TestApp/Shared +# + +echoColor() { + COLOR='\033[0;35m' + NC='\033[0m' + printf "${COLOR}$1${NC}\n" +} + +echoRed() { + COLOR='\033[0;31m' + NC='\033[0m' + printf "${COLOR}$1${NC}\n" +} + +echoColor "Generating Firebase Remote Config Feature Rolouts Test App" +echoColor "Copying GoogleService-Info.plist from google3. Checking gcert status" +if gcertstatus; then + G3Path="/google/src/files/head/depot/google3/third_party/firebase/ios/Secrets/RemoteConfig/FeatureRollouts/GoogleService-Info.plist" + Dest="$DIR/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared" + cp $G3Path $Dest + echoColor "Copied $G3Path to $Dest" +else + echoRed "gcert token is not valid. If you are a Google Employee, run 'gcert', and then repeat this command. Non-Google employees will need to download a GoogleService-Info.plist and place it in $DIR/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp" +fi + + +echoColor "Running 'pod install'" +cd $DIR/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp +pod install + +# Upon a `pod install`, Crashlytics will copy these files at the root directory +# due to a funky interaction with its cocoapod. This line deletes these extra +# copies of the files as they should only live in Crashlytics/ +rm -f $DIR/run $DIR/upload-symbols + +open *.xcworkspace + diff --git a/FirebaseRemoteConfigInterop.podspec b/FirebaseRemoteConfigInterop.podspec new file mode 100644 index 00000000000..86b86b24e6a --- /dev/null +++ b/FirebaseRemoteConfigInterop.podspec @@ -0,0 +1,34 @@ +Pod::Spec.new do |s| + s.name = 'FirebaseRemoteConfigInterop' + s.version = '10.23.0' + s.summary = 'Interfaces that allow other Firebase SDKs to use Remote Config functionality.' + + s.description = <<-DESC + Not for public use. + A set of protocols that other Firebase SDKs can use to interoperate with FirebaseRemoetConfig in a safe + and reliable manner. + DESC + + s.homepage = 'https://firebase.google.com' + s.license = { :type => 'Apache-2.0', :file => 'LICENSE' } + s.authors = 'Google, Inc.' + + # NOTE that these should not be used externally, this is for Firebase pods to depend on each + # other. + s.source = { + :git => 'https://github.com/firebase/firebase-ios-sdk.git', + :tag => 'CocoaPods-' + s.version.to_s + } + + s.swift_version = '5.3' + s.cocoapods_version = '>= 1.12.0' + s.prefix_header_file = false + + s.social_media_url = 'https://twitter.com/Firebase' + s.ios.deployment_target = '11.0' + s.osx.deployment_target = '10.13' + s.tvos.deployment_target = '12.0' + s.watchos.deployment_target = '6.0' + + s.source_files = 'FirebaseRemoteConfig/Interop/*.swift' +end diff --git a/FirebaseRemoteConfigSwift/Tests/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift index b695e375887..892f14ec834 100644 --- a/FirebaseRemoteConfigSwift/Tests/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift +++ b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift @@ -16,6 +16,7 @@ import XCTest import FirebaseCore import FirebaseRemoteConfig +import FirebaseRemoteConfigInterop import FirebaseRemoteConfigSwift final class FirebaseRemoteConfigSwift_APIBuildTests: XCTestCase { diff --git a/FirebaseSessions/Tests/TestApp/Podfile b/FirebaseSessions/Tests/TestApp/Podfile index 67c05149217..4bea966bc4f 100644 --- a/FirebaseSessions/Tests/TestApp/Podfile +++ b/FirebaseSessions/Tests/TestApp/Podfile @@ -7,6 +7,7 @@ def shared_pods pod 'FirebaseCoreInternal', :path => '../../../' pod 'FirebaseCoreExtension', :path => '../../../' pod 'FirebaseSessions', :path => '../../../' + pod 'FirebaseRemoteConfigInterop', :path => '../../../' end target 'AppQualityDevApp_iOS' do diff --git a/IntegrationTesting/ClientApp/Podfile b/IntegrationTesting/ClientApp/Podfile index 7efd2cdd1ea..2a72d478739 100644 --- a/IntegrationTesting/ClientApp/Podfile +++ b/IntegrationTesting/ClientApp/Podfile @@ -17,6 +17,7 @@ target 'ClientApp-CocoaPods' do pod 'FirebaseAppCheck', :path => '../../' pod 'FirebaseRemoteConfig', :path => '../../' pod 'FirebaseRemoteConfigSwift', :path => '../../' + pod 'FirebaseRemoteConfigInterop', :path => '../../' pod 'FirebaseAppDistribution', :path => '../../' pod 'FirebaseAuth', :path => '../../' pod 'FirebaseCrashlytics', :path => '../../' diff --git a/IntegrationTesting/CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Podfile b/IntegrationTesting/CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Podfile index 03bfe2e2d04..e45e8cd4908 100644 --- a/IntegrationTesting/CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Podfile +++ b/IntegrationTesting/CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Podfile @@ -25,6 +25,7 @@ target 'CocoapodsIntegrationTest' do pod 'FirebaseInstallations', :path => '../../' pod 'FirebaseMessaging', :path => '../../' pod 'FirebaseMessagingInterop', :path => '../../' + pod 'FirebaseRemoteConfigInterop', :path => '../../' pod 'FirebasePerformance', :path => '../../' pod 'FirebaseStorage', :path => '../../' end diff --git a/Package.swift b/Package.swift index ae854aa3f10..cd16df6ca5f 100644 --- a/Package.swift +++ b/Package.swift @@ -497,11 +497,17 @@ let package = Package( ), .target( name: "FirebaseCrashlytics", - dependencies: ["FirebaseCore", "FirebaseInstallations", "FirebaseSessions", - .product(name: "GoogleDataTransport", package: "GoogleDataTransport"), - .product(name: "GULEnvironment", package: "GoogleUtilities"), - .product(name: "FBLPromises", package: "Promises"), - .product(name: "nanopb", package: "nanopb")], + dependencies: [ + "FirebaseCore", + "FirebaseInstallations", + "FirebaseSessions", + "FirebaseRemoteConfigInterop", + "FirebaseCrashlyticsSwift", + .product(name: "GoogleDataTransport", package: "GoogleDataTransport"), + .product(name: "GULEnvironment", package: "GoogleUtilities"), + .product(name: "FBLPromises", package: "Promises"), + .product(name: "nanopb", package: "nanopb"), + ], path: "Crashlytics", exclude: [ "run", @@ -514,6 +520,7 @@ let package = Package( "upload-symbols", "CrashlyticsInputFiles.xcfilelist", "third_party/libunwind/LICENSE", + "Crashlytics/Rollouts/", ], sources: [ "Crashlytics/", @@ -543,6 +550,19 @@ let package = Package( .linkedFramework("SystemConfiguration", .when(platforms: [.iOS, .macOS, .tvOS])), ] ), + .target( + name: "FirebaseCrashlyticsSwift", + dependencies: ["FirebaseRemoteConfigInterop"], + path: "Crashlytics", + sources: [ + "Crashlytics/Rollouts/", + ] + ), + .testTarget( + name: "FirebaseCrashlyticsSwiftUnit", + dependencies: ["FirebaseCrashlyticsSwift"], + path: "Crashlytics/UnitTestsSwift/" + ), .testTarget( name: "FirebaseCrashlyticsUnit", dependencies: ["FirebaseCrashlytics", .product(name: "OCMock", package: "ocmock")], @@ -967,6 +987,7 @@ let package = Package( "FirebaseCore", "FirebaseABTesting", "FirebaseInstallations", + "FirebaseRemoteConfigInterop", .product(name: "GULNSData", package: "GoogleUtilities"), ], path: "FirebaseRemoteConfig/Sources", @@ -996,6 +1017,14 @@ let package = Package( .headerSearchPath("../../.."), ] ), + .testTarget( + name: "RemoteConfigSwiftUnit", + dependencies: ["FirebaseRemoteConfigInternal"], + path: "FirebaseRemoteConfig/Tests/SwiftUnit", + cSettings: [ + .headerSearchPath("../../.."), + ] + ), .target( name: "FirebaseRemoteConfig", dependencies: [ @@ -1039,6 +1068,15 @@ let package = Package( .headerSearchPath("../../../"), ] ), + // Internal headers only for consuming from other SDK. + .target( + name: "FirebaseRemoteConfigInterop", + path: "FirebaseRemoteConfig/Interop", + publicHeadersPath: ".", + cSettings: [ + .headerSearchPath("../../"), + ] + ), // MARK: - Firebase Sessions diff --git a/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift b/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift index 5bd4f3dd5e2..8575cf37e36 100755 --- a/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift +++ b/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift @@ -32,6 +32,7 @@ public let shared = Manifest( Pod("FirebaseMessagingInterop"), Pod("FirebaseInstallations"), Pod("FirebaseSessions"), + Pod("FirebaseRemoteConfigInterop"), Pod("GoogleAppMeasurement", isClosedSource: true), Pod("GoogleAppMeasurementOnDeviceConversion", isClosedSource: true, platforms: ["ios"]), Pod("FirebaseAnalytics", isClosedSource: true, zip: true), diff --git a/scripts/localize_podfile.swift b/scripts/localize_podfile.swift index f07cb3124a0..8b60a2cbdd5 100755 --- a/scripts/localize_podfile.swift +++ b/scripts/localize_podfile.swift @@ -39,6 +39,7 @@ let implicitPods = [ "FirebaseAppCheckInterop", "FirebaseAuthInterop", "FirebaseMessagingInterop", "FirebaseCoreInternal", "FirebaseSessions", "FirebaseSharedSwift", + "FirebaseRemoteConfigInterop", ] let binaryPods = [