From 2455ff812c11af32c3a13a22bb0ebd4fb703a401 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 7 Oct 2024 14:02:48 -0400 Subject: [PATCH 01/13] [Config] Add custom signal setter API --- .../Sources/FIRRemoteConfig.m | 5 ++ .../FirebaseRemoteConfig/FIRRemoteConfig.h | 4 + .../Swift/CustomSignals.swift | 85 +++++++++++++++++++ ...ebaseRemoteConfigSwift_APIBuildTests.swift | 10 +++ 4 files changed, 104 insertions(+) create mode 100644 FirebaseRemoteConfig/Swift/CustomSignals.swift diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index 561ada50693..0308764dab6 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -237,6 +237,11 @@ - (void)callListeners:(NSString *)key config:(NSDictionary *)config { } } +- (void)setCustomSignals:(nullable NSDictionary *)customSignals + WithCompletion:(void (^_Nullable)(NSError *_Nullable error))completionHandler { + // TODO: Implement. +} + #pragma mark - fetch - (void)fetchWithCompletionHandler:(FIRRemoteConfigFetchCompletion)completionHandler { diff --git a/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h b/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h index 0a45617545f..f4c451d5a99 100644 --- a/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h +++ b/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h @@ -357,4 +357,8 @@ typedef void (^FIRRemoteConfigUpdateCompletion)(FIRRemoteConfigUpdate *_Nullable (FIRRemoteConfigUpdateCompletion _Nonnull)listener NS_SWIFT_NAME(addOnConfigUpdateListener(remoteConfigUpdateCompletion:)); +- (void)setCustomSignals:(nullable NSDictionary *)customSignals + WithCompletion:(void (^_Nullable)(NSError *_Nullable error))completionHandler + NS_REFINED_FOR_SWIFT; + @end diff --git a/FirebaseRemoteConfig/Swift/CustomSignals.swift b/FirebaseRemoteConfig/Swift/CustomSignals.swift new file mode 100644 index 00000000000..a7496652360 --- /dev/null +++ b/FirebaseRemoteConfig/Swift/CustomSignals.swift @@ -0,0 +1,85 @@ +// 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 +#if SWIFT_PACKAGE + @_exported import FirebaseRemoteConfigInternal +#endif // SWIFT_PACKAGE + +// TODO: Document. +public struct CustomSignal { + private enum Kind { + case string(String) + case integer(Int) + } + + private let kind: Kind + + private init(kind: Kind) { + self.kind = kind + } + + /// Returns a string backed custom signal. + /// - Parameter string: The given string to back the custom signal with. + /// - Returns: A string backed custom signal. + public static func string(_ string: String) -> Self { + Self(kind: .string(string)) + } + + /// Returns an integer backed custom signal. + /// - Parameter integer: The given integer to back the custom signal with. + /// - Returns: An integer backed custom signal. + public static func integer(_ integer: Int) -> Self { + Self(kind: .integer(integer)) + } + + fileprivate func toNSObject() -> NSObject { + switch kind { + case let .string(string): + return string as NSString + case let .integer(int): + return int as NSNumber + } + } +} + +extension CustomSignal: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self = .string(value) + } +} + +extension CustomSignal: ExpressibleByIntegerLiteral { + public init(integerLiteral value: Int) { + self = .integer(value) + } +} + +public extension RemoteConfig { + /// Sets custom signals for this Remote Config instance. + /// - Parameter customSignals: A dictionary mapping string keys to custom + /// signals to be set for the app instance. + func setCustomSignals(_ customSignals: [String: CustomSignal]) async throws { + return try await withCheckedThrowingContinuation { continuation in + let customSignals = customSignals.mapValues { $0.toNSObject() } + self.__setCustomSignals(customSignals) { error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } + } +} diff --git a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift index 56fc336908e..cb3de2a6dd4 100644 --- a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift +++ b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift @@ -223,5 +223,15 @@ final class FirebaseRemoteConfig_APIBuildTests: XCTestCase { struct MyEncodableValue: Encodable {} let _: Void = try config.setDefaults(from: MyEncodableValue()) + + Task { + let signals: [String: CustomSignal] = [ + "signal_1": .integer(5), + "signal_2": .string("true"), + "signal_3": 5, + "signal_4": "true", + ] + try await config.setCustomSignals(signals) + } } } From 372538c2df7b1f95700966c10f9ff899e4b6d299 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 7 Oct 2024 14:19:35 -0400 Subject: [PATCH 02/13] Make API test clearer --- .../SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift index cb3de2a6dd4..f687b5f4c63 100644 --- a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift +++ b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift @@ -227,9 +227,9 @@ final class FirebaseRemoteConfig_APIBuildTests: XCTestCase { Task { let signals: [String: CustomSignal] = [ "signal_1": .integer(5), - "signal_2": .string("true"), + "signal_2": .string("enable_feature"), "signal_3": 5, - "signal_4": "true", + "signal_4": "enable_feature", ] try await config.setCustomSignals(signals) } From 2d840c4b2936e09584010947b723429a1c53a851 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 7 Oct 2024 15:02:39 -0400 Subject: [PATCH 03/13] Rename to CustomSignal to CustomSignalValue --- FirebaseRemoteConfig/Swift/CustomSignals.swift | 8 ++++---- .../FirebaseRemoteConfigSwift_APIBuildTests.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/FirebaseRemoteConfig/Swift/CustomSignals.swift b/FirebaseRemoteConfig/Swift/CustomSignals.swift index a7496652360..e21b880207e 100644 --- a/FirebaseRemoteConfig/Swift/CustomSignals.swift +++ b/FirebaseRemoteConfig/Swift/CustomSignals.swift @@ -18,7 +18,7 @@ import Foundation #endif // SWIFT_PACKAGE // TODO: Document. -public struct CustomSignal { +public struct CustomSignalValue { private enum Kind { case string(String) case integer(Int) @@ -54,13 +54,13 @@ public struct CustomSignal { } } -extension CustomSignal: ExpressibleByStringLiteral { +extension CustomSignalValue: ExpressibleByStringLiteral { public init(stringLiteral value: String) { self = .string(value) } } -extension CustomSignal: ExpressibleByIntegerLiteral { +extension CustomSignalValue: ExpressibleByIntegerLiteral { public init(integerLiteral value: Int) { self = .integer(value) } @@ -70,7 +70,7 @@ public extension RemoteConfig { /// Sets custom signals for this Remote Config instance. /// - Parameter customSignals: A dictionary mapping string keys to custom /// signals to be set for the app instance. - func setCustomSignals(_ customSignals: [String: CustomSignal]) async throws { + func setCustomSignals(_ customSignals: [String: CustomSignalValue]) async throws { return try await withCheckedThrowingContinuation { continuation in let customSignals = customSignals.mapValues { $0.toNSObject() } self.__setCustomSignals(customSignals) { error in diff --git a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift index f687b5f4c63..9469030aa6b 100644 --- a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift +++ b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift @@ -225,7 +225,7 @@ final class FirebaseRemoteConfig_APIBuildTests: XCTestCase { let _: Void = try config.setDefaults(from: MyEncodableValue()) Task { - let signals: [String: CustomSignal] = [ + let signals: [String: CustomSignalValue] = [ "signal_1": .integer(5), "signal_2": .string("enable_feature"), "signal_3": 5, From 4a25d7b0de96cc0f37864849fcee6d793748a233 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 7 Oct 2024 16:57:13 -0400 Subject: [PATCH 04/13] Work for interpolated strings cc: @andrewheard --- FirebaseRemoteConfig/Swift/CustomSignals.swift | 2 +- .../SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/FirebaseRemoteConfig/Swift/CustomSignals.swift b/FirebaseRemoteConfig/Swift/CustomSignals.swift index e21b880207e..0029291dd8d 100644 --- a/FirebaseRemoteConfig/Swift/CustomSignals.swift +++ b/FirebaseRemoteConfig/Swift/CustomSignals.swift @@ -54,7 +54,7 @@ public struct CustomSignalValue { } } -extension CustomSignalValue: ExpressibleByStringLiteral { +extension CustomSignalValue: ExpressibleByStringInterpolation { public init(stringLiteral value: String) { self = .string(value) } diff --git a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift index 9469030aa6b..14817cec5d1 100644 --- a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift +++ b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift @@ -230,6 +230,7 @@ final class FirebaseRemoteConfig_APIBuildTests: XCTestCase { "signal_2": .string("enable_feature"), "signal_3": 5, "signal_4": "enable_feature", + "signal_5": "enable_feature_\("secret")", ] try await config.setCustomSignals(signals) } From 4c2bc890d65f88a44f94362a727bb4e47c1f9c88 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 8 Oct 2024 10:36:03 -0400 Subject: [PATCH 05/13] Add support for deleting key --- FirebaseRemoteConfig/Swift/CustomSignals.swift | 4 ++-- .../SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/FirebaseRemoteConfig/Swift/CustomSignals.swift b/FirebaseRemoteConfig/Swift/CustomSignals.swift index 0029291dd8d..cd8d49cee25 100644 --- a/FirebaseRemoteConfig/Swift/CustomSignals.swift +++ b/FirebaseRemoteConfig/Swift/CustomSignals.swift @@ -70,9 +70,9 @@ public extension RemoteConfig { /// Sets custom signals for this Remote Config instance. /// - Parameter customSignals: A dictionary mapping string keys to custom /// signals to be set for the app instance. - func setCustomSignals(_ customSignals: [String: CustomSignalValue]) async throws { + func setCustomSignals(_ customSignals: [String: CustomSignalValue?]) async throws { return try await withCheckedThrowingContinuation { continuation in - let customSignals = customSignals.mapValues { $0.toNSObject() } + let customSignals = customSignals.mapValues { $0?.toNSObject() ?? NSNull() } self.__setCustomSignals(customSignals) { error in if let error { continuation.resume(throwing: error) diff --git a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift index 14817cec5d1..c02f13566f4 100644 --- a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift +++ b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift @@ -225,12 +225,13 @@ final class FirebaseRemoteConfig_APIBuildTests: XCTestCase { let _: Void = try config.setDefaults(from: MyEncodableValue()) Task { - let signals: [String: CustomSignalValue] = [ + let signals: [String: CustomSignalValue?] = [ "signal_1": .integer(5), "signal_2": .string("enable_feature"), "signal_3": 5, "signal_4": "enable_feature", "signal_5": "enable_feature_\("secret")", + "signal_6": nil, // Used to delete the custom signal for a given key. ] try await config.setCustomSignals(signals) } From a8e3617ea9dd7df57e5ee4ea904fb5f1afc6ed98 Mon Sep 17 00:00:00 2001 From: tusharkhandelwal8 Date: Thu, 24 Oct 2024 15:55:51 +0530 Subject: [PATCH 06/13] Complete implementation for setting/updating custom signals with tests --- .../Sources/FIRRemoteConfig.m | 49 ++++++++++++++++++- .../Sources/Private/RCNConfigSettings.h | 5 ++ .../FirebaseRemoteConfig/FIRRemoteConfig.h | 11 +++++ .../Sources/RCNConfigSettings.m | 23 ++++++++- .../Sources/RCNUserDefaultsManager.h | 2 + .../Sources/RCNUserDefaultsManager.m | 16 ++++++ .../Tests/Unit/RCNRemoteConfigTest.m | 24 +++++++++ .../Tests/Unit/RCNUserDefaultsManagerTests.m | 27 ++++++++++ 8 files changed, 155 insertions(+), 2 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index 0308764dab6..31b2b51f9bd 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -34,6 +34,8 @@ /// Remote Config Error Domain. /// TODO: Rename according to obj-c style for constants. NSString *const FIRRemoteConfigErrorDomain = @"com.google.remoteconfig.ErrorDomain"; +// Remote Config Custom Signals Error Domain +NSString *const FIRRemoteConfigCustomSignalsErrorDomain = @"com.google.remoteconfig.customsignals.ErrorDomain"; // Remote Config Realtime Error Domain NSString *const FIRRemoteConfigUpdateErrorDomain = @"com.google.remoteconfig.update.ErrorDomain"; /// Remote Config Error Info End Time Seconds; @@ -239,7 +241,52 @@ - (void)callListeners:(NSString *)key config:(NSDictionary *)config { - (void)setCustomSignals:(nullable NSDictionary *)customSignals WithCompletion:(void (^_Nullable)(NSError *_Nullable error))completionHandler { - // TODO: Implement. + void (^setCustomSignalsBlock)(void) = ^{ + if (!customSignals) { + if (completionHandler) { + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(nil); + }); + } + return; + } + + for (NSString *key in customSignals) { + NSObject *value = customSignals[key]; + if (value && ![value isKindOfClass:[NSString class]] && ![value isKindOfClass:[NSNumber class]]) { + if (completionHandler) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSError *error = [NSError errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain + code:FIRRemoteConfigCustomSignalsErrorInvalidValueType + userInfo:@{NSLocalizedDescriptionKey: @"Invalid value type. Must be NSString or NSNumber."}]; + completionHandler(error); + }); + } + return; + } + } + + NSMutableDictionary *newCustomSignals = + [[NSMutableDictionary alloc] initWithDictionary:self->_settings.customSignals]; + + for (NSString *key in customSignals) { + NSObject *value = customSignals[key]; + if (value) { + [newCustomSignals setObject:value forKey:key]; + } else { + [newCustomSignals removeObjectForKey:key]; + } + } + + self->_settings.customSignals = newCustomSignals; + if (completionHandler) { + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(nil); + }); + } + }; + + dispatch_async(_queue, setCustomSignalsBlock); } #pragma mark - fetch diff --git a/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h b/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h index 034c50c7330..e292bdbbb11 100644 --- a/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h +++ b/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h @@ -84,6 +84,11 @@ /// Last active template version. @property(nonatomic, readwrite, assign) NSString *lastActiveTemplateVersion; +#pragma mark - Custom Signals + +/// A dictionary to hold custom signals that are set by the developer. +@property (nonatomic, readwrite, strong) NSMutableDictionary *customSignals; + #pragma mark Throttling properties /// Throttling intervals are based on https://cloud.google.com/storage/docs/exponential-backoff diff --git a/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h b/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h index f4c451d5a99..cc42085705c 100644 --- a/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h +++ b/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h @@ -96,6 +96,17 @@ typedef NS_ERROR_ENUM(FIRRemoteConfigUpdateErrorDomain, FIRRemoteConfigUpdateErr FIRRemoteConfigUpdateErrorUnavailable = 8004, } NS_SWIFT_NAME(RemoteConfigUpdateError); +/// Error domain for custom signals errors. +extern NSString *const _Nonnull FIRRemoteConfigCustomSignalsErrorDomain NS_SWIFT_NAME(RemoteConfigCustomSignalsErrorDomain); + +/// Firebase Remote Config custom signals error. +typedef NS_ERROR_ENUM(FIRRemoteConfigCustomSignalsErrorDomain, FIRRemoteConfigCustomSignalsError) { + /// Unknown error. + FIRRemoteConfigCustomSignalsErrorUnknown = 8001, + /// Invalid value type in the custom signals dictionary. + FIRRemoteConfigCustomSignalsErrorInvalidValueType = 8002, +} NS_SWIFT_NAME(RemoteConfigCustomSignalsError); + /// Enumerated value that indicates the source of Remote Config data. Data can come from /// the Remote Config service, the DefaultConfig that is available when the app is first installed, /// or a static initialized value if data is not available from the service or DefaultConfig. diff --git a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m index 48e414e2f83..1bdd0336602 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m @@ -108,7 +108,8 @@ - (instancetype)initWithDatabaseManager:(RCNConfigDBManager *)manager @"New config database created. Resetting user defaults."); [_userDefaultsManager resetUserDefaults]; } - + + _customSignals = [_userDefaultsManager customSignals]; _isFetchInProgress = NO; _lastFetchedTemplateVersion = [_userDefaultsManager lastFetchedTemplateVersion]; _lastActiveTemplateVersion = [_userDefaultsManager lastActiveTemplateVersion]; @@ -444,6 +445,21 @@ - (NSString *)nextRequestWithUserProperties:(NSDictionary *)userProperties { } } } + + if (_customSignals.count > 0) { + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:_customSignals + options:0 + error:&error]; + if (!error) { + ret = [ret + stringByAppendingString:[NSString + stringWithFormat:@", custom_signals:%@", + [[NSString alloc] + initWithData:jsonData + encoding:NSUTF8StringEncoding]]]; + } + } ret = [ret stringByAppendingString:@"}"]; return ret; } @@ -517,6 +533,11 @@ - (void)setLastSetDefaultsTimeInterval:(NSTimeInterval)lastSetDefaultsTimestamp completionHandler:nil]; } +- (void)setCustomSignals:(NSMutableDictionary *)customSignals { + _customSignals = customSignals; + [_userDefaultsManager setCustomSignals:customSignals]; +} + #pragma mark Throttling - (BOOL)hasMinimumFetchIntervalElapsed:(NSTimeInterval)minimumFetchInterval { diff --git a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h index b235f217d81..70fbcef4713 100644 --- a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h +++ b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h @@ -47,6 +47,8 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, assign) NSString *lastFetchedTemplateVersion; /// Last active template version. @property(nonatomic, assign) NSString *lastActiveTemplateVersion; +/// A dictionary to hold the latest custom signals set by the developer. +@property (nonatomic, readwrite, strong) NSMutableDictionary *customSignals; /// Designated initializer. - (instancetype)initWithAppName:(NSString *)appName diff --git a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m index 880a2157fe1..0c7b6a4c699 100644 --- a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m +++ b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m @@ -34,6 +34,7 @@ static NSString *const kRCNUserDefaultsKeyNameCurrentRealtimeThrottlingRetryInterval = @"currentRealtimeThrottlingRetryInterval"; static NSString *const kRCNUserDefaultsKeyNameRealtimeRetryCount = @"realtimeRetryCount"; +static NSString *const kRCNUserDefaultsKeyCustomSignals = @"customSignals"; @interface RCNUserDefaultsManager () { /// User Defaults instance for this bundleID. NSUserDefaults is guaranteed to be thread-safe. @@ -141,6 +142,21 @@ - (void)setLastActiveTemplateVersion:(NSString *)templateVersion { } } +- (NSMutableDictionary *)customSignals { + NSDictionary *userDefaults = [self instanceUserDefaults]; + if ([userDefaults objectForKey:kRCNUserDefaultsKeyCustomSignals]) { + return [userDefaults objectForKey:kRCNUserDefaultsKeyCustomSignals]; + } + + return [[NSMutableDictionary alloc] init]; +} + +- (void)setCustomSignals:(NSMutableDictionary *)customSignals { + if (customSignals) { + [self setInstanceUserDefaultsValue:customSignals forKey:kRCNUserDefaultsKeyCustomSignals]; + } +} + - (NSTimeInterval)lastETagUpdateTime { NSNumber *lastETagUpdateTime = [[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNamelastETagUpdateTime]; diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m index e84bd024b8b..3d903f703cd 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m @@ -1834,6 +1834,30 @@ - (void)testFetchAndActivateRolloutsNotifyInterop { [self waitForExpectations:@[ notificationExpectation ] timeout:_expectationTimeout]; } +- (void)testSetCustomSingals { + NSMutableArray *expectations = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + expectations[i] = [self expectationWithDescription: + [NSString stringWithFormat:@"Set custom signals - instance %d", i]]; + + NSDictionary *testSignals = @{ + @"signal1" : @"stringValue", + @"signal2" : @"stringValue2", + }; + + [_configInstances[i] setCustomSignals:testSignals + WithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + NSMutableDictionary *retrievedSignals = self->_configInstances[i].settings.customSignals; + XCTAssertEqualObjects(retrievedSignals, testSignals); + [expectations[i] fulfill]; + }]; + } + [self waitForExpectationsWithTimeout:_expectationTimeout handler:nil]; +} + #pragma mark - Test Helpers - (FIROptions *)firstAppOptions { diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m b/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m index 5f915d73632..793c8b31014 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m @@ -23,6 +23,8 @@ static NSString* const AppName = @"testApp"; static NSString* const FQNamespace1 = @"testNamespace1:testApp"; static NSString* const FQNamespace2 = @"testNamespace2:testApp"; +static NSMutableDictionary *customSignals1 = nil; +static NSMutableDictionary *customSignals2 = nil; @interface RCNUserDefaultsManagerTests : XCTestCase @@ -36,6 +38,13 @@ - (void)setUp { [[NSUserDefaults standardUserDefaults] removePersistentDomainForName:[NSBundle mainBundle].bundleIdentifier]; RCNUserDefaultsSampleTimeStamp = [[NSDate date] timeIntervalSince1970]; + + customSignals1 = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"signal1" : @"stringValue", + }]; + customSignals2 = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"signal2" : @"stringValue2", + }]; } - (void)testUserDefaultsEtagWriteAndRead { @@ -168,6 +177,18 @@ - (void)testUserDefaultsCurrentRealtimeThrottlingRetryIntervalWriteAndRead { RCNUserDefaultsSampleTimeStamp - 2.0); } +- (void)testUserDefaultsCustomSignalsWriteAndRead { + RCNUserDefaultsManager* manager = + [[RCNUserDefaultsManager alloc] initWithAppName:AppName + bundleID:[NSBundle mainBundle].bundleIdentifier + namespace:FQNamespace1]; + [manager setCustomSignals:customSignals1]; + XCTAssertEqualObjects([manager customSignals], customSignals1); + + [manager setCustomSignals:customSignals2]; + XCTAssertEqualObjects([manager customSignals], customSignals2); +} + - (void)testUserDefaultsForMultipleNamespaces { RCNUserDefaultsManager* manager1 = [[RCNUserDefaultsManager alloc] initWithAppName:AppName @@ -248,6 +269,12 @@ - (void)testUserDefaultsForMultipleNamespaces { [manager2 setLastActiveTemplateVersion:@"2"]; XCTAssertEqualObjects([manager1 lastActiveTemplateVersion], @"1"); XCTAssertEqualObjects([manager2 lastActiveTemplateVersion], @"2"); + + /// Custom Singnals + [manager1 setCustomSignals:customSignals1]; + [manager2 setCustomSignals:customSignals2]; + XCTAssertEqualObjects([manager1 customSignals], customSignals1); + XCTAssertEqualObjects([manager2 customSignals], customSignals2); } - (void)testUserDefaultsReset { From b047554b499eeb886349810a95e06f6bc7730739 Mon Sep 17 00:00:00 2001 From: tusharkhandelwal8 Date: Sun, 27 Oct 2024 16:38:35 +0530 Subject: [PATCH 07/13] Fix lint errors and code formatting. --- .../Sources/FIRRemoteConfig.m | 18 +++++++++++------ .../Sources/Private/RCNConfigSettings.h | 2 +- .../FirebaseRemoteConfig/FIRRemoteConfig.h | 10 +++++----- .../Sources/RCNConfigSettings.m | 6 +++--- .../Sources/RCNUserDefaultsManager.h | 2 +- .../Tests/Unit/RCNRemoteConfigTest.m | 18 +++++++++-------- .../Tests/Unit/RCNUserDefaultsManagerTests.m | 20 +++++++++---------- 7 files changed, 42 insertions(+), 34 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index 31b2b51f9bd..5f58f7d52da 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -35,7 +35,8 @@ /// TODO: Rename according to obj-c style for constants. NSString *const FIRRemoteConfigErrorDomain = @"com.google.remoteconfig.ErrorDomain"; // Remote Config Custom Signals Error Domain -NSString *const FIRRemoteConfigCustomSignalsErrorDomain = @"com.google.remoteconfig.customsignals.ErrorDomain"; +NSString *const FIRRemoteConfigCustomSignalsErrorDomain = + @"com.google.remoteconfig.customsignals.ErrorDomain"; // Remote Config Realtime Error Domain NSString *const FIRRemoteConfigUpdateErrorDomain = @"com.google.remoteconfig.update.ErrorDomain"; /// Remote Config Error Info End Time Seconds; @@ -250,15 +251,20 @@ - (void)setCustomSignals:(nullable NSDictionary *)custom } return; } - + for (NSString *key in customSignals) { NSObject *value = customSignals[key]; - if (value && ![value isKindOfClass:[NSString class]] && ![value isKindOfClass:[NSNumber class]]) { + if (value && ![value isKindOfClass:[NSString class]] && + ![value isKindOfClass:[NSNumber class]]) { if (completionHandler) { dispatch_async(dispatch_get_main_queue(), ^{ - NSError *error = [NSError errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain - code:FIRRemoteConfigCustomSignalsErrorInvalidValueType - userInfo:@{NSLocalizedDescriptionKey: @"Invalid value type. Must be NSString or NSNumber."}]; + NSError *error = + [NSError errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain + code:FIRRemoteConfigCustomSignalsErrorInvalidValueType + userInfo:@{ + NSLocalizedDescriptionKey : + @"Invalid value type. Must be NSString or NSNumber." + }]; completionHandler(error); }); } diff --git a/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h b/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h index e292bdbbb11..dd77a9c75d2 100644 --- a/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h +++ b/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h @@ -87,7 +87,7 @@ #pragma mark - Custom Signals /// A dictionary to hold custom signals that are set by the developer. -@property (nonatomic, readwrite, strong) NSMutableDictionary *customSignals; +@property(nonatomic, readwrite, strong) NSMutableDictionary *customSignals; #pragma mark Throttling properties diff --git a/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h b/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h index cc42085705c..033c7b508e4 100644 --- a/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h +++ b/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h @@ -100,11 +100,11 @@ typedef NS_ERROR_ENUM(FIRRemoteConfigUpdateErrorDomain, FIRRemoteConfigUpdateErr extern NSString *const _Nonnull FIRRemoteConfigCustomSignalsErrorDomain NS_SWIFT_NAME(RemoteConfigCustomSignalsErrorDomain); /// Firebase Remote Config custom signals error. -typedef NS_ERROR_ENUM(FIRRemoteConfigCustomSignalsErrorDomain, FIRRemoteConfigCustomSignalsError) { - /// Unknown error. - FIRRemoteConfigCustomSignalsErrorUnknown = 8001, - /// Invalid value type in the custom signals dictionary. - FIRRemoteConfigCustomSignalsErrorInvalidValueType = 8002, +typedef NS_ERROR_ENUM(FIRRemoteConfigCustomSignalsErrorDomain, FIRRemoteConfigCustomSignalsError){ + /// Unknown error. + FIRRemoteConfigCustomSignalsErrorUnknown = 8001, + /// Invalid value type in the custom signals dictionary. + FIRRemoteConfigCustomSignalsErrorInvalidValueType = 8002, } NS_SWIFT_NAME(RemoteConfigCustomSignalsError); /// Enumerated value that indicates the source of Remote Config data. Data can come from diff --git a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m index 1bdd0336602..2a65fbc7e6a 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m @@ -108,7 +108,7 @@ - (instancetype)initWithDatabaseManager:(RCNConfigDBManager *)manager @"New config database created. Resetting user defaults."); [_userDefaultsManager resetUserDefaults]; } - + _customSignals = [_userDefaultsManager customSignals]; _isFetchInProgress = NO; _lastFetchedTemplateVersion = [_userDefaultsManager lastFetchedTemplateVersion]; @@ -445,7 +445,7 @@ - (NSString *)nextRequestWithUserProperties:(NSDictionary *)userProperties { } } } - + if (_customSignals.count > 0) { NSError *error; NSData *jsonData = [NSJSONSerialization dataWithJSONObject:_customSignals @@ -533,7 +533,7 @@ - (void)setLastSetDefaultsTimeInterval:(NSTimeInterval)lastSetDefaultsTimestamp completionHandler:nil]; } -- (void)setCustomSignals:(NSMutableDictionary *)customSignals { +- (void)setCustomSignals:(NSMutableDictionary *)customSignals { _customSignals = customSignals; [_userDefaultsManager setCustomSignals:customSignals]; } diff --git a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h index 70fbcef4713..e86ba0774c5 100644 --- a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h +++ b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h @@ -48,7 +48,7 @@ NS_ASSUME_NONNULL_BEGIN /// Last active template version. @property(nonatomic, assign) NSString *lastActiveTemplateVersion; /// A dictionary to hold the latest custom signals set by the developer. -@property (nonatomic, readwrite, strong) NSMutableDictionary *customSignals; +@property(nonatomic, readwrite, strong) NSMutableDictionary *customSignals; /// Designated initializer. - (instancetype)initWithAppName:(NSString *)appName diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m index 3d903f703cd..c611d306cb3 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m @@ -1839,8 +1839,9 @@ - (void)testSetCustomSingals { [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { - expectations[i] = [self expectationWithDescription: - [NSString stringWithFormat:@"Set custom signals - instance %d", i]]; + expectations[i] = [self + expectationWithDescription:[NSString + stringWithFormat:@"Set custom signals - instance %d", i]]; NSDictionary *testSignals = @{ @"signal1" : @"stringValue", @@ -1848,12 +1849,13 @@ - (void)testSetCustomSingals { }; [_configInstances[i] setCustomSignals:testSignals - WithCompletion:^(NSError *_Nullable error) { - XCTAssertNil(error); - NSMutableDictionary *retrievedSignals = self->_configInstances[i].settings.customSignals; - XCTAssertEqualObjects(retrievedSignals, testSignals); - [expectations[i] fulfill]; - }]; + WithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + NSMutableDictionary *retrievedSignals = + self->_configInstances[i].settings.customSignals; + XCTAssertEqualObjects(retrievedSignals, testSignals); + [expectations[i] fulfill]; + }]; } [self waitForExpectationsWithTimeout:_expectationTimeout handler:nil]; } diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m b/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m index 793c8b31014..c463a8d0eac 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m @@ -23,8 +23,8 @@ static NSString* const AppName = @"testApp"; static NSString* const FQNamespace1 = @"testNamespace1:testApp"; static NSString* const FQNamespace2 = @"testNamespace2:testApp"; -static NSMutableDictionary *customSignals1 = nil; -static NSMutableDictionary *customSignals2 = nil; +static NSMutableDictionary* customSignals1 = nil; +static NSMutableDictionary* customSignals2 = nil; @interface RCNUserDefaultsManagerTests : XCTestCase @@ -38,13 +38,13 @@ - (void)setUp { [[NSUserDefaults standardUserDefaults] removePersistentDomainForName:[NSBundle mainBundle].bundleIdentifier]; RCNUserDefaultsSampleTimeStamp = [[NSDate date] timeIntervalSince1970]; - + customSignals1 = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"signal1" : @"stringValue", - }]; - customSignals2 = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"signal2" : @"stringValue2", - }]; + @"signal1" : @"stringValue", + }]; + customSignals2 = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"signal2" : @"stringValue2", + }]; } - (void)testUserDefaultsEtagWriteAndRead { @@ -184,7 +184,7 @@ - (void)testUserDefaultsCustomSignalsWriteAndRead { namespace:FQNamespace1]; [manager setCustomSignals:customSignals1]; XCTAssertEqualObjects([manager customSignals], customSignals1); - + [manager setCustomSignals:customSignals2]; XCTAssertEqualObjects([manager customSignals], customSignals2); } @@ -269,7 +269,7 @@ - (void)testUserDefaultsForMultipleNamespaces { [manager2 setLastActiveTemplateVersion:@"2"]; XCTAssertEqualObjects([manager1 lastActiveTemplateVersion], @"1"); XCTAssertEqualObjects([manager2 lastActiveTemplateVersion], @"2"); - + /// Custom Singnals [manager1 setCustomSignals:customSignals1]; [manager2 setCustomSignals:customSignals2]; From 8d0f566bdada12bdd8e1ab76047c6028d46b7fde Mon Sep 17 00:00:00 2001 From: tusharkhandelwal8 Date: Wed, 30 Oct 2024 13:19:17 +0530 Subject: [PATCH 08/13] Set minimum iOS version to 13 for setCustomSignals --- FirebaseRemoteConfig/Swift/CustomSignals.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/FirebaseRemoteConfig/Swift/CustomSignals.swift b/FirebaseRemoteConfig/Swift/CustomSignals.swift index cd8d49cee25..cb658700da3 100644 --- a/FirebaseRemoteConfig/Swift/CustomSignals.swift +++ b/FirebaseRemoteConfig/Swift/CustomSignals.swift @@ -66,6 +66,7 @@ extension CustomSignalValue: ExpressibleByIntegerLiteral { } } +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) public extension RemoteConfig { /// Sets custom signals for this Remote Config instance. /// - Parameter customSignals: A dictionary mapping string keys to custom From aebbcf592224c083558e82144910164e2195432d Mon Sep 17 00:00:00 2001 From: tusharkhandelwal8 Date: Thu, 7 Nov 2024 15:14:22 +0530 Subject: [PATCH 09/13] Add limits, input validation, and unit tests for custom signals --- .../Sources/FIRRemoteConfig.m | 60 ++++++++++--- .../FirebaseRemoteConfig/FIRRemoteConfig.h | 2 + .../Tests/Unit/RCNRemoteConfigTest.m | 88 +++++++++++++++++++ 3 files changed, 139 insertions(+), 11 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index 5f58f7d52da..44b583817fc 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -245,53 +245,91 @@ - (void)setCustomSignals:(nullable NSDictionary *)custom void (^setCustomSignalsBlock)(void) = ^{ if (!customSignals) { if (completionHandler) { - dispatch_async(dispatch_get_main_queue(), ^{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ completionHandler(nil); }); } return; } + // Validate value type, and key and value length for (NSString *key in customSignals) { NSObject *value = customSignals[key]; - if (value && ![value isKindOfClass:[NSString class]] && + if (![value isKindOfClass:[NSNull class]] && ![value isKindOfClass:[NSString class]] && ![value isKindOfClass:[NSNumber class]]) { if (completionHandler) { - dispatch_async(dispatch_get_main_queue(), ^{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSError *error = [NSError errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain code:FIRRemoteConfigCustomSignalsErrorInvalidValueType userInfo:@{ NSLocalizedDescriptionKey : - @"Invalid value type. Must be NSString or NSNumber." + @"Invalid value type. Must be NSString, NSNumber or NSNull" }]; completionHandler(error); }); } return; } + + if (key.length > 250 || + ([value isKindOfClass:[NSString class]] && [(NSString *)value length] > 500)) { + if (completionHandler) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSError *error = [NSError errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain + code:FIRRemoteConfigCustomSignalsErrorLimitExceeded + userInfo:@{ + NSLocalizedDescriptionKey : + @"Custom signal keys and string values must be " + @"250 and 500 characters or less respectively." + }]; + completionHandler(error); + }); + } + return; + } } + // Merge new signals with existing ones, overwriting existing keys. + // Also, remove entries where the new value is null. NSMutableDictionary *newCustomSignals = [[NSMutableDictionary alloc] initWithDictionary:self->_settings.customSignals]; for (NSString *key in customSignals) { NSObject *value = customSignals[key]; - if (value) { + if (![value isKindOfClass:[NSNull class]]) { [newCustomSignals setObject:value forKey:key]; } else { [newCustomSignals removeObjectForKey:key]; } } - self->_settings.customSignals = newCustomSignals; - if (completionHandler) { - dispatch_async(dispatch_get_main_queue(), ^{ - completionHandler(nil); - }); + // Check the size limit. + if (newCustomSignals.count > 100) { + if (completionHandler) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSError *error = [NSError + errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain + code:FIRRemoteConfigCustomSignalsErrorLimitExceeded + userInfo:@{ + NSLocalizedDescriptionKey : @"Custom signals count exceeds the limit of 100." + }]; + completionHandler(error); + }); + } + return; } - }; + // Update only if there are changes. + if (![newCustomSignals isEqualToDictionary:self->_settings.customSignals]) { + self->_settings.customSignals = newCustomSignals; + if (completionHandler) { + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(nil); + }); + } + } + }; dispatch_async(_queue, setCustomSignalsBlock); } diff --git a/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h b/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h index 033c7b508e4..3bf03b2f864 100644 --- a/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h +++ b/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h @@ -105,6 +105,8 @@ typedef NS_ERROR_ENUM(FIRRemoteConfigCustomSignalsErrorDomain, FIRRemoteConfigCu FIRRemoteConfigCustomSignalsErrorUnknown = 8001, /// Invalid value type in the custom signals dictionary. FIRRemoteConfigCustomSignalsErrorInvalidValueType = 8002, + /// Limit exceeded for key length, value length, or number of signals. + FIRRemoteConfigCustomSignalsErrorLimitExceeded = 8003, } NS_SWIFT_NAME(RemoteConfigCustomSignalsError); /// Enumerated value that indicates the source of Remote Config data. Data can come from diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m index c611d306cb3..afb476d92b1 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m @@ -1860,6 +1860,94 @@ - (void)testSetCustomSingals { [self waitForExpectationsWithTimeout:_expectationTimeout handler:nil]; } +- (void)testSetCustomSignalsMultipleTimes { + NSMutableArray *expectations = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + expectations[i] = [self + expectationWithDescription: + [NSString stringWithFormat:@"Set custom signals multiple times - instance %d", i]]; + + // First set of signals + NSDictionary *testSignals1 = @{ + @"signal1" : @"stringValue1", + @"signal2" : @"stringValue2", + }; + + // Second set of signals (overwrites, remove and adds new) + NSDictionary *testSignals2 = @{ + @"signal1" : @"updatedValue1", + @"signal2" : [NSNull null], + @"signal3" : @5, + }; + + // Expected final set of signals + NSDictionary *expectedSignals = @{ + @"signal1" : @"updatedValue1", + @"signal3" : @5, + }; + + [_configInstances[i] setCustomSignals:testSignals1 + WithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [_configInstances[i] + setCustomSignals:testSignals2 + WithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + NSMutableDictionary *retrievedSignals = + self->_configInstances[i].settings.customSignals; + XCTAssertEqualObjects(retrievedSignals, expectedSignals); + [expectations[i] fulfill]; + }]; + }]; + } + [self waitForExpectationsWithTimeout:_expectationTimeout handler:nil]; +} + +- (void)testSetCustomSignals_invalidInput_throwsException { + NSMutableArray *expectations = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + expectations[i] = + [self expectationWithDescription: + [NSString stringWithFormat:@"Set custom signals expects error - instance %d", i]]; + + // Invalid value type. + NSDictionary *invalidSignals1 = @{@"name" : [NSDate date]}; + + // Key length exceeds limit. + NSDictionary *invalidSignals2 = + @{[@"a" stringByPaddingToLength:251 withString:@"a" startingAtIndex:0] : @"value"}; + + // Value length exceeds limit. + NSDictionary *invalidSignals3 = + @{@"key" : [@"a" stringByPaddingToLength:501 withString:@"a" startingAtIndex:0]}; + + [_configInstances[i] + setCustomSignals:invalidSignals1 + WithCompletion:^(NSError *_Nullable error) { + XCTAssertNotNil(error); + XCTAssertEqual(error.code, FIRRemoteConfigCustomSignalsErrorInvalidValueType); + }]; + [_configInstances[i] + setCustomSignals:invalidSignals2 + WithCompletion:^(NSError *_Nullable error) { + XCTAssertNotNil(error); + XCTAssertEqual(error.code, FIRRemoteConfigCustomSignalsErrorLimitExceeded); + }]; + [_configInstances[i] + setCustomSignals:invalidSignals3 + WithCompletion:^(NSError *_Nullable error) { + XCTAssertNotNil(error); + XCTAssertEqual(error.code, FIRRemoteConfigCustomSignalsErrorLimitExceeded); + [expectations[i] fulfill]; + }]; + } + [self waitForExpectationsWithTimeout:_expectationTimeout handler:nil]; +} + #pragma mark - Test Helpers - (FIROptions *)firstAppOptions { From b2de614291967b18455e605480c1b643c49b6173 Mon Sep 17 00:00:00 2001 From: tusharkhandelwal8 Date: Fri, 8 Nov 2024 14:28:34 +0530 Subject: [PATCH 10/13] Move last completionHandler block outside if condition --- FirebaseRemoteConfig/Sources/FIRRemoteConfig.m | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index 44b583817fc..ddd0f701bd4 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -323,11 +323,11 @@ - (void)setCustomSignals:(nullable NSDictionary *)custom // Update only if there are changes. if (![newCustomSignals isEqualToDictionary:self->_settings.customSignals]) { self->_settings.customSignals = newCustomSignals; - if (completionHandler) { - dispatch_async(dispatch_get_main_queue(), ^{ - completionHandler(nil); - }); - } + } + if (completionHandler) { + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(nil); + }); } }; dispatch_async(_queue, setCustomSignalsBlock); From 548eefe7f91ad61fdf2f20c5d38d095868df113b Mon Sep 17 00:00:00 2001 From: tusharkhandelwal8 Date: Fri, 15 Nov 2024 10:14:15 +0530 Subject: [PATCH 11/13] Add limits as constants and dispatch all completion blocks to global queue. --- .../Sources/FIRRemoteConfig.m | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index ddd0f701bd4..f39e4c577a9 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -50,6 +50,12 @@ @"FIRRemoteConfigActivateNotification"; static NSNotificationName FIRRolloutsStateDidChangeNotificationName = @"FIRRolloutsStateDidChangeNotification"; +/// Maximum allowed length for a custom signal key (in characters). +static const NSUInteger FIRRemoteConfigCustomSignalsMaxKeyLength = 250; +/// Maximum allowed length for a string value in custom signals (in characters). +static const NSUInteger FIRRemoteConfigCustomSignalsMaxStringValueLength = 500; +/// Maximum number of custom signals allowed. +static const NSUInteger FIRRemoteConfigCustomSignalsMaxCount = 100; /// Listener for the get methods. typedef void (^FIRRemoteConfigListener)(NSString *_Nonnull, NSDictionary *_Nonnull); @@ -272,17 +278,21 @@ - (void)setCustomSignals:(nullable NSDictionary *)custom return; } - if (key.length > 250 || - ([value isKindOfClass:[NSString class]] && [(NSString *)value length] > 500)) { + if (key.length > FIRRemoteConfigCustomSignalsMaxKeyLength || + ([value isKindOfClass:[NSString class]] && + [(NSString *)value length] > FIRRemoteConfigCustomSignalsMaxStringValueLength)) { if (completionHandler) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - NSError *error = [NSError errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain - code:FIRRemoteConfigCustomSignalsErrorLimitExceeded - userInfo:@{ - NSLocalizedDescriptionKey : - @"Custom signal keys and string values must be " - @"250 and 500 characters or less respectively." - }]; + NSError *error = [NSError + errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain + code:FIRRemoteConfigCustomSignalsErrorLimitExceeded + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Custom signal keys and string values must be " + @"%lu and %lu characters or less respectively.", + FIRRemoteConfigCustomSignalsMaxKeyLength, + FIRRemoteConfigCustomSignalsMaxStringValueLength] + }]; completionHandler(error); }); } @@ -305,14 +315,16 @@ - (void)setCustomSignals:(nullable NSDictionary *)custom } // Check the size limit. - if (newCustomSignals.count > 100) { + if (newCustomSignals.count > FIRRemoteConfigCustomSignalsMaxCount) { if (completionHandler) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSError *error = [NSError errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain code:FIRRemoteConfigCustomSignalsErrorLimitExceeded userInfo:@{ - NSLocalizedDescriptionKey : @"Custom signals count exceeds the limit of 100." + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Custom signals count exceeds the limit of %lu.", + FIRRemoteConfigCustomSignalsMaxCount] }]; completionHandler(error); }); @@ -325,7 +337,7 @@ - (void)setCustomSignals:(nullable NSDictionary *)custom self->_settings.customSignals = newCustomSignals; } if (completionHandler) { - dispatch_async(dispatch_get_main_queue(), ^{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ completionHandler(nil); }); } From 6df9e3219ca40fbe23622df7a32c450b311d06fe Mon Sep 17 00:00:00 2001 From: tusharkhandelwal8 Date: Mon, 18 Nov 2024 16:18:24 +0530 Subject: [PATCH 12/13] Store Custom Signals value as String instead of Object in iOS SDK --- FirebaseRemoteConfig/Sources/FIRRemoteConfig.m | 5 +++-- FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h | 2 +- FirebaseRemoteConfig/Sources/RCNConfigSettings.m | 2 +- FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h | 2 +- FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m | 6 +++--- FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m | 8 ++++---- .../Tests/Unit/RCNUserDefaultsManagerTests.m | 6 +++--- 7 files changed, 16 insertions(+), 15 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index f39e4c577a9..1627cfe476a 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -302,13 +302,14 @@ - (void)setCustomSignals:(nullable NSDictionary *)custom // Merge new signals with existing ones, overwriting existing keys. // Also, remove entries where the new value is null. - NSMutableDictionary *newCustomSignals = + NSMutableDictionary *newCustomSignals = [[NSMutableDictionary alloc] initWithDictionary:self->_settings.customSignals]; for (NSString *key in customSignals) { NSObject *value = customSignals[key]; if (![value isKindOfClass:[NSNull class]]) { - [newCustomSignals setObject:value forKey:key]; + NSString *stringValue = [value description]; + [newCustomSignals setObject:stringValue forKey:key]; } else { [newCustomSignals removeObjectForKey:key]; } diff --git a/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h b/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h index dd77a9c75d2..1e583ae89cc 100644 --- a/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h +++ b/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h @@ -87,7 +87,7 @@ #pragma mark - Custom Signals /// A dictionary to hold custom signals that are set by the developer. -@property(nonatomic, readwrite, strong) NSMutableDictionary *customSignals; +@property(nonatomic, readwrite, strong) NSMutableDictionary *customSignals; #pragma mark Throttling properties diff --git a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m index 2a65fbc7e6a..d9e315504c4 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m @@ -533,7 +533,7 @@ - (void)setLastSetDefaultsTimeInterval:(NSTimeInterval)lastSetDefaultsTimestamp completionHandler:nil]; } -- (void)setCustomSignals:(NSMutableDictionary *)customSignals { +- (void)setCustomSignals:(NSMutableDictionary *)customSignals { _customSignals = customSignals; [_userDefaultsManager setCustomSignals:customSignals]; } diff --git a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h index e86ba0774c5..82a3e9c1492 100644 --- a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h +++ b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h @@ -48,7 +48,7 @@ NS_ASSUME_NONNULL_BEGIN /// Last active template version. @property(nonatomic, assign) NSString *lastActiveTemplateVersion; /// A dictionary to hold the latest custom signals set by the developer. -@property(nonatomic, readwrite, strong) NSMutableDictionary *customSignals; +@property(nonatomic, readwrite, strong) NSMutableDictionary *customSignals; /// Designated initializer. - (instancetype)initWithAppName:(NSString *)appName diff --git a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m index 0c7b6a4c699..9ee51558b90 100644 --- a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m +++ b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m @@ -142,16 +142,16 @@ - (void)setLastActiveTemplateVersion:(NSString *)templateVersion { } } -- (NSMutableDictionary *)customSignals { +- (NSMutableDictionary *)customSignals { NSDictionary *userDefaults = [self instanceUserDefaults]; if ([userDefaults objectForKey:kRCNUserDefaultsKeyCustomSignals]) { return [userDefaults objectForKey:kRCNUserDefaultsKeyCustomSignals]; } - return [[NSMutableDictionary alloc] init]; + return [[NSMutableDictionary alloc] init]; } -- (void)setCustomSignals:(NSMutableDictionary *)customSignals { +- (void)setCustomSignals:(NSMutableDictionary *)customSignals { if (customSignals) { [self setInstanceUserDefaultsValue:customSignals forKey:kRCNUserDefaultsKeyCustomSignals]; } diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m index afb476d92b1..5d70f5c9232 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m @@ -1851,7 +1851,7 @@ - (void)testSetCustomSingals { [_configInstances[i] setCustomSignals:testSignals WithCompletion:^(NSError *_Nullable error) { XCTAssertNil(error); - NSMutableDictionary *retrievedSignals = + NSMutableDictionary *retrievedSignals = self->_configInstances[i].settings.customSignals; XCTAssertEqualObjects(retrievedSignals, testSignals); [expectations[i] fulfill]; @@ -1883,9 +1883,9 @@ - (void)testSetCustomSignalsMultipleTimes { }; // Expected final set of signals - NSDictionary *expectedSignals = @{ + NSDictionary *expectedSignals = @{ @"signal1" : @"updatedValue1", - @"signal3" : @5, + @"signal3" : @"5", }; [_configInstances[i] setCustomSignals:testSignals1 @@ -1895,7 +1895,7 @@ - (void)testSetCustomSignalsMultipleTimes { setCustomSignals:testSignals2 WithCompletion:^(NSError *_Nullable error) { XCTAssertNil(error); - NSMutableDictionary *retrievedSignals = + NSMutableDictionary *retrievedSignals = self->_configInstances[i].settings.customSignals; XCTAssertEqualObjects(retrievedSignals, expectedSignals); [expectations[i] fulfill]; diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m b/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m index c463a8d0eac..e470da1d36b 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m @@ -23,8 +23,8 @@ static NSString* const AppName = @"testApp"; static NSString* const FQNamespace1 = @"testNamespace1:testApp"; static NSString* const FQNamespace2 = @"testNamespace2:testApp"; -static NSMutableDictionary* customSignals1 = nil; -static NSMutableDictionary* customSignals2 = nil; +static NSMutableDictionary* customSignals1 = nil; +static NSMutableDictionary* customSignals2 = nil; @interface RCNUserDefaultsManagerTests : XCTestCase @@ -270,7 +270,7 @@ - (void)testUserDefaultsForMultipleNamespaces { XCTAssertEqualObjects([manager1 lastActiveTemplateVersion], @"1"); XCTAssertEqualObjects([manager2 lastActiveTemplateVersion], @"2"); - /// Custom Singnals + /// Custom Signals [manager1 setCustomSignals:customSignals1]; [manager2 setCustomSignals:customSignals2]; XCTAssertEqualObjects([manager1 customSignals], customSignals1); From de73b8f2b21b424f931ee872dfb01a70cd7a5428 Mon Sep 17 00:00:00 2001 From: tusharkhandelwal8 Date: Fri, 22 Nov 2024 04:00:45 +0530 Subject: [PATCH 13/13] Extend CustomSignalValue to include Double type --- .../Swift/CustomSignals.swift | 19 ++++++++++++++++++- ...ebaseRemoteConfigSwift_APIBuildTests.swift | 4 +++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/FirebaseRemoteConfig/Swift/CustomSignals.swift b/FirebaseRemoteConfig/Swift/CustomSignals.swift index cb658700da3..4dfcdf2e13f 100644 --- a/FirebaseRemoteConfig/Swift/CustomSignals.swift +++ b/FirebaseRemoteConfig/Swift/CustomSignals.swift @@ -17,11 +17,13 @@ import Foundation @_exported import FirebaseRemoteConfigInternal #endif // SWIFT_PACKAGE -// TODO: Document. +/// Represents a value associated with a key in a custom signal, restricted to the allowed data +/// types : String, Int, Double. public struct CustomSignalValue { private enum Kind { case string(String) case integer(Int) + case double(Double) } private let kind: Kind @@ -44,12 +46,21 @@ public struct CustomSignalValue { Self(kind: .integer(integer)) } + /// Returns an floating-point backed custom signal. + /// - Parameter double: The given floating-point value to back the custom signal with. + /// - Returns: An floating-point backed custom signal + public static func double(_ double: Double) -> Self { + Self(kind: .double(double)) + } + fileprivate func toNSObject() -> NSObject { switch kind { case let .string(string): return string as NSString case let .integer(int): return int as NSNumber + case let .double(double): + return double as NSNumber } } } @@ -66,6 +77,12 @@ extension CustomSignalValue: ExpressibleByIntegerLiteral { } } +extension CustomSignalValue: ExpressibleByFloatLiteral { + public init(floatLiteral value: Double) { + self = .double(value) + } +} + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) public extension RemoteConfig { /// Sets custom signals for this Remote Config instance. diff --git a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift index c02f13566f4..8b70c99a609 100644 --- a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift +++ b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift @@ -231,7 +231,9 @@ final class FirebaseRemoteConfig_APIBuildTests: XCTestCase { "signal_3": 5, "signal_4": "enable_feature", "signal_5": "enable_feature_\("secret")", - "signal_6": nil, // Used to delete the custom signal for a given key. + "signal_6": .double(3.14), + "signal_7": 3.14159, + "signal_8": nil, // Used to delete the custom signal for a given key. ] try await config.setCustomSignals(signals) }