From 9c50f5e731cb78b01d08de83323481c1a2c63dff Mon Sep 17 00:00:00 2001 From: themiswang Date: Thu, 7 Dec 2023 15:45:29 -0500 Subject: [PATCH 01/19] [Rollouts] RC interop implementation (#12173) --- ClientApp/Podfile | 46 +++++++++++++ .../Podfile | 37 +++++++++++ CoreOnly/Tests/FirebasePodTest/Podfile | 1 + Example/watchOSSample/Podfile | 1 + FirebaseRemoteConfig.podspec | 4 +- .../Interop/RemoteConfigInterop.swift | 21 ++++++ .../Interop/RolloutAssignment.swift | 46 +++++++++++++ .../Interop/RolloutsStateSubscriber.swift | 20 ++++++ .../Sources/FIRRemoteConfigComponent.h | 8 ++- .../Sources/FIRRemoteConfigComponent.m | 49 +++++++++++++- FirebaseRemoteConfig/Tests/Sample/Podfile | 1 + .../SwiftUnit/RemoteConfigInteropTests.swift | 65 +++++++++++++++++++ .../Tests/Unit/FIRRemoteConfigComponentTest.m | 43 +++++++++++- .../Tests/Unit/RCNRemoteConfigTest.m | 2 + FirebaseRemoteConfigInterop.podspec | 34 ++++++++++ Package.swift | 33 ++++++++-- scripts/localize_podfile.swift | 1 + 17 files changed, 402 insertions(+), 10 deletions(-) create mode 100644 ClientApp/Podfile create mode 100644 CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Podfile create mode 100644 FirebaseRemoteConfig/Interop/RemoteConfigInterop.swift create mode 100644 FirebaseRemoteConfig/Interop/RolloutAssignment.swift create mode 100644 FirebaseRemoteConfig/Interop/RolloutsStateSubscriber.swift create mode 100644 FirebaseRemoteConfig/Tests/SwiftUnit/RemoteConfigInteropTests.swift create mode 100644 FirebaseRemoteConfigInterop.podspec diff --git a/ClientApp/Podfile b/ClientApp/Podfile new file mode 100644 index 00000000000..3a69069714f --- /dev/null +++ b/ClientApp/Podfile @@ -0,0 +1,46 @@ +source 'https://github.com/firebase/SpecsDev.git' +source 'https://github.com/firebase/SpecsStaging.git' +source 'https://cdn.cocoapods.org/' + +target 'ClientApp-CocoaPods' do + platform :ios, '11.0' + + use_frameworks! + + pod 'FirebaseCore', :path => '../' + pod 'FirebaseInstallations', :path => '../' + pod 'FirebaseAnalytics' # Binary pods don't work with `:path`. + pod 'FirebaseAnalyticsOnDeviceConversion', :path => '../' + pod 'FirebaseABTesting', :path => '../' + pod 'FirebaseAppCheck', :path => '../' + pod 'FirebaseRemoteConfig', :path => '../' + pod 'FirebaseRemoteConfigSwift', :path => '../' + pod 'FirebaseRemoteConfigInterop', :path => '../' + pod 'FirebaseAppDistribution', :path => '../' + pod 'FirebaseAuth', :path => '../' + pod 'FirebaseCrashlytics', :path => '../' + pod 'FirebaseDatabase', :path => '../' + pod 'FirebaseDatabaseSwift', :path => '../' + pod 'FirebaseDynamicLinks', :path => '../' + pod 'FirebaseFirestore', :path => '../' + pod 'FirebaseFirestoreSwift', :path => '../' + pod 'FirebaseFunctions', :path => '../' + pod 'FirebaseInAppMessaging', :path => '../' + pod 'FirebaseMessaging', :path => '../' + pod 'FirebasePerformance', :path => '../' + pod 'FirebaseStorage', :path => '../' + pod 'FirebaseMLModelDownloader', :path => '../' + pod 'Firebase', :path => '../' +end + +target 'ClientApp-CocoaPods-iOS13' do + platform :ios, '13.0' + + use_frameworks! + + pod 'FirebaseAnalytics' # Binary pods don't work with `:path`. + pod 'FirebaseAnalyticsSwift', :path => '../' # Requires iOS 13.0+ + pod 'FirebaseInAppMessaging', :path => '../' + pod 'FirebaseInAppMessagingSwift', :path => '../' # Requires iOS 13.0+ + +end diff --git a/CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Podfile b/CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Podfile new file mode 100644 index 00000000000..9741e179cf3 --- /dev/null +++ b/CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Podfile @@ -0,0 +1,37 @@ +source 'https://github.com/firebase/SpecsDev.git' +source 'https://github.com/firebase/SpecsStaging.git' +source 'https://cdn.cocoapods.org/' + +# Uncomment the next line to define a global platform for your project +platform :ios, '11.0' + +target 'CocoapodsIntegrationTest' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + pod 'FirebaseABTesting', :path => '../' + pod 'FirebaseAppDistribution', :path => '../' + pod 'FirebaseAppCheckInterop', :path => '../' + pod 'FirebaseCore', :path => '../' + pod 'FirebaseCoreExtension', :path => '../' + pod 'FirebaseCoreInternal', :path => '../' + pod 'FirebaseCrashlytics', :path => '../' + pod 'FirebaseAuth', :path => '../' + pod 'FirebaseAuthInterop', :path => '../' + pod 'FirebaseDatabase', :path => '../' + pod 'FirebaseDynamicLinks', :path => '../' + pod 'FirebaseFirestore', :path => '../' + pod 'FirebaseFunctions', :path => '../' + pod 'FirebaseInAppMessaging', :path => '../' + pod 'FirebaseInstallations', :path => '../' + pod 'FirebaseMessaging', :path => '../' + pod 'FirebaseMessagingInterop', :path => '../' + pod 'FirebaseRemoteConfigInterop', :path => '../' + pod 'FirebasePerformance', :path => '../' + pod 'FirebaseStorage', :path => '../' +end + +# Using the new speed-enhancing features available with CocoaPods 1.7+ +# [sudo] gem install cocoapods --pre +install! 'cocoapods', + :generate_multiple_pod_projects => true, + :incremental_installation => true 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/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/FirebaseRemoteConfig.podspec b/FirebaseRemoteConfig.podspec index efef50fef50..200164e466e 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.20' 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/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..3358ec8ab79 --- /dev/null +++ b/FirebaseRemoteConfig/Interop/RolloutAssignment.swift @@ -0,0 +1,46 @@ +// 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 + + 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() + + 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/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..08927453adb 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 { + // TODO(Themisw): Adding the registered subscriber reference to the namespace instance + // [self.instances[namespace] addRemoteConfigInteropSubscriber:subscriber]; } @end 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/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/RCNRemoteConfigTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m index e1a23b5a695..6bf23d0777f 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" @@ -286,6 +287,7 @@ __unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status, - (void)tearDown { [_DBManager removeDatabaseOnDatabaseQueueAtPath:_DBPath]; + [FIRRemoteConfigComponent clearAllComponentInstances]; [[NSUserDefaults standardUserDefaults] removePersistentDomainForName:_userDefaultsSuiteName]; [_DBManagerMock stopMocking]; _DBManagerMock = nil; diff --git a/FirebaseRemoteConfigInterop.podspec b/FirebaseRemoteConfigInterop.podspec new file mode 100644 index 00000000000..f2ce8cec9d6 --- /dev/null +++ b/FirebaseRemoteConfigInterop.podspec @@ -0,0 +1,34 @@ +Pod::Spec.new do |s| + s.name = 'FirebaseRemoteConfigInterop' + s.version = '10.20.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.4.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/Package.swift b/Package.swift index ae854aa3f10..98d4872a0d8 100644 --- a/Package.swift +++ b/Package.swift @@ -497,11 +497,16 @@ 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", + .product(name: "GoogleDataTransport", package: "GoogleDataTransport"), + .product(name: "GULEnvironment", package: "GoogleUtilities"), + .product(name: "FBLPromises", package: "Promises"), + .product(name: "nanopb", package: "nanopb"), + ], path: "Crashlytics", exclude: [ "run", @@ -967,6 +972,7 @@ let package = Package( "FirebaseCore", "FirebaseABTesting", "FirebaseInstallations", + "FirebaseRemoteConfigInterop", .product(name: "GULNSData", package: "GoogleUtilities"), ], path: "FirebaseRemoteConfig/Sources", @@ -996,6 +1002,14 @@ let package = Package( .headerSearchPath("../../.."), ] ), + .testTarget( + name: "RemoteConfigSwiftUnit", + dependencies: ["FirebaseRemoteConfigInternal"], + path: "FirebaseRemoteConfig/Tests/SwiftUnit", + cSettings: [ + .headerSearchPath("../../.."), + ] + ), .target( name: "FirebaseRemoteConfig", dependencies: [ @@ -1039,6 +1053,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/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 = [ From 06947d7526fdec90a2b83d1d19f5edaec144ce69 Mon Sep 17 00:00:00 2001 From: Doudou Nan <146472823+ddnan@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:27:34 -0800 Subject: [PATCH 02/19] [Rollouts]Add a rollout metadata constant (#12209) --- FirebaseRemoteConfig/Sources/RCNConfigConstants.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigConstants.h b/FirebaseRemoteConfig/Sources/RCNConfigConstants.h index db0e0213ae1..e3cd99088a9 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigConstants.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigConstants.h @@ -37,6 +37,8 @@ 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"; /// Error key. static NSString *const RCNFetchResponseKeyError = @"error"; /// Error code. From 4a7c1c969eedd161e02d420b07ac0f0971ea51be Mon Sep 17 00:00:00 2001 From: themiswang Date: Wed, 20 Dec 2023 16:06:02 -0500 Subject: [PATCH 03/19] [Rollouts] Crashlytics Rollouts interop Integration (#12200) --- Crashlytics/Crashlytics/FIRCrashlytics.m | 35 +++++++++- .../CrashlyticsRemoteConfigManager.swift | 66 +++++++++++++++++++ .../CrashlyticsRemoteConfigManagerTests.swift | 64 ++++++++++++++++++ FirebaseCrashlytics.podspec | 6 +- Package.swift | 15 +++++ 5 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift create mode 100644 Crashlytics/UnitTestsSwift/CrashlyticsRemoteConfigManagerTests.swift diff --git a/Crashlytics/Crashlytics/FIRCrashlytics.m b/Crashlytics/Crashlytics/FIRCrashlytics.m index 4d112cddad4..c1b6ce48c02 100644 --- a/Crashlytics/Crashlytics/FIRCrashlytics.m +++ b/Crashlytics/Crashlytics/FIRCrashlytics.m @@ -58,6 +58,12 @@ #import @import FirebaseSessions; +@import FirebaseRemoteConfigInterop; +#if SWIFT_PACKAGE +@import FirebaseCrashlyticsSwift; +#else // Swift Package Manager +#import "FirebaseCrashlytics/FirebaseCrashlytics-Swift.h" +#endif // Cocoapod #if TARGET_OS_IPHONE #import @@ -76,7 +82,10 @@ @protocol FIRCrashlyticsInstanceProvider @end -@interface FIRCrashlytics () +@interface FIRCrashlytics () @property(nonatomic) BOOL didPreviouslyCrash; @property(nonatomic, copy) NSString *googleAppID; @@ -91,6 +100,8 @@ @interface FIRCrashlytics () )analytics - sessions:(id)sessions { + sessions:(id)sessions + remoteConfig:(id)remoteConfig { self = [super init]; if (self) { @@ -157,6 +169,15 @@ - (instancetype)initWithApp:(FIRApp *)app [sessions registerWithSubscriber:self]; } + if (remoteConfig) { + FIRCLSDebugLog(@"Registering RemoteConfig SDK subscription for rollouts data"); + + _remoteConfigManager = [[FIRCLSRemoteConfigManager alloc] initWithRemoteConfig:remoteConfig]; + + // TODO(themisw): Import "firebase" from the interop in the future. + [remoteConfig registerRolloutsStateSubscriber:self for:@"firebase"]; + } + _reportUploader = [[FIRCLSReportUploader alloc] initWithManagerData:_managerData]; _existingReportManager = @@ -215,6 +236,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 +246,8 @@ + (void)load { appInfo:NSBundle.mainBundle.infoDictionary installations:installations analytics:analytics - sessions:sessions]; + sessions:sessions + remoteConfig:remoteConfig]; }; FIRComponent *component = @@ -407,4 +430,10 @@ - (FIRSessionsSubscriberName)sessionsSubscriberName { return FIRSessionsSubscriberNameCrashlytics; } +#pragma mark - FIRRolloutsStateSubscriber +- (void)rolloutsStateDidChange:(FIRRolloutsState *_Nonnull)rolloutsState { + [_remoteConfigManager updateRolloutsStateWithRolloutsState:rolloutsState]; + // TODO(themisw): writing the rollout state change to persistence +} + @end diff --git a/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift b/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift new file mode 100644 index 00000000000..84acd6a4ade --- /dev/null +++ b/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift @@ -0,0 +1,66 @@ +// 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 + +protocol CrashlyticsPersistentLog: NSObject { + func updateRolloutsStateToPersistence(rolloutAssignments: [RolloutAssignment]) +} + +@objc(FIRCLSRemoteConfigManager) +public class CrashlyticsRemoteConfigManager: NSObject { + public static let maxRolloutAssignments = 128 + public static let maxParameterValueLength = 256 + + var remoteConfig: RemoteConfigInterop + public private(set) var rolloutAssignment: [RolloutAssignment] = [] + weak var persistenceDelegate: CrashlyticsPersistentLog? + + @objc public init(remoteConfig: RemoteConfigInterop) { + self.remoteConfig = remoteConfig + } + + @objc public func updateRolloutsState(rolloutsState: RolloutsState) { + rolloutAssignment = normalizeRolloutAssignment(assignments: Array(rolloutsState.assignments)) + } +} + +private extension CrashlyticsRemoteConfigManager { + func normalizeRolloutAssignment(assignments: [RolloutAssignment]) -> [RolloutAssignment] { + var validatedAssignments = assignments + if assignments.count > CrashlyticsRemoteConfigManager.maxRolloutAssignments { + debugPrint("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[.. 10.5' s.dependency 'FirebaseInstallations', '~> 10.0' s.dependency 'FirebaseSessions', '~> 10.5' + s.dependency 'FirebaseRemoteConfigInterop', '~> 10.20' 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/Package.swift b/Package.swift index 98d4872a0d8..cd16df6ca5f 100644 --- a/Package.swift +++ b/Package.swift @@ -502,6 +502,7 @@ let package = Package( "FirebaseInstallations", "FirebaseSessions", "FirebaseRemoteConfigInterop", + "FirebaseCrashlyticsSwift", .product(name: "GoogleDataTransport", package: "GoogleDataTransport"), .product(name: "GULEnvironment", package: "GoogleUtilities"), .product(name: "FBLPromises", package: "Promises"), @@ -519,6 +520,7 @@ let package = Package( "upload-symbols", "CrashlyticsInputFiles.xcfilelist", "third_party/libunwind/LICENSE", + "Crashlytics/Rollouts/", ], sources: [ "Crashlytics/", @@ -548,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")], From 162f6905bb93ecced03d4d59512dabb474879db1 Mon Sep 17 00:00:00 2001 From: themiswang Date: Fri, 5 Jan 2024 11:09:54 -0500 Subject: [PATCH 04/19] [Rollouts] Add integration test app (#12239) --- Crashlytics/Crashlytics/FIRCrashlytics.m | 2 +- .../project.pbxproj | 1053 +++++++++++++++++ .../FeatureRolloutsTestApp/ContentView.swift | 29 + .../FeatureRolloutsTestApp.entitlements | 10 + .../ContentView.swift | 25 + .../ContentView.swift | 26 + .../ContentView.swift | 30 + .../Tests/FeatureRolloutsTestApp/Podfile | 51 + .../Shared/CrashButtonView.swift | 62 + .../Shared/FeatureRolloutsTestAppApp.swift | 31 + .../generate_featureRolloutsTestApp.sh | 58 + FirebaseSessions/Tests/TestApp/Podfile | 1 + 12 files changed, 1377 insertions(+), 1 deletion(-) create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp.xcodeproj/project.pbxproj create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp/ContentView.swift create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp/FeatureRolloutsTestApp.entitlements create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/ContentView.swift create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_Crashlytics_iOS/ContentView.swift create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_RemoteConfig_iOS/ContentView.swift create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Podfile create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/CrashButtonView.swift create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/FeatureRolloutsTestAppApp.swift create mode 100755 FirebaseRemoteConfig/generate_featureRolloutsTestApp.sh diff --git a/Crashlytics/Crashlytics/FIRCrashlytics.m b/Crashlytics/Crashlytics/FIRCrashlytics.m index c1b6ce48c02..e487007d66f 100644 --- a/Crashlytics/Crashlytics/FIRCrashlytics.m +++ b/Crashlytics/Crashlytics/FIRCrashlytics.m @@ -62,7 +62,7 @@ #if SWIFT_PACKAGE @import FirebaseCrashlyticsSwift; #else // Swift Package Manager -#import "FirebaseCrashlytics/FirebaseCrashlytics-Swift.h" +#import #endif // Cocoapod #if TARGET_OS_IPHONE diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp.xcodeproj/project.pbxproj b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..65d88ebcaa9 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp.xcodeproj/project.pbxproj @@ -0,0 +1,1053 @@ +// !$*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 */; }; + 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 = ""; }; + 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 */, + 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 */, + C49C48992B4720AE00BC1456 /* ContentView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C49C48792B4704F500BC1456 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 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..f2b83652da4 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_CrashlyticsRemoteConfig_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 { + CrashButtonView() + .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..5dfea79becb --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_RemoteConfig_iOS/ContentView.swift @@ -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. +// + +import Foundation +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/Podfile b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Podfile new file mode 100644 index 00000000000..c5f9ddf9e46 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Podfile @@ -0,0 +1,51 @@ +# 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 => '../../../' +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/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/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 From 40826e865d40be063d29e24e59d996457e072f3e Mon Sep 17 00:00:00 2001 From: themiswang Date: Wed, 10 Jan 2024 18:47:48 -0500 Subject: [PATCH 05/19] [Rollouts] Rollouts serialization (#12258) --- .../CrashlyticsRemoteConfigManager.swift | 20 +++++++++- .../Rollouts/EncodedRolloutAssignment.swift | 34 +++++++++++++++++ .../Rollouts/StringToHexConverter.swift | 38 +++++++++++++++++++ Crashlytics/UnitTests/FIRCLSFileTests.m | 31 +++++++++++++++ .../CrashlyticsRemoteConfigManagerTests.swift | 23 +++++++++++ .../Interop/RolloutAssignment.swift | 7 ++-- 6 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 Crashlytics/Crashlytics/Rollouts/EncodedRolloutAssignment.swift create mode 100644 Crashlytics/Crashlytics/Rollouts/StringToHexConverter.swift diff --git a/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift b/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift index 84acd6a4ade..578cffa08ce 100644 --- a/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift +++ b/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift @@ -25,7 +25,7 @@ public class CrashlyticsRemoteConfigManager: NSObject { public static let maxParameterValueLength = 256 var remoteConfig: RemoteConfigInterop - public private(set) var rolloutAssignment: [RolloutAssignment] = [] + @objc public private(set) var rolloutAssignment: [RolloutAssignment] = [] weak var persistenceDelegate: CrashlyticsPersistentLog? @objc public init(remoteConfig: RemoteConfigInterop) { @@ -35,6 +35,24 @@ public class CrashlyticsRemoteConfigManager: NSObject { @objc public func updateRolloutsState(rolloutsState: RolloutsState) { rolloutAssignment = normalizeRolloutAssignment(assignments: Array(rolloutsState.assignments)) } + + @objc public func getRolloutAssignmentsEncodedJson() -> String? { + let contentEncodedRolloutAssignments = rolloutAssignment.map { assignment in + EncodedRolloutAssignment(assignment: assignment) + } + + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + encoder.outputFormatting = .sortedKeys + let encodeData = try? encoder.encode(contentEncodedRolloutAssignments) + if let data = encodeData, let returnString = String(data: data, encoding: .utf8) { + return returnString + } + + // TODO(themisw): Hook into core logging functions + debugPrint("Failed to serialize rollouts", encodeData ?? "nil") + return nil + } } private extension CrashlyticsRemoteConfigManager { diff --git a/Crashlytics/Crashlytics/Rollouts/EncodedRolloutAssignment.swift b/Crashlytics/Crashlytics/Rollouts/EncodedRolloutAssignment.swift new file mode 100644 index 00000000000..53e29a198c2 --- /dev/null +++ b/Crashlytics/Crashlytics/Rollouts/EncodedRolloutAssignment.swift @@ -0,0 +1,34 @@ +// 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(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..896c053e7fe 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 // Cocoapod + #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/UnitTestsSwift/CrashlyticsRemoteConfigManagerTests.swift b/Crashlytics/UnitTestsSwift/CrashlyticsRemoteConfigManagerTests.swift index fe8b31d0204..4f175060ccd 100644 --- a/Crashlytics/UnitTestsSwift/CrashlyticsRemoteConfigManagerTests.swift +++ b/Crashlytics/UnitTestsSwift/CrashlyticsRemoteConfigManagerTests.swift @@ -45,6 +45,18 @@ final class CrashlyticsRemoteConfigManagerTests: XCTestCase { 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 { @@ -61,4 +73,15 @@ final class CrashlyticsRemoteConfigManagerTests: XCTestCase { } } } + + 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) + rcManager.updateRolloutsState(rolloutsState: singleRollout) + + let string = rcManager.getRolloutAssignmentsEncodedJson() + XCTAssertEqual(string, expectedString) + } } diff --git a/FirebaseRemoteConfig/Interop/RolloutAssignment.swift b/FirebaseRemoteConfig/Interop/RolloutAssignment.swift index 3358ec8ab79..715412bb4f1 100644 --- a/FirebaseRemoteConfig/Interop/RolloutAssignment.swift +++ b/FirebaseRemoteConfig/Interop/RolloutAssignment.swift @@ -22,8 +22,9 @@ public class RolloutAssignment: NSObject { @objc public var parameterKey: String @objc public var parameterValue: String - public init(rolloutId: String, variantId: String, templateVersion: Int64, parameterKey: String, - parameterValue: String) { + @objc public init(rolloutId: String, variantId: String, templateVersion: Int64, + parameterKey: String, + parameterValue: String) { self.rolloutId = rolloutId self.variantId = variantId self.templateVersion = templateVersion @@ -37,7 +38,7 @@ public class RolloutAssignment: NSObject { public class RolloutsState: NSObject { @objc public var assignments: Set = Set() - public init(assignmentList: [RolloutAssignment]) { + @objc public init(assignmentList: [RolloutAssignment]) { for assignment in assignmentList { assignments.insert(assignment) } From 96e2e99921f3268fc1956b072a6f30dd0ceb7cac Mon Sep 17 00:00:00 2001 From: Doudou Nan <146472823+ddnan@users.noreply.github.com> Date: Thu, 18 Jan 2024 10:43:50 -0800 Subject: [PATCH 06/19] [Rollouts] Implement insert and load logic for rollout metadata in RC (#12262) --- .../Sources/RCNConfigContent.m | 43 +++-- .../Sources/RCNConfigDBManager.h | 14 +- .../Sources/RCNConfigDBManager.m | 122 +++++++++++-- .../Sources/RCNConfigDefines.h | 2 + .../Tests/Unit/RCNConfigContentTest.m | 4 +- .../Tests/Unit/RCNConfigDBManagerTest.m | 165 ++++++++++++++++-- 6 files changed, 303 insertions(+), 47 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.m b/FirebaseRemoteConfig/Sources/RCNConfigContent.m index 4f55a2e9274..f28ee662b34 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. diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h index 39c3e213b73..318c69ab122 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 value The value that rollout metadata array. +/// @param handler The callback. +- (void)insertOrUpdateRolloutTableWithKey:(NSString *)key + value:(NSArray *)value + 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..823d1c29895 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 *)value + completionHandler:(RCNDBCompletion)handler { + dispatch_async(_databaseOperationQueue, ^{ + BOOL success = [self insertOrUpdateRolloutTableWithKey:key value:value]; + 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/Tests/Unit/RCNConfigContentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m index d4f33bf0f71..7c7290e7551 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m @@ -44,7 +44,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 +53,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 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."]; From b9b970dd2858ec704d22ecdd9d9a6288c2690ea3 Mon Sep 17 00:00:00 2001 From: themiswang Date: Tue, 23 Jan 2024 14:44:57 -0500 Subject: [PATCH 07/19] [Rollouts]Writing Rollouts to persistence (#12300) --- .../Components/FIRCLSUserLogging.h | 4 +- .../Components/FIRCLSUserLogging.m | 16 +++- .../FIRCLSRolloutsPersistenceManager.h | 29 +++++++ .../FIRCLSRolloutsPersistenceManager.m | 61 ++++++++++++++ Crashlytics/Crashlytics/FIRCrashlytics.m | 24 ++++-- .../Crashlytics/Handlers/FIRCLSException.h | 5 +- .../Crashlytics/Handlers/FIRCLSException.mm | 41 +++++---- .../Crashlytics/Models/FIRCLSInternalReport.h | 1 + .../Crashlytics/Models/FIRCLSInternalReport.m | 1 + .../CrashlyticsRemoteConfigManager.swift | 83 +++++++++++++++---- .../Rollouts/EncodedRolloutAssignment.swift | 10 +++ Crashlytics/UnitTests/FIRCLSLoggingTests.m | 10 +-- .../FIRCLSRolloutsPersistenceManagerTests.m | 70 ++++++++++++++++ .../UnitTests/FIRRecordExceptionModelTests.m | 2 +- .../CrashlyticsRemoteConfigManagerTests.swift | 58 +++++++++++-- 15 files changed, 360 insertions(+), 55 deletions(-) create mode 100644 Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h create mode 100644 Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m create mode 100644 Crashlytics/UnitTests/FIRCLSRolloutsPersistenceManagerTests.m 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..83c7f25ca8b --- /dev/null +++ b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h @@ -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. + +#if SWIFT_PACKAGE +@import FirebaseCrashlyticsSwift; +#else // Swift Package Manager +#import +#endif // Cocoapod + +@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; +@end diff --git a/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m new file mode 100644 index 00000000000..c0c0a38ed6b --- /dev/null +++ b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m @@ -0,0 +1,61 @@ +// 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 // Cocoapod + +@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]; + }); +} +@end diff --git a/Crashlytics/Crashlytics/FIRCrashlytics.m b/Crashlytics/Crashlytics/FIRCrashlytics.m index e487007d66f..f5c606929ac 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" @@ -172,7 +174,11 @@ - (instancetype)initWithApp:(FIRApp *)app if (remoteConfig) { FIRCLSDebugLog(@"Registering RemoteConfig SDK subscription for rollouts data"); - _remoteConfigManager = [[FIRCLSRemoteConfigManager alloc] initWithRemoteConfig:remoteConfig]; + FIRCLSRolloutsPersistenceManager *persistenceManager = + [[FIRCLSRolloutsPersistenceManager alloc] initWithFileManager:_fileManager]; + _remoteConfigManager = + [[FIRCLSRemoteConfigManager alloc] initWithRemoteConfig:remoteConfig + persistenceDelegate:persistenceManager]; // TODO(themisw): Import "firebase" from the interop in the future. [remoteConfig registerRolloutsStateSubscriber:self for:@"firebase"]; @@ -400,11 +406,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 { @@ -432,8 +440,12 @@ - (FIRSessionsSubscriberName)sessionsSubscriberName { #pragma mark - FIRRolloutsStateSubscriber - (void)rolloutsStateDidChange:(FIRRolloutsState *_Nonnull)rolloutsState { - [_remoteConfigManager updateRolloutsStateWithRolloutsState:rolloutsState]; - // TODO(themisw): writing the rollout state change to persistence + 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..5a43a83834a 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 index 578cffa08ce..b16396fe59c 100644 --- a/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift +++ b/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift @@ -15,8 +15,9 @@ import FirebaseRemoteConfigInterop import Foundation -protocol CrashlyticsPersistentLog: NSObject { - func updateRolloutsStateToPersistence(rolloutAssignments: [RolloutAssignment]) +@objc(FIRCLSPersistenceLog) +public protocol CrashlyticsPersistenceLog { + func updateRolloutsStateToPersistence(rollouts: Data, reportID: String) } @objc(FIRCLSRemoteConfigManager) @@ -24,29 +25,47 @@ 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 - @objc public private(set) var rolloutAssignment: [RolloutAssignment] = [] - weak var persistenceDelegate: CrashlyticsPersistentLog? + var persistenceDelegate: CrashlyticsPersistenceLog - @objc public init(remoteConfig: RemoteConfigInterop) { - self.remoteConfig = remoteConfig + @objc public var rolloutAssignment: [RolloutAssignment] { + lock.lock() + defer { lock.unlock() } + let copy = _rolloutAssignment + return copy } - @objc public func updateRolloutsState(rolloutsState: RolloutsState) { - rolloutAssignment = normalizeRolloutAssignment(assignments: Array(rolloutsState.assignments)) + @objc public init(remoteConfig: RemoteConfigInterop, + persistenceDelegate: CrashlyticsPersistenceLog) { + self.remoteConfig = remoteConfig + self.persistenceDelegate = persistenceDelegate } - @objc public func getRolloutAssignmentsEncodedJson() -> String? { - let contentEncodedRolloutAssignments = rolloutAssignment.map { assignment in - EncodedRolloutAssignment(assignment: assignment) + @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 + ) } + } - let encoder = JSONEncoder() - encoder.keyEncodingStrategy = .convertToSnakeCase - encoder.outputFormatting = .sortedKeys - let encodeData = try? encoder.encode(contentEncodedRolloutAssignments) - if let data = encodeData, let returnString = String(data: data, encoding: .utf8) { - return returnString + /// Return string format: [{RolloutAssignment1}, {RolloutAssignment2}, {RolloutAssignment3}...] + /// This will get insert 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) } // TODO(themisw): Hook into core logging functions @@ -81,4 +100,34 @@ private extension CrashlyticsRemoteConfigManager { return validatedAssignments } + + // Helper for later convert Data to String. Because `FIRCLSFileWriteStringUnquoted` takes string + // as input + func getRolloutAssignmentsEncodedJsonData() -> 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 index 53e29a198c2..725b63050ec 100644 --- a/Crashlytics/Crashlytics/Rollouts/EncodedRolloutAssignment.swift +++ b/Crashlytics/Crashlytics/Rollouts/EncodedRolloutAssignment.swift @@ -15,6 +15,16 @@ 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 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..70e99939ea8 --- /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 // Cocoapod + +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 index 4f175060ccd..097d24fdd0c 100644 --- a/Crashlytics/UnitTestsSwift/CrashlyticsRemoteConfigManagerTests.swift +++ b/Crashlytics/UnitTestsSwift/CrashlyticsRemoteConfigManagerTests.swift @@ -25,6 +25,10 @@ class RemoteConfigConfigMock: RemoteConfigInterop { for namespace: String) {} } +class PersistanceManagerMock: CrashlyticsPersistenceLog { + func updateRolloutsStateToPersistence(rollouts: Data, reportID: String) {} +} + final class CrashlyticsRemoteConfigManagerTests: XCTestCase { let rollouts: RolloutsState = { let assignment1 = RolloutAssignment( @@ -60,8 +64,11 @@ final class CrashlyticsRemoteConfigManagerTests: XCTestCase { let rcInterop = RemoteConfigConfigMock() func testRemoteConfigManagerProperlyProcessRolloutsState() throws { - let rcManager = CrashlyticsRemoteConfigManager(remoteConfig: rcInterop) - rcManager.updateRolloutsState(rolloutsState: rollouts) + let rcManager = CrashlyticsRemoteConfigManager( + remoteConfig: rcInterop, + persistenceDelegate: PersistanceManagerMock() + ) + rcManager.updateRolloutsState(rolloutsState: rollouts, reportID: "12R") XCTAssertEqual(rcManager.rolloutAssignment.count, 2) for assignment in rollouts.assignments { @@ -78,10 +85,51 @@ final class CrashlyticsRemoteConfigManagerTests: XCTestCase { let expectedString = "[{\"parameter_key\":\"6d795f66656174757265\",\"parameter_value\":\"e8bf99e698af7468656d6973e79a84e6b58be8af95e695b0e68daeefbc8ce8be93e585a5e4b8ade69687\",\"rollout_id\":\"726f6c6c6f75745f31\",\"template_version\":1,\"variant_id\":\"636f6e74726f6c\"}]" - let rcManager = CrashlyticsRemoteConfigManager(remoteConfig: rcInterop) - rcManager.updateRolloutsState(rolloutsState: singleRollout) + let rcManager = CrashlyticsRemoteConfigManager( + remoteConfig: rcInterop, + persistenceDelegate: PersistanceManagerMock() + ) + rcManager.updateRolloutsState(rolloutsState: singleRollout, reportID: "456") - let string = rcManager.getRolloutAssignmentsEncodedJson() + 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) + } } From 3d9a9a7c817be6bd47eb8b41809766248a561c10 Mon Sep 17 00:00:00 2001 From: Doudou Nan <146472823+ddnan@users.noreply.github.com> Date: Tue, 30 Jan 2024 11:37:25 -0800 Subject: [PATCH 08/19] [Rollouts] Set active rollout metadata after activating config (#12316) --- .../Sources/FIRRemoteConfig.m | 6 ++ .../Sources/Private/RCNConfigSettings.h | 9 ++- .../Sources/RCNConfigConstants.h | 4 +- .../Sources/RCNConfigContent.h | 3 + .../Sources/RCNConfigContent.m | 19 +++++ FirebaseRemoteConfig/Sources/RCNConfigFetch.m | 2 +- .../Sources/RCNConfigSettings.m | 10 ++- .../Sources/RCNUserDefaultsManager.h | 4 +- .../Sources/RCNUserDefaultsManager.m | 19 ++++- .../Tests/Unit/RCNConfigContentTest.m | 77 ++++++++++++++++--- .../Tests/Unit/RCNUserDefaultsManagerTests.m | 27 +++++-- 11 files changed, 156 insertions(+), 24 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index 4035d558707..098bde00a9c 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -329,6 +329,12 @@ - (void)activateWithCompletion:(FIRRemoteConfigActivateChangeCompletion)completi // New config has been activated at this point FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000069", @"Config activated."); [strongSelf->_configContent activatePersonalization]; + // Update activeRolloutMetadata + [strongSelf->_configContent activateRolloutMetadata]; + // Update last active template version number in setting and userDefaults. + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [strongSelf->_settings updateLastActiveTemplateVersion]; + }); // Update experiments only for 3p namespace NSString *namespace = [strongSelf->_FIRNamespace substringToIndex:[strongSelf->_FIRNamespace rangeOfString:@":"].location]; 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 e3cd99088a9..e6ff7dfe033 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigConstants.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigConstants.h @@ -60,5 +60,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..b14c2a8aceb 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.h @@ -65,6 +65,9 @@ typedef NS_ENUM(NSInteger, RCNDBSource) { /// Gets the active config and Personalization metadata. - (NSDictionary *)getConfigAndMetadataForNamespace:(NSString *)FIRNamespace; +/// Sets the fetched rollout metadata to active and return the active rollout metadata. +- (NSArray *)activateRolloutMetadata; + /// 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 f28ee662b34..0d4da6d7d30 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.m @@ -280,6 +280,7 @@ - (void)updateConfigContentWithResponse:(NSDictionary *)response [self handleUpdateStateForConfigNamespace:currentNamespace withEntries:response[RCNFetchResponseKeyEntries]]; [self handleUpdatePersonalization:response[RCNFetchResponseKeyPersonalizationMetadata]]; + [self handleUpdateRolloutFetchedMetadata:response[RCNFetchResponseKeyRolloutMetadata]]; return; } } @@ -290,6 +291,14 @@ - (void)activatePersonalization { fromSource:RCNDBSourceActive]; } +- (NSArray *)activateRolloutMetadata { + _activeRolloutMetadata = _fetchedRolloutMetadata; + [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyActiveMetadata + value:_activeRolloutMetadata + completionHandler:nil]; + return _activeRolloutMetadata; +} + #pragma mark State handling - (void)handleNoChangeStateForConfigNamespace:(NSString *)currentNamespace { if (!_fetchedConfig[currentNamespace]) { @@ -353,6 +362,16 @@ - (void)handleUpdatePersonalization:(NSDictionary *)metadata { [_DBManager insertOrUpdatePersonalizationConfig:metadata fromSource:RCNDBSourceFetched]; } +- (void)handleUpdateRolloutFetchedMetadata:(NSArray *)metadata { + if (!metadata) { + return; + } + _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 diff --git a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m index c3a0f16ddd8..d535738f91d 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m @@ -105,7 +105,7 @@ - (instancetype)initWithContent:(RCNConfigContent *)content _content = content; _fetchSession = [self newFetchSession]; _options = options; - _templateVersionNumber = [self->_settings lastTemplateVersion]; + _templateVersionNumber = [self->_settings lastFetchedTemplateVersion]; } return self; } diff --git a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m index 0b3e3ad1164..5672351a7ee 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,7 @@ - (void)updateMetadataWithFetchSuccessStatus:(BOOL)fetchSuccess [self updateLastFetchTimeInterval:[[NSDate date] timeIntervalSince1970]]; // Note: We expect the googleAppID to always be available. _deviceContext = FIRRemoteConfigDeviceContextWithProjectIdentifier(_googleAppID); - [_userDefaultsManager setLastTemplateVersion:templateVersion]; + [_userDefaultsManager setLastFetchedTemplateVersion:templateVersion]; } [self updateMetadataTable]; @@ -377,6 +378,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/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/Unit/RCNConfigContentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m index 7c7290e7551..edf1dd2ae8b 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m @@ -332,7 +332,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 +367,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 +394,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 +422,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 +444,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 +469,51 @@ - (void)testConfigUpdate_p13nMetadataUpdated_returnsKey { XCTAssertTrue([[update updatedKeys] containsObject:existingParam]); } +- (void)testConfigUpdate_rolloutMetadataUpdated_returnsKey { + NSString *namespace = @"test_namespace"; + NSString *key = @"key1"; + NSString *value1 = @"value1"; + NSString *value2 = @"value2"; + NSString *rolloutId1 = @"1"; + NSString *variantId1 = @"A"; + NSString *variantId2 = @"B"; + NSArray *rolloutMetadata = @[ + @{@"rollout_id" : rolloutId1, @"variant_id" : variantId1, @"affected_parameter_keys" : @[ key ]} + ]; + // variant_id changed + NSArray *updatedRolloutMetadata = @[ + @{@"rollout_id" : rolloutId1, @"variant_id" : variantId2, @"affected_parameter_keys" : @[ key ]} + ]; + + // Populate fetched config + NSMutableDictionary *fetchResponse = [self createFetchResponseWithConfigEntries:@{key : value1} + p13nMetadata:nil + rolloutMetadata:rolloutMetadata]; + [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; + // populate active config with the same content + NSArray *result = [_configContent activateRolloutMetadata]; + XCTAssertEqualObjects(rolloutMetadata, result); + FIRRemoteConfigValue *rcValue1 = + [[FIRRemoteConfigValue alloc] initWithData:[value1 dataUsingEncoding:NSUTF8StringEncoding] + source:FIRRemoteConfigSourceRemote]; + + NSDictionary *namespaceToConfig = @{namespace : @{key : rcValue1}}; + [_configContent copyFromDictionary:namespaceToConfig + toSource:RCNDBSourceActive + forNamespace:namespace]; + // New fetch response has updated rollout metadata + NSMutableDictionary *fetchResponse2 = + [self createFetchResponseWithConfigEntries:@{key : value2} + p13nMetadata:nil + rolloutMetadata:updatedRolloutMetadata]; + [_configContent updateConfigContentWithResponse:fetchResponse2 forNamespace:namespace]; + + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; + + XCTAssertTrue([update updatedKeys].count == 1); + XCTAssertTrue([[update updatedKeys] containsObject:key]); +} + - (void)testConfigUpdate_valueSourceChanged_returnsKey { NSString *namespace = @"test_namespace"; NSString *existingParam = @"key1"; @@ -477,7 +530,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 +544,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/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 { From d974256976e479ba87ec427d53653050ef48041c Mon Sep 17 00:00:00 2001 From: Doudou Nan <146472823+ddnan@users.noreply.github.com> Date: Fri, 2 Feb 2024 10:00:24 -0800 Subject: [PATCH 09/19] [Rollouts] Diff rollout metadata for updatedKeys in RemoteConfigUpdater (#12333) --- .../Sources/RCNConfigConstants.h | 6 ++ .../Sources/RCNConfigContent.m | 44 +++++++++ .../Tests/Unit/RCNConfigContentTest.m | 90 +++++++++++++++---- 3 files changed, 122 insertions(+), 18 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigConstants.h b/FirebaseRemoteConfig/Sources/RCNConfigConstants.h index e6ff7dfe033..51d248c4106 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigConstants.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigConstants.h @@ -39,6 +39,12 @@ static NSString *const RCNFetchResponseKeyExperimentDescriptions = @"experimentD 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. diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.m b/FirebaseRemoteConfig/Sources/RCNConfigContent.m index 0d4da6d7d30..7746651b8d2 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.m @@ -441,6 +441,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]) { @@ -469,8 +471,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/Tests/Unit/RCNConfigContentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m index edf1dd2ae8b..43cdda1778c 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m @@ -471,47 +471,101 @@ - (void)testConfigUpdate_p13nMetadataUpdated_returnsKey { - (void)testConfigUpdate_rolloutMetadataUpdated_returnsKey { NSString *namespace = @"test_namespace"; - NSString *key = @"key1"; - NSString *value1 = @"value1"; - NSString *value2 = @"value2"; + NSString *key1 = @"key1"; + NSString *key2 = @"kety2"; + NSString *value = @"value"; NSString *rolloutId1 = @"1"; + NSString *rolloutId2 = @"2"; NSString *variantId1 = @"A"; NSString *variantId2 = @"B"; - NSArray *rolloutMetadata = @[ - @{@"rollout_id" : rolloutId1, @"variant_id" : variantId1, @"affected_parameter_keys" : @[ key ]} - ]; - // variant_id changed + NSArray *rolloutMetadata = @[ @{ + RCNFetchResponseKeyRolloutID : rolloutId1, + RCNFetchResponseKeyVariantID : variantId1, + RCNFetchResponseKeyAffectedParameterKeys : @[ key1 ] + } ]; + // Update rolltou metadata NSArray *updatedRolloutMetadata = @[ - @{@"rollout_id" : rolloutId1, @"variant_id" : variantId2, @"affected_parameter_keys" : @[ key ]} + @{ + RCNFetchResponseKeyRolloutID : rolloutId1, + RCNFetchResponseKeyVariantID : variantId2, + RCNFetchResponseKeyAffectedParameterKeys : @[ key1 ] + }, + @{ + RCNFetchResponseKeyRolloutID : rolloutId2, + RCNFetchResponseKeyVariantID : variantId1, + RCNFetchResponseKeyAffectedParameterKeys : @[ key2 ] + }, ]; - // Populate fetched config - NSMutableDictionary *fetchResponse = [self createFetchResponseWithConfigEntries:@{key : value1} + NSMutableDictionary *fetchResponse = [self createFetchResponseWithConfigEntries:@{key1 : value} p13nMetadata:nil rolloutMetadata:rolloutMetadata]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; // populate active config with the same content NSArray *result = [_configContent activateRolloutMetadata]; XCTAssertEqualObjects(rolloutMetadata, result); - FIRRemoteConfigValue *rcValue1 = - [[FIRRemoteConfigValue alloc] initWithData:[value1 dataUsingEncoding:NSUTF8StringEncoding] + FIRRemoteConfigValue *rcValue = + [[FIRRemoteConfigValue alloc] initWithData:[value dataUsingEncoding:NSUTF8StringEncoding] source:FIRRemoteConfigSourceRemote]; - NSDictionary *namespaceToConfig = @{namespace : @{key : rcValue1}}; + NSDictionary *namespaceToConfig = @{namespace : @{key1 : rcValue}}; [_configContent copyFromDictionary:namespaceToConfig toSource:RCNDBSourceActive forNamespace:namespace]; // New fetch response has updated rollout metadata - NSMutableDictionary *fetchResponse2 = - [self createFetchResponseWithConfigEntries:@{key : value2} + [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:updatedRolloutMetadata]; - [_configContent updateConfigContentWithResponse:fetchResponse2 forNamespace:namespace]; + rolloutMetadata:rolloutMetadata]; + [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; + // populate active config with the same content + NSArray *result = [_configContent activateRolloutMetadata]; + XCTAssertEqualObjects(rolloutMetadata, result); + 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:key]); + XCTAssertTrue([[update updatedKeys] containsObject:key2]); } - (void)testConfigUpdate_valueSourceChanged_returnsKey { From 09d4d564643fbbb5b5d43a90acc124ec86ff9d6d Mon Sep 17 00:00:00 2001 From: Doudou Nan <146472823+ddnan@users.noreply.github.com> Date: Mon, 5 Feb 2024 19:03:04 -0800 Subject: [PATCH 10/19] [Rollouts]Add remote config logic to featureRollouts test app (#12349) --- .../project.pbxproj | 6 +++ .../ContentView.swift | 2 + .../ContentView.swift | 9 +--- .../Shared/RemoteConfigButtonView.swift | 51 +++++++++++++++++++ 4 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/RemoteConfigButtonView.swift diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp.xcodeproj/project.pbxproj b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp.xcodeproj/project.pbxproj index 65d88ebcaa9..d7f955d7c07 100644 --- a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp.xcodeproj/project.pbxproj +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* 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 */; }; @@ -76,6 +78,7 @@ 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 = ""; }; @@ -204,6 +207,7 @@ children = ( C49C48A02B47261000BC1456 /* GoogleService-Info.plist */, C427C4A22B4603F60088A488 /* FeatureRolloutsTestAppApp.swift */, + 951D70142B71AD9A00BE7EED /* RemoteConfigButtonView.swift */, C49C489D2B4722C100BC1456 /* CrashButtonView.swift */, ); path = Shared; @@ -615,6 +619,7 @@ buildActionMask = 2147483647; files = ( C49C48702B4704F300BC1456 /* FeatureRolloutsTestAppApp.swift in Sources */, + 951D70162B71AD9B00BE7EED /* RemoteConfigButtonView.swift in Sources */, C49C48992B4720AE00BC1456 /* ContentView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -623,6 +628,7 @@ 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 */, diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/ContentView.swift b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/ContentView.swift index f2b83652da4..ac68e43b8a8 100644 --- a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/ContentView.swift +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/ContentView.swift @@ -21,5 +21,7 @@ struct ContentView: View { var body: some View { CrashButtonView() .padding() + RemoteConfigButtonView() + .padding() } } diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_RemoteConfig_iOS/ContentView.swift b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_RemoteConfig_iOS/ContentView.swift index 5dfea79becb..51e437b030a 100644 --- a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_RemoteConfig_iOS/ContentView.swift +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_RemoteConfig_iOS/ContentView.swift @@ -19,12 +19,7 @@ import SwiftUI struct ContentView: View { var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") - } - .padding() + RemoteConfigButtonView() + .padding() } } 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") + } + } +} From 5cfc356994f5e8ce09cfbfacba63203b35ccc981 Mon Sep 17 00:00:00 2001 From: Doudou Nan <146472823+ddnan@users.noreply.github.com> Date: Mon, 12 Feb 2024 15:48:05 -0800 Subject: [PATCH 11/19] [Rollouts] Notify RolloutsState change to Interop subscriber (#12334) --- Crashlytics/Crashlytics/FIRCrashlytics.m | 26 ++--- .../Interop/RemoteConfigConstants.swift | 21 ++++ .../Sources/FIRRemoteConfig.m | 110 +++++++++++++++--- .../Sources/FIRRemoteConfigComponent.m | 4 +- .../Sources/Private/FIRRemoteConfig_Private.h | 4 + .../Sources/RCNConfigContent.h | 6 +- .../Sources/RCNConfigContent.m | 14 ++- FirebaseRemoteConfig/Sources/RCNConfigFetch.m | 3 +- .../Sources/RCNConfigSettings.m | 1 + FirebaseRemoteConfig/Sources/RCNConstants3P.m | 20 ---- .../RemoteConfigSampleApp/ViewController.m | 9 +- .../Tests/Unit/RCNConfigContentTest.m | 82 ++++++++++--- .../Tests/Unit/RCNConfigTest.m | 33 +++--- .../Tests/Unit/RCNInstanceIDTest.m | 4 +- .../Tests/Unit/RCNRemoteConfigTest.m | 63 +++++++++- .../Tests/Unit/RCNThrottlingTests.m | 23 ++-- ...ebaseRemoteConfigSwift_APIBuildTests.swift | 3 +- 17 files changed, 313 insertions(+), 113 deletions(-) create mode 100644 FirebaseRemoteConfig/Interop/RemoteConfigConstants.swift delete mode 100644 FirebaseRemoteConfig/Sources/RCNConstants3P.m diff --git a/Crashlytics/Crashlytics/FIRCrashlytics.m b/Crashlytics/Crashlytics/FIRCrashlytics.m index f5c606929ac..1e6e93e540f 100644 --- a/Crashlytics/Crashlytics/FIRCrashlytics.m +++ b/Crashlytics/Crashlytics/FIRCrashlytics.m @@ -171,19 +171,6 @@ - (instancetype)initWithApp:(FIRApp *)app [sessions registerWithSubscriber:self]; } - if (remoteConfig) { - FIRCLSDebugLog(@"Registering RemoteConfig SDK subscription for rollouts data"); - - FIRCLSRolloutsPersistenceManager *persistenceManager = - [[FIRCLSRolloutsPersistenceManager alloc] initWithFileManager:_fileManager]; - _remoteConfigManager = - [[FIRCLSRemoteConfigManager alloc] initWithRemoteConfig:remoteConfig - persistenceDelegate:persistenceManager]; - - // TODO(themisw): Import "firebase" from the interop in the future. - [remoteConfig registerRolloutsStateSubscriber:self for:@"firebase"]; - } - _reportUploader = [[FIRCLSReportUploader alloc] initWithManagerData:_managerData]; _existingReportManager = @@ -216,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; } 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/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index 098bde00a9c..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,16 +333,20 @@ - (void)activateWithCompletion:(FIRRemoteConfigActivateChangeCompletion)completi // New config has been activated at this point FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000069", @"Config activated."); [strongSelf->_configContent activatePersonalization]; - // Update activeRolloutMetadata - [strongSelf->_configContent activateRolloutMetadata]; // Update last active template version number in setting and userDefaults. - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [strongSelf->_settings updateLastActiveTemplateVersion]; - }); + [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]; }); @@ -383,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 { @@ -408,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; } @@ -619,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.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m index 08927453adb..81055451ae4 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m @@ -148,8 +148,8 @@ + (void)load { - (void)registerRolloutsStateSubscriber:(id)subscriber for:(NSString * _Nonnull)namespace { - // TODO(Themisw): Adding the registered subscriber reference to the namespace instance - // [self.instances[namespace] addRemoteConfigInteropSubscriber:subscriber]; + 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/RCNConfigContent.h b/FirebaseRemoteConfig/Sources/RCNConfigContent.h index b14c2a8aceb..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,8 +67,8 @@ typedef NS_ENUM(NSInteger, RCNDBSource) { /// Gets the active config and Personalization metadata. - (NSDictionary *)getConfigAndMetadataForNamespace:(NSString *)FIRNamespace; -/// Sets the fetched rollout metadata to active and return the active rollout metadata. -- (NSArray *)activateRolloutMetadata; +/// 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 7746651b8d2..1c266734c40 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.m @@ -291,12 +291,13 @@ - (void)activatePersonalization { fromSource:RCNDBSourceActive]; } -- (NSArray *)activateRolloutMetadata { +- (void)activateRolloutMetadata:(void (^)(BOOL success))completionHandler { _activeRolloutMetadata = _fetchedRolloutMetadata; [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyActiveMetadata value:_activeRolloutMetadata - completionHandler:nil]; - return _activeRolloutMetadata; + completionHandler:^(BOOL success, NSDictionary *result) { + completionHandler(success); + }]; } #pragma mark State handling @@ -364,7 +365,7 @@ - (void)handleUpdatePersonalization:(NSDictionary *)metadata { - (void)handleUpdateRolloutFetchedMetadata:(NSArray *)metadata { if (!metadata) { - return; + metadata = [[NSArray alloc] init]; } _fetchedRolloutMetadata = metadata; [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyFetchedMetadata @@ -399,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. diff --git a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m index d535738f91d..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 = @@ -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 5672351a7ee..e85a63f4873 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m @@ -293,6 +293,7 @@ - (void)updateMetadataWithFetchSuccessStatus:(BOOL)fetchSuccess [self updateLastFetchTimeInterval:[[NSDate date] timeIntervalSince1970]]; // Note: We expect the googleAppID to always be available. _deviceContext = FIRRemoteConfigDeviceContextWithProjectIdentifier(_googleAppID); + _lastFetchedTemplateVersion = templateVersion; [_userDefaultsManager setLastFetchedTemplateVersion:templateVersion]; } diff --git a/FirebaseRemoteConfig/Sources/RCNConstants3P.m b/FirebaseRemoteConfig/Sources/RCNConstants3P.m deleted file mode 100644 index 6bd5d78d094..00000000000 --- a/FirebaseRemoteConfig/Sources/RCNConstants3P.m +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2019 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h" - -/// Firebase Remote Config service default namespace. -NSString *const FIRNamespaceGoogleMobilePlatform = @"firebase"; 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/Unit/RCNConfigContentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m index 43cdda1778c..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; @@ -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"); } @@ -502,8 +505,8 @@ - (void)testConfigUpdate_rolloutMetadataUpdated_returnsKey { rolloutMetadata:rolloutMetadata]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; // populate active config with the same content - NSArray *result = [_configContent activateRolloutMetadata]; - XCTAssertEqualObjects(rolloutMetadata, result); + [_configContent activateRolloutMetadata:nil]; + XCTAssertEqualObjects(rolloutMetadata, _configContent.activeRolloutMetadata); FIRRemoteConfigValue *rcValue = [[FIRRemoteConfigValue alloc] initWithData:[value dataUsingEncoding:NSUTF8StringEncoding] source:FIRRemoteConfigSourceRemote]; @@ -548,8 +551,8 @@ - (void)testConfigUpdate_rolloutMetadataDeleted_returnsKey { rolloutMetadata:rolloutMetadata]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; // populate active config with the same content - NSArray *result = [_configContent activateRolloutMetadata]; - XCTAssertEqualObjects(rolloutMetadata, result); + [_configContent activateRolloutMetadata:nil]; + XCTAssertEqualObjects(rolloutMetadata, _configContent.activeRolloutMetadata); FIRRemoteConfigValue *rcValue = [[FIRRemoteConfigValue alloc] initWithData:[value dataUsingEncoding:NSUTF8StringEncoding] source:FIRRemoteConfigSourceRemote]; @@ -568,6 +571,49 @@ - (void)testConfigUpdate_rolloutMetadataDeleted_returnsKey { 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"; 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 6bf23d0777f..e02b8ecaabf 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m @@ -32,6 +32,9 @@ #import #import "FirebaseCore/Extension/FirebaseCoreInternal.h" +@import FirebaseRemoteConfigInterop; + +@protocol FIRRolloutsStateSubscriber; @interface RCNConfigFetch (ForTest) - (instancetype)initWithContent:(RCNConfigContent *)content @@ -131,6 +134,7 @@ @interface RCNRemoteConfigTest : XCTestCase { NSTimeInterval _checkCompletionTimeout; NSMutableArray *_configInstances; NSMutableArray *> *_entries; + NSArray *_rolloutMetadata; NSMutableArray *> *_response; NSMutableArray *_responseData; NSMutableArray *_URLResponse; @@ -146,6 +150,7 @@ @interface RCNRemoteConfigTest : XCTestCase { NSString *_fullyQualifiedNamespace; RCNConfigSettings *_settings; dispatch_queue_t _queue; + NSString *_namespaceGoogleMobilePlatform; } @end @@ -181,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++) { @@ -205,7 +211,7 @@ - (void)setUp { case RCNTestRCInstanceSecondApp: currentAppName = RCNTestsSecondFIRAppName; currentOptions = [self secondAppOptions]; - currentNamespace = FIRNamespaceGoogleMobilePlatform; + currentNamespace = _namespaceGoogleMobilePlatform; break; case RCNTestRCInstanceDefault: default: @@ -260,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]; @@ -596,7 +612,7 @@ - (void)testFetchConfigsFailed { case RCNTestRCInstanceSecondApp: currentAppName = RCNTestsSecondFIRAppName; currentOptions = [self secondAppOptions]; - currentNamespace = FIRNamespaceGoogleMobilePlatform; + currentNamespace = _namespaceGoogleMobilePlatform; break; case RCNTestRCInstanceDefault: default: @@ -709,7 +725,7 @@ - (void)testFetchConfigsFailedErrorNoNetwork { case RCNTestRCInstanceSecondApp: currentAppName = RCNTestsSecondFIRAppName; currentOptions = [self secondAppOptions]; - currentNamespace = FIRNamespaceGoogleMobilePlatform; + currentNamespace = _namespaceGoogleMobilePlatform; break; case RCNTestRCInstanceDefault: default: @@ -913,7 +929,7 @@ - (void)testActivateOnFetchNoChangeStatus { case RCNTestRCInstanceSecondApp: currentAppName = RCNTestsSecondFIRAppName; currentOptions = [self secondAppOptions]; - currentNamespace = FIRNamespaceGoogleMobilePlatform; + currentNamespace = _namespaceGoogleMobilePlatform; break; case RCNTestRCInstanceDefault: default: @@ -1784,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/FirebaseRemoteConfigSwift/Tests/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift index b695e375887..ccf78ab68c0 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 { @@ -23,7 +24,7 @@ final class FirebaseRemoteConfigSwift_APIBuildTests: XCTestCase { // MARK: - FirebaseRemoteConfig // TODO(ncooke3): These global constants should be lowercase. - let _: String = FirebaseRemoteConfig.NamespaceGoogleMobilePlatform + let _: String = RemoteConfigConstants.NamespaceGoogleMobilePlatform let _: String = FirebaseRemoteConfig.RemoteConfigThrottledEndTimeInSecondsKey // TODO(ncooke3): This should probably not be initializable. From 6030c01b724e253f1c60594cc706b1e92a8c164c Mon Sep 17 00:00:00 2001 From: themiswang Date: Wed, 21 Feb 2024 16:07:56 -0500 Subject: [PATCH 12/19] [Rollouts] Address feature branch comments (#12411) --- ClientApp/Podfile | 46 ------------------- .../Podfile | 37 --------------- .../FIRCLSRolloutsPersistenceManager.h | 3 +- .../FIRCLSRolloutsPersistenceManager.m | 7 ++- Crashlytics/Crashlytics/FIRCrashlytics.m | 2 +- .../Crashlytics/Handlers/FIRCLSException.mm | 2 +- .../CrashlyticsRemoteConfigManager.swift | 18 ++++++-- Crashlytics/UnitTests/FIRCLSFileTests.m | 2 +- .../FIRCLSRolloutsPersistenceManagerTests.m | 2 +- FirebaseCrashlytics.podspec | 2 +- FirebaseRemoteConfig.podspec | 2 +- FirebaseRemoteConfigInterop.podspec | 4 +- IntegrationTesting/ClientApp/Podfile | 1 + .../Podfile | 1 + 14 files changed, 31 insertions(+), 98 deletions(-) delete mode 100644 ClientApp/Podfile delete mode 100644 CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Podfile diff --git a/ClientApp/Podfile b/ClientApp/Podfile deleted file mode 100644 index 3a69069714f..00000000000 --- a/ClientApp/Podfile +++ /dev/null @@ -1,46 +0,0 @@ -source 'https://github.com/firebase/SpecsDev.git' -source 'https://github.com/firebase/SpecsStaging.git' -source 'https://cdn.cocoapods.org/' - -target 'ClientApp-CocoaPods' do - platform :ios, '11.0' - - use_frameworks! - - pod 'FirebaseCore', :path => '../' - pod 'FirebaseInstallations', :path => '../' - pod 'FirebaseAnalytics' # Binary pods don't work with `:path`. - pod 'FirebaseAnalyticsOnDeviceConversion', :path => '../' - pod 'FirebaseABTesting', :path => '../' - pod 'FirebaseAppCheck', :path => '../' - pod 'FirebaseRemoteConfig', :path => '../' - pod 'FirebaseRemoteConfigSwift', :path => '../' - pod 'FirebaseRemoteConfigInterop', :path => '../' - pod 'FirebaseAppDistribution', :path => '../' - pod 'FirebaseAuth', :path => '../' - pod 'FirebaseCrashlytics', :path => '../' - pod 'FirebaseDatabase', :path => '../' - pod 'FirebaseDatabaseSwift', :path => '../' - pod 'FirebaseDynamicLinks', :path => '../' - pod 'FirebaseFirestore', :path => '../' - pod 'FirebaseFirestoreSwift', :path => '../' - pod 'FirebaseFunctions', :path => '../' - pod 'FirebaseInAppMessaging', :path => '../' - pod 'FirebaseMessaging', :path => '../' - pod 'FirebasePerformance', :path => '../' - pod 'FirebaseStorage', :path => '../' - pod 'FirebaseMLModelDownloader', :path => '../' - pod 'Firebase', :path => '../' -end - -target 'ClientApp-CocoaPods-iOS13' do - platform :ios, '13.0' - - use_frameworks! - - pod 'FirebaseAnalytics' # Binary pods don't work with `:path`. - pod 'FirebaseAnalyticsSwift', :path => '../' # Requires iOS 13.0+ - pod 'FirebaseInAppMessaging', :path => '../' - pod 'FirebaseInAppMessagingSwift', :path => '../' # Requires iOS 13.0+ - -end diff --git a/CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Podfile b/CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Podfile deleted file mode 100644 index 9741e179cf3..00000000000 --- a/CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Podfile +++ /dev/null @@ -1,37 +0,0 @@ -source 'https://github.com/firebase/SpecsDev.git' -source 'https://github.com/firebase/SpecsStaging.git' -source 'https://cdn.cocoapods.org/' - -# Uncomment the next line to define a global platform for your project -platform :ios, '11.0' - -target 'CocoapodsIntegrationTest' do - # Comment the next line if you don't want to use dynamic frameworks - use_frameworks! - pod 'FirebaseABTesting', :path => '../' - pod 'FirebaseAppDistribution', :path => '../' - pod 'FirebaseAppCheckInterop', :path => '../' - pod 'FirebaseCore', :path => '../' - pod 'FirebaseCoreExtension', :path => '../' - pod 'FirebaseCoreInternal', :path => '../' - pod 'FirebaseCrashlytics', :path => '../' - pod 'FirebaseAuth', :path => '../' - pod 'FirebaseAuthInterop', :path => '../' - pod 'FirebaseDatabase', :path => '../' - pod 'FirebaseDynamicLinks', :path => '../' - pod 'FirebaseFirestore', :path => '../' - pod 'FirebaseFunctions', :path => '../' - pod 'FirebaseInAppMessaging', :path => '../' - pod 'FirebaseInstallations', :path => '../' - pod 'FirebaseMessaging', :path => '../' - pod 'FirebaseMessagingInterop', :path => '../' - pod 'FirebaseRemoteConfigInterop', :path => '../' - pod 'FirebasePerformance', :path => '../' - pod 'FirebaseStorage', :path => '../' -end - -# Using the new speed-enhancing features available with CocoaPods 1.7+ -# [sudo] gem install cocoapods --pre -install! 'cocoapods', - :generate_multiple_pod_projects => true, - :incremental_installation => true diff --git a/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h index 83c7f25ca8b..c84c4d34c70 100644 --- a/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h +++ b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h @@ -16,7 +16,7 @@ @import FirebaseCrashlyticsSwift; #else // Swift Package Manager #import -#endif // Cocoapod +#endif // CocoaPods @interface FIRCLSRolloutsPersistenceManager : NSObject @@ -26,4 +26,5 @@ - (void)updateRolloutsStateToPersistenceWithRollouts:(NSData *_Nonnull)rollouts reportID:(NSString *_Nonnull)reportID; +- (void)debugLog:(NSString *_Nonnull)messages; @end diff --git a/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m index c0c0a38ed6b..21094df4281 100644 --- a/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m +++ b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m @@ -18,11 +18,12 @@ #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 // Cocoapod +#endif // CocoaPods @interface FIRCLSRolloutsPersistenceManager : NSObject @property(nonatomic, readonly) FIRCLSFileManager *fileManager; @@ -58,4 +59,8 @@ - (void)updateRolloutsStateToPersistenceWithRollouts:(NSData *_Nonnull)rollouts [rolloutsFile writeData:newLineData]; }); } + +- (void)debugLog:(NSString *_Nonnull)messages { + FIRCLSDebugLog(messages); +} @end diff --git a/Crashlytics/Crashlytics/FIRCrashlytics.m b/Crashlytics/Crashlytics/FIRCrashlytics.m index 1e6e93e540f..85502b2a9d9 100644 --- a/Crashlytics/Crashlytics/FIRCrashlytics.m +++ b/Crashlytics/Crashlytics/FIRCrashlytics.m @@ -65,7 +65,7 @@ @import FirebaseCrashlyticsSwift; #else // Swift Package Manager #import -#endif // Cocoapod +#endif // CocoaPods #if TARGET_OS_IPHONE #import diff --git a/Crashlytics/Crashlytics/Handlers/FIRCLSException.mm b/Crashlytics/Crashlytics/Handlers/FIRCLSException.mm index 5a43a83834a..b92cd9848dd 100644 --- a/Crashlytics/Crashlytics/Handlers/FIRCLSException.mm +++ b/Crashlytics/Crashlytics/Handlers/FIRCLSException.mm @@ -279,7 +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. + // Also copy rollouts.clsrecord if applicable. NSError *error = nil; BOOL copied = [fileManager.underlyingFileManager copyItemAtPath:currentReportPath toPath:newReportPath diff --git a/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift b/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift index b16396fe59c..d6d5cb16b82 100644 --- a/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift +++ b/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift @@ -18,6 +18,7 @@ import Foundation @objc(FIRCLSPersistenceLog) public protocol CrashlyticsPersistenceLog { func updateRolloutsStateToPersistence(rollouts: Data, reportID: String) + func debugLog(message: String) } @objc(FIRCLSRemoteConfigManager) @@ -49,7 +50,7 @@ public class CrashlyticsRemoteConfigManager: NSObject { _rolloutAssignment = normalizeRolloutAssignment(assignments: Array(rolloutsState.assignments)) lock.unlock() - // writring to persistence + // Writring to persistence if let rolloutsData = getRolloutsStateEncodedJsonData() { persistenceDelegate.updateRolloutsStateToPersistence( @@ -60,7 +61,7 @@ public class CrashlyticsRemoteConfigManager: NSObject { } /// Return string format: [{RolloutAssignment1}, {RolloutAssignment2}, {RolloutAssignment3}...] - /// This will get insert into each clsrcord for non-fatal events. + /// 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() @@ -68,8 +69,12 @@ public class CrashlyticsRemoteConfigManager: NSObject { return String(data: data, encoding: .utf8) } - // TODO(themisw): Hook into core logging functions - debugPrint("Failed to serialize rollouts", encodeData ?? "nil") + let debugInfo = encodeData?.debugDescription ?? "nil" + persistenceDelegate.debugLog(message: String( + format: "Failed to serialize rollouts: %@", + arguments: [debugInfo] + )) + return nil } } @@ -78,7 +83,10 @@ private extension CrashlyticsRemoteConfigManager { func normalizeRolloutAssignment(assignments: [RolloutAssignment]) -> [RolloutAssignment] { var validatedAssignments = assignments if assignments.count > CrashlyticsRemoteConfigManager.maxRolloutAssignments { - debugPrint("Rollouts excess the maximum number of assignments can pass to Crashlytics") + persistenceDelegate + .debugLog( + message: "Rollouts excess the maximum number of assignments can pass to Crashlytics" + ) validatedAssignments = Array(assignments[.. -#endif // Cocoapod +#endif // CocoaPods #import diff --git a/Crashlytics/UnitTests/FIRCLSRolloutsPersistenceManagerTests.m b/Crashlytics/UnitTests/FIRCLSRolloutsPersistenceManagerTests.m index 70e99939ea8..aec030d7538 100644 --- a/Crashlytics/UnitTests/FIRCLSRolloutsPersistenceManagerTests.m +++ b/Crashlytics/UnitTests/FIRCLSRolloutsPersistenceManagerTests.m @@ -23,7 +23,7 @@ @import FirebaseCrashlyticsSwift; #else // Swift Package Manager #import -#endif // Cocoapod +#endif // CocoaPods NSString *reportId = @"1234567"; diff --git a/FirebaseCrashlytics.podspec b/FirebaseCrashlytics.podspec index c7b05afcdef..e67e886b399 100644 --- a/FirebaseCrashlytics.podspec +++ b/FirebaseCrashlytics.podspec @@ -62,7 +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.20' + s.dependency 'FirebaseRemoteConfigInterop', '~> 10.23' s.dependency 'PromisesObjC', '~> 2.1' s.dependency 'GoogleDataTransport', '~> 9.2' s.dependency 'GoogleUtilities/Environment', '~> 7.8' diff --git a/FirebaseRemoteConfig.podspec b/FirebaseRemoteConfig.podspec index 200164e466e..9767a218a60 100644 --- a/FirebaseRemoteConfig.podspec +++ b/FirebaseRemoteConfig.podspec @@ -56,7 +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.20' + s.dependency 'FirebaseRemoteConfigInterop', '~> 10.23' s.test_spec 'unit' do |unit_tests| unit_tests.scheme = { :code_coverage => true } diff --git a/FirebaseRemoteConfigInterop.podspec b/FirebaseRemoteConfigInterop.podspec index f2ce8cec9d6..86b86b24e6a 100644 --- a/FirebaseRemoteConfigInterop.podspec +++ b/FirebaseRemoteConfigInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseRemoteConfigInterop' - s.version = '10.20.0' + s.version = '10.23.0' s.summary = 'Interfaces that allow other Firebase SDKs to use Remote Config functionality.' s.description = <<-DESC @@ -21,7 +21,7 @@ Pod::Spec.new do |s| } s.swift_version = '5.3' - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false s.social_media_url = 'https://twitter.com/Firebase' 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 From 5fa75076f03d71b0ac538544534b4ced78b88803 Mon Sep 17 00:00:00 2001 From: Doudou Nan <146472823+ddnan@users.noreply.github.com> Date: Mon, 26 Feb 2024 14:29:29 -0800 Subject: [PATCH 13/19] Add Fireperf dependency to feature rollouts test app (#12430) --- FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Podfile | 1 + 1 file changed, 1 insertion(+) diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Podfile b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Podfile index c5f9ddf9e46..975c45eaa98 100644 --- a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Podfile +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Podfile @@ -7,6 +7,7 @@ def shared_pods pod 'FirebaseCoreInternal', :path => '../../../' pod 'FirebaseCoreExtension', :path => '../../../' pod 'FirebaseRemoteConfigInterop', :path => '../../../' + pod 'FirebasePerformance', :path => '../../../' end target 'FeatureRolloutsTestApp_iOS' do From 4ffcaefb1e04b6eec8d780f4b01015a8f69fe0ec Mon Sep 17 00:00:00 2001 From: themiswang Date: Mon, 4 Mar 2024 16:09:39 -0500 Subject: [PATCH 14/19] add release notes and privacy manifest for interop (#12464) --- Crashlytics/CHANGELOG.md | 1 + FirebaseRemoteConfig/CHANGELOG.md | 3 +++ ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift | 1 + 3 files changed, 5 insertions(+) diff --git a/Crashlytics/CHANGELOG.md b/Crashlytics/CHANGELOG.md index 2c06843fcea..1d3875915f4 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 information from Remote Config. # 10.22.0 - [fixed] Force validation or rotation of FIDs for FirebaseSessions. 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/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), From c528a52e0f90c24fb936a690bf03c3c387195b22 Mon Sep 17 00:00:00 2001 From: themiswang Date: Fri, 8 Mar 2024 14:35:44 -0500 Subject: [PATCH 15/19] fix unit test (#12499) --- .../UnitTestsSwift/CrashlyticsRemoteConfigManagerTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Crashlytics/UnitTestsSwift/CrashlyticsRemoteConfigManagerTests.swift b/Crashlytics/UnitTestsSwift/CrashlyticsRemoteConfigManagerTests.swift index 097d24fdd0c..6c2e070e47f 100644 --- a/Crashlytics/UnitTestsSwift/CrashlyticsRemoteConfigManagerTests.swift +++ b/Crashlytics/UnitTestsSwift/CrashlyticsRemoteConfigManagerTests.swift @@ -27,6 +27,7 @@ class RemoteConfigConfigMock: RemoteConfigInterop { class PersistanceManagerMock: CrashlyticsPersistenceLog { func updateRolloutsStateToPersistence(rollouts: Data, reportID: String) {} + func debugLog(message: String) {} } final class CrashlyticsRemoteConfigManagerTests: XCTestCase { From 5dabfd57394dae536e7f3ba0d1db5f8178e9b714 Mon Sep 17 00:00:00 2001 From: themiswang Date: Fri, 8 Mar 2024 15:13:28 -0500 Subject: [PATCH 16/19] Fix rollouts tests (#12500) --- .../Controllers/FIRCLSRolloutsPersistenceManager.h | 2 +- .../Controllers/FIRCLSRolloutsPersistenceManager.m | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h index c84c4d34c70..bda6eabbf5e 100644 --- a/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h +++ b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h @@ -26,5 +26,5 @@ - (void)updateRolloutsStateToPersistenceWithRollouts:(NSData *_Nonnull)rollouts reportID:(NSString *_Nonnull)reportID; -- (void)debugLog:(NSString *_Nonnull)messages; +- (void)debugLogWithMessage:(NSString *_Nonnull)message; @end diff --git a/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m index 21094df4281..3e7867dab76 100644 --- a/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m +++ b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m @@ -60,7 +60,8 @@ - (void)updateRolloutsStateToPersistenceWithRollouts:(NSData *_Nonnull)rollouts }); } -- (void)debugLog:(NSString *_Nonnull)messages { - FIRCLSDebugLog(messages); +- (void)debugLogWithMessage:(NSString *_Nonnull)message { + FIRCLSDebugLog(message); } + @end From 37fce03c21416d1a91abfb2677eb3203c5922421 Mon Sep 17 00:00:00 2001 From: themiswang Date: Fri, 8 Mar 2024 19:32:44 -0500 Subject: [PATCH 17/19] reword parameter name (#12502) --- FirebaseRemoteConfig/Sources/RCNConfigDBManager.h | 4 ++-- FirebaseRemoteConfig/Sources/RCNConfigDBManager.m | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h index 318c69ab122..fba094624ca 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h @@ -114,10 +114,10 @@ typedef void (^RCNDBLoadCompletion)(BOOL success, /// Insert rollout metadata in rollout table. /// @param key Key indicating whether rollout metadata is fetched or active and defined in /// RCNConfigDefines.h. -/// @param value The value that rollout metadata array. +/// @param metadataList The metadata info for each rollout entry . /// @param handler The callback. - (void)insertOrUpdateRolloutTableWithKey:(NSString *)key - value:(NSArray *)value + value:(NSArray *)metadataList completionHandler:(RCNDBCompletion)handler; /// Clear the record of given namespace and package name diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m index 823d1c29895..5b21306a85a 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m @@ -623,10 +623,10 @@ - (BOOL)insertOrUpdatePersonalizationConfig:(NSDictionary *)dataValue } - (void)insertOrUpdateRolloutTableWithKey:(NSString *)key - value:(NSArray *)value + value:(NSArray *)metadataList completionHandler:(RCNDBCompletion)handler { dispatch_async(_databaseOperationQueue, ^{ - BOOL success = [self insertOrUpdateRolloutTableWithKey:key value:value]; + BOOL success = [self insertOrUpdateRolloutTableWithKey:key value:metadataList]; if (handler) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ handler(success, nil); From 1e3b0e8ecb85b166f0d14e2c63f80c213db859d9 Mon Sep 17 00:00:00 2001 From: themiswang Date: Mon, 11 Mar 2024 13:24:15 -0400 Subject: [PATCH 18/19] edit release notes (#12515) --- Crashlytics/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Crashlytics/CHANGELOG.md b/Crashlytics/CHANGELOG.md index 1d3875915f4..62f507a7c01 100644 --- a/Crashlytics/CHANGELOG.md +++ b/Crashlytics/CHANGELOG.md @@ -1,6 +1,6 @@ # Unreleased - [added] Updated upload-symbols to 13.7 with VisionPro build phase support. (#12306) -- [changed] Added support for Crashlytics to report information from Remote Config. +- [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. From 98ecf0e3a407fc32b45d4934871c6efa85e9cc4c Mon Sep 17 00:00:00 2001 From: Doudou Nan <146472823+ddnan@users.noreply.github.com> Date: Mon, 11 Mar 2024 11:14:54 -0700 Subject: [PATCH 19/19] [Rollouts] Revert constants change in remote config public api test (#12503) --- FirebaseRemoteConfig/Sources/RCNConstants3P.m | 21 +++++++++++++++++++ ...ebaseRemoteConfigSwift_APIBuildTests.swift | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 FirebaseRemoteConfig/Sources/RCNConstants3P.m diff --git a/FirebaseRemoteConfig/Sources/RCNConstants3P.m b/FirebaseRemoteConfig/Sources/RCNConstants3P.m new file mode 100644 index 00000000000..e64295be62c --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNConstants3P.m @@ -0,0 +1,21 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FirebaseRemoteConfig/Sources/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/FirebaseRemoteConfigSwift/Tests/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift index ccf78ab68c0..892f14ec834 100644 --- a/FirebaseRemoteConfigSwift/Tests/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift +++ b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift @@ -24,7 +24,7 @@ final class FirebaseRemoteConfigSwift_APIBuildTests: XCTestCase { // MARK: - FirebaseRemoteConfig // TODO(ncooke3): These global constants should be lowercase. - let _: String = RemoteConfigConstants.NamespaceGoogleMobilePlatform + let _: String = FirebaseRemoteConfig.NamespaceGoogleMobilePlatform let _: String = FirebaseRemoteConfig.RemoteConfigThrottledEndTimeInSecondsKey // TODO(ncooke3): This should probably not be initializable.