diff --git a/Crashlytics/Crashlytics/FIRCrashlytics.m b/Crashlytics/Crashlytics/FIRCrashlytics.m index 4d112cddad4..7d42e501f1e 100644 --- a/Crashlytics/Crashlytics/FIRCrashlytics.m +++ b/Crashlytics/Crashlytics/FIRCrashlytics.m @@ -58,6 +58,7 @@ #import @import FirebaseSessions; +@import FirebaseRemoteConfigInterop; #if TARGET_OS_IPHONE #import @@ -104,7 +105,8 @@ - (instancetype)initWithApp:(FIRApp *)app appInfo:(NSDictionary *)appInfo installations:(FIRInstallations *)installations analytics:(id)analytics - sessions:(id)sessions { + sessions:(id)sessions + remoteConfig:(id)remoteConfig { self = [super init]; if (self) { @@ -206,6 +208,10 @@ + (void)load { FIRDependency *sessionsDep = [FIRDependency dependencyWithProtocol:@protocol(FIRSessionsProvider)]; + FIRDependency *remoteConfigDep = + [FIRDependency dependencyWithProtocol:@protocol(FIRRemoteConfigInterop)]; + + // break point on this also with rc creatioBlock and check the order FIRComponentCreationBlock creationBlock = ^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) { if (!container.app.isDefaultApp) { @@ -215,6 +221,11 @@ + (void)load { id analytics = FIR_COMPONENT(FIRAnalyticsInterop, container); id sessions = FIR_COMPONENT(FIRSessionsProvider, container); + id remoteConfig = FIR_COMPONENT(FIRRemoteConfigInterop, container); + + if (remoteConfig) { + [remoteConfig registerRolloutsStateSubscriber:@"fire-cls" subscriber:self]; + } FIRInstallations *installations = [FIRInstallations installationsWithApp:container.app]; @@ -224,13 +235,14 @@ + (void)load { appInfo:NSBundle.mainBundle.infoDictionary installations:installations analytics:analytics - sessions:sessions]; + sessions:sessions + remoteConfig:remoteConfig]; }; FIRComponent *component = [FIRComponent componentWithProtocol:@protocol(FIRCrashlyticsInstanceProvider) instantiationTiming:FIRInstantiationTimingEagerInDefaultApp - dependencies:@[ analyticsDep, sessionsDep ] + dependencies:@[ analyticsDep, sessionsDep, remoteConfigDep ] creationBlock:creationBlock]; return @[ component ]; } diff --git a/Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlytics.h b/Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlytics.h index 1ace1bed780..58603838cf4 100644 --- a/Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlytics.h +++ b/Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlytics.h @@ -16,7 +16,7 @@ #import "FIRCrashlyticsReport.h" #import "FIRExceptionModel.h" - +@protocol FIRRolloutsStateSubscriber; #if __has_include() #warning "FirebaseCrashlytics and Crashlytics are not compatible \ in the same app because including multiple crash reporters can \ @@ -35,7 +35,7 @@ NS_ASSUME_NONNULL_BEGIN * we suggest using a wrapper class or a protocol extension. */ NS_SWIFT_NAME(Crashlytics) -@interface FIRCrashlytics : NSObject +@interface FIRCrashlytics : NSObject /** :nodoc: */ - (instancetype)init NS_UNAVAILABLE; diff --git a/FirebaseRemoteConfig/Interop/RemoteConfigInterop.swift b/FirebaseRemoteConfig/Interop/RemoteConfigInterop.swift new file mode 100644 index 00000000000..9a1e2db6ca9 --- /dev/null +++ b/FirebaseRemoteConfig/Interop/RemoteConfigInterop.swift @@ -0,0 +1,14 @@ +// +// File.swift +// +// +// Created by Themis Wang on 2023-11-16. +// + +import Foundation + +@objc(FIRRemoteConfigInterop) +public protocol RemoteConfigInterop { + func registerRolloutsStateSubscriber(_ namespace: String, + subscriber: RolloutsStateSubscriber?) +} diff --git a/FirebaseRemoteConfig/Interop/RolloutAssignment.swift b/FirebaseRemoteConfig/Interop/RolloutAssignment.swift new file mode 100644 index 00000000000..563a3e0b301 --- /dev/null +++ b/FirebaseRemoteConfig/Interop/RolloutAssignment.swift @@ -0,0 +1,39 @@ +// +// File.swift +// +// +// Created by Themis Wang on 2023-11-16. +// + +import Foundation + +@objc(FIRRolloutAssignment) +public class RolloutAssignment: NSObject { + @objc public var rolloutId: String + @objc public var variantId: String + @objc public var templateVersion: String + @objc public var parameterKey: String + @objc public var parameterValue: String + + public init(rolloutId: String, variantId: String, templateVersion: String, 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..22c8df2430c --- /dev/null +++ b/FirebaseRemoteConfig/Interop/RolloutsStateSubscriber.swift @@ -0,0 +1,13 @@ +// +// File.swift +// +// +// Created by Themis Wang on 2023-11-16. +// + +import Foundation + +@objc(FIRRolloutsStateSubscriber) +public protocol RolloutsStateSubscriber { + func onRolloutsStateChanged(_ rolloutsState: RolloutsState) +} diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index 4035d558707..1fda435376e 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -31,6 +31,8 @@ #import "FirebaseRemoteConfig/Sources/RCNDevice.h" #import "FirebaseRemoteConfig/Sources/RCNPersonalization.h" +@import FirebaseRemoteConfigInterop; + /// Remote Config Error Domain. /// TODO: Rename according to obj-c style for constants. NSString *const FIRRemoteConfigErrorDomain = @"com.google.remoteconfig.ErrorDomain"; @@ -73,6 +75,7 @@ @implementation FIRRemoteConfig { dispatch_queue_t _queue; NSString *_appName; NSMutableArray *_listeners; + NSMutableArray *_subscribers; } static NSMutableDictionary *> @@ -176,6 +179,8 @@ - (instancetype)initWithAppName:(NSString *)appName [_settings loadConfigFromMetadataTable]; + _subscribers = [[NSMutableArray alloc] init]; + if (analytics) { _listeners = [[NSMutableArray alloc] init]; RCNPersonalization *personalization = @@ -613,4 +618,8 @@ - (FIRConfigUpdateListenerRegistration *)addOnConfigUpdateListener: return [self->_configRealtime addConfigUpdateListener:listener]; } +- (void)addRemoteConfigInteropSubscriber:(id)subscriber { + [self->_subscribers addObject:subscriber]; +} + @end diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h b/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h index f015ea14974..3c6ec1ec730 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; diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m index e0adc7bccbf..096faecda5d 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m @@ -23,6 +23,17 @@ #import "Interop/Analytics/Public/FIRAnalyticsInterop.h" @implementation FIRRemoteConfigComponent +static FIRRemoteConfigComponent *_sharedDefaultAppRemoteConfigSingleton = nil; + ++ (FIRRemoteConfigComponent *)sharedDefaultAppRemoteConfigSingletonWithDefaultApp:(FIRApp *)app { + @synchronized([FIRRemoteConfigComponent class]) { + if (!_sharedDefaultAppRemoteConfigSingleton) { + _sharedDefaultAppRemoteConfigSingleton = [[self alloc] initWithApp:app]; + } + return _sharedDefaultAppRemoteConfigSingleton; + } + return nil; +} /// Default method for retrieving a Remote Config instance, or creating one if it doesn't exist. - (FIRRemoteConfig *)remoteConfigForNamespace:(NSString *)remoteConfigNamespace { @@ -60,6 +71,7 @@ - (FIRRemoteConfig *)remoteConfigForNamespace:(NSString *)remoteConfigNamespace FIRApp *app = self.app; id analytics = app.isDefaultApp ? FIR_COMPONENT(FIRAnalyticsInterop, app.container) : nil; + instance = [[FIRRemoteConfig alloc] initWithAppName:app.name FIROptions:app.options namespace:remoteConfigNamespace @@ -95,6 +107,7 @@ + (void)load { + (NSArray *)componentsToRegister { FIRDependency *analyticsDep = [FIRDependency dependencyWithProtocol:@protocol(FIRAnalyticsInterop) isRequired:NO]; + FIRComponent *rcProvider = [FIRComponent componentWithProtocol:@protocol(FIRRemoteConfigProvider) instantiationTiming:FIRInstantiationTimingAlwaysEager @@ -102,9 +115,35 @@ + (void)load { creationBlock:^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) { // Cache the component so instances of Remote Config are cached. *isCacheable = YES; + if (container.app.isDefaultApp) { + return [FIRRemoteConfigComponent + sharedDefaultAppRemoteConfigSingletonWithDefaultApp:container.app]; + } return [[FIRRemoteConfigComponent alloc] initWithApp:container.app]; }]; - return @[ rcProvider ]; + + // Crashlytics only works on default app, the only shared component between Provider and Interop + // is the component for default app + 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; + if (container.app.isDefaultApp) { + return [FIRRemoteConfigComponent + sharedDefaultAppRemoteConfigSingletonWithDefaultApp:container.app]; + } + return nil; + }]; + return @[ rcProvider, rcInterop ]; +} + +- (void)registerRolloutsStateSubscriber:(nonnull NSString *)nameSpace + subscriber:(nullable id)subscriber { + // adding the registered subscriber reference to the namespace instance + [self.instances[nameSpace] addRemoteConfigInteropSubscriber:subscriber]; } @end diff --git a/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h b/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h index ef7def6fd9d..c4935e82ce9 100644 --- a/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h +++ b/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h @@ -22,6 +22,8 @@ @class RCNConfigDBManager; @class RCNConfigFetch; @class RCNConfigRealtime; + +@protocol FIRRemoteConfigComponentDelegate; @protocol FIRAnalyticsInterop; NS_ASSUME_NONNULL_BEGIN diff --git a/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h b/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h index 29cd12a5143..f147f32b431 100644 --- a/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h +++ b/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h @@ -17,6 +17,7 @@ #import @class FIRApp; +@protocol FIRRolloutsStateSubscriber; /// The Firebase Remote Config service default namespace, to be used if the API method does not /// specify a different namespace. Use the default namespace if configuring from the Google Firebase @@ -150,6 +151,8 @@ NS_SWIFT_NAME(RemoteConfigValue) @property(nonatomic, readonly, nullable) id JSONValue NS_SWIFT_NAME(jsonValue); /// Identifies the source of the fetched value. @property(nonatomic, readonly) FIRRemoteConfigSource source; + +@property(nonatomic, readonly, nonnull) NSMutableArray> *subscribers; @end #pragma mark - FIRRemoteConfigSettings @@ -357,4 +360,5 @@ typedef void (^FIRRemoteConfigUpdateCompletion)(FIRRemoteConfigUpdate *_Nullable (FIRRemoteConfigUpdateCompletion _Nonnull)listener NS_SWIFT_NAME(addOnConfigUpdateListener(remoteConfigUpdateCompletion:)); +- (void)addRemoteConfigInteropSubscriber:(nonnull id)subscriber; @end diff --git a/Package.swift b/Package.swift index 661d1a76295..3cb72a8636b 100644 --- a/Package.swift +++ b/Package.swift @@ -491,11 +491,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", @@ -957,6 +962,7 @@ let package = Package( "FirebaseCore", "FirebaseABTesting", "FirebaseInstallations", + "FirebaseRemoteConfigInterop", .product(name: "GULNSData", package: "GoogleUtilities"), ], path: "FirebaseRemoteConfig/Sources", @@ -1028,6 +1034,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