Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 1 addition & 24 deletions FirebaseRemoteConfig/Swift/CustomSignals.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public struct CustomSignalValue {
Self(kind: .double(double))
}

fileprivate func toNSObject() -> NSObject {
func toNSObject() -> NSObject {
switch kind {
case let .string(string):
return string as NSString
Expand Down Expand Up @@ -82,26 +82,3 @@ extension CustomSignalValue: ExpressibleByFloatLiteral {
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.
/// - Parameter customSignals: A dictionary mapping string keys to custom
/// signals to be set for the app instance.
///
/// When a new key is provided, a new key-value pair is added to the custom signals.
/// If an existing key is provided with a new value, the corresponding signal is updated.
/// If the value for a key is `nil`, the signal associated with that key is removed.
func setCustomSignals(_ customSignals: [String: CustomSignalValue?]) async throws {
return try await withCheckedThrowingContinuation { continuation in
let customSignals = customSignals.mapValues { $0?.toNSObject() ?? NSNull() }
self.__setCustomSignals(customSignals) { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
}
}
5 changes: 4 additions & 1 deletion FirebaseRemoteConfig/SwiftNew/ConfigConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ enum ConfigConstants {

/// Remote Config Error Domain.
static let remoteConfigErrorDomain = "com.google.remoteconfig.ErrorDomain"
// Remote Config Realtime Error Domain
/// Remote Config Realtime Error Domain
static let remoteConfigUpdateErrorDomain = "com.google.remoteconfig.update.ErrorDomain"
/// Error domain for custom signals errors.
static let remoteConfigCustomSignalsErrorDomain =
"com.google.remoteconfig.customsignals.ErrorDomain"

// MARK: - Fetch Response Keys

Expand Down
15 changes: 15 additions & 0 deletions FirebaseRemoteConfig/SwiftNew/ConfigSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,13 @@ let RCNHTTPDefaultConnectionTimeout: TimeInterval = 60
// Ignore JSON serialization error.
}
}
if customSignals.count > 0,
let jsonData = try? JSONSerialization.data(withJSONObject: customSignals),
let jsonString = String(data: jsonData, encoding: .utf8) {
request += ", custom_signals:\(jsonString)"
// Log the keys of the custom signals sent during fetch.
RCLog.debug("I-RCN000078", "Keys of custom signals during fetch: \(customSignals.keys)")
}
}
request += "}"
return request
Expand Down Expand Up @@ -524,6 +531,14 @@ let RCNHTTPDefaultConnectionTimeout: TimeInterval = 60
}
}

/// A dictionary to hold custom signals set by the developer.
@objc public var customSignals: [String: String] {
get { _userDefaultsManager.customSignals }
set {
_userDefaultsManager.customSignals = newValue
}
}

// MARK: - Throttling

/// Returns true if the last fetch is outside the minimum fetch interval supplied.
Expand Down
172 changes: 164 additions & 8 deletions FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public class RemoteConfigSettings: NSObject, NSCopying {

/// Indicates whether updated data was successfully fetched.
@objc(FIRRemoteConfigFetchStatus)
public enum RemoteConfigFetchStatus: Int {
public enum RemoteConfigFetchStatus: Int, Sendable {
/// Config has never been fetched.
case noFetchYet
/// Config fetch succeeded.
Expand Down Expand Up @@ -134,6 +134,17 @@ public enum RemoteConfigUpdateError: Int, LocalizedError, CustomNSError {
}
}

/// Firebase Remote Config custom signals error.
@objc(FIRRemoteConfigCustomSignalsError)
public enum RemoteConfigCustomSignalsError: Int, CustomNSError {
/// Unknown error.
case unknown = 8101
/// Invalid value type in the custom signals dictionary.
case invalidValueType = 8102
/// Limit exceeded for key length, value length, or number of signals.
case limitExceeded = 8103
}

/// 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
Expand Down Expand Up @@ -468,7 +479,10 @@ open class RemoteConfig: NSObject, NSFastEnumeration {
/// and avoid calling this method again.
///
/// - Parameter completionHandler Fetch operation callback with status and error parameters.
@objc public func fetch(completionHandler: ((RemoteConfigFetchStatus, Error?) -> Void)? = nil) {
@objc public func fetch(completionHandler: (
@Sendable (RemoteConfigFetchStatus, Error?) -> Void
)? =
nil) {
queue.async {
self.fetch(withExpirationDuration: self.settings.minimumFetchInterval,
completionHandler: completionHandler)
Expand Down Expand Up @@ -515,7 +529,10 @@ open class RemoteConfig: NSObject, NSFastEnumeration {
/// To stop the periodic sync, call `Installations.delete(completion:)`
/// and avoid calling this method again.
@objc public func fetch(withExpirationDuration expirationDuration: TimeInterval,
completionHandler: ((RemoteConfigFetchStatus, Error?) -> Void)? = nil) {
completionHandler: (
@Sendable (RemoteConfigFetchStatus, Error?) -> Void
)? =
nil) {
configFetch.fetchConfig(withExpirationDuration: expirationDuration,
completionHandler: completionHandler)
}
Expand Down Expand Up @@ -554,8 +571,7 @@ open class RemoteConfig: NSObject, NSFastEnumeration {
///
/// - Parameter completionHandler Fetch operation callback with status and error parameters.
@objc public func fetchAndActivate(completionHandler:
((RemoteConfigFetchAndActivateStatus, Error?) -> Void)? =
nil) {
(@Sendable (RemoteConfigFetchAndActivateStatus, Error?) -> Void)? = nil) {
fetch { [weak self] fetchStatus, error in
guard let self else { return }
// Fetch completed. We are being called on the main queue.
Expand Down Expand Up @@ -602,7 +618,7 @@ open class RemoteConfig: NSObject, NSFastEnumeration {
/// Applies Fetched Config data to the Active Config, causing updates to the behavior and
/// appearance of the app to take effect (depending on how config data is used in the app).
/// - Parameter completion Activate operation callback with changed and error parameters.
@objc public func activate(completion: ((Bool, Error?) -> Void)? = nil) {
@objc public func activate(completion: (@Sendable (Bool, Error?) -> Void)? = nil) {
queue.async { [weak self] in
guard let self else {
let error = NSError(
Expand Down Expand Up @@ -882,8 +898,9 @@ open class RemoteConfig: NSObject, NSFastEnumeration {
/// contains a remove method, which can be used to stop receiving updates for the provided
/// listener.
@discardableResult
@objc(addOnConfigUpdateListener:) public func addOnConfigUpdateListener(remoteConfigUpdateCompletion listener: @Sendable @escaping (RemoteConfigUpdate?,
Error?)
@objc(addOnConfigUpdateListener:)
public func addOnConfigUpdateListener(remoteConfigUpdateCompletion listener: @Sendable @escaping (RemoteConfigUpdate?,
Error?)
-> Void)
-> ConfigUpdateListenerRegistration {
return configRealtime.addConfigUpdateListener(listener)
Expand Down Expand Up @@ -951,6 +968,145 @@ open class RemoteConfig: NSObject, NSFastEnumeration {
}
return rolloutsAssignments
}

let customSignalsMaxKeyLength = 250
let customSignalsMaxStringValueLength = 500
let customSignalsMaxCount = 100

// MARK: - Custom Signals

/// 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.
///
/// When a new key is provided, a new key-value pair is added to the custom signals.
/// If an existing key is provided with a new value, the corresponding signal is updated.
/// If the value for a key is `nil`, the signal associated with that key is removed.
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
public
func setCustomSignals(_ customSignals: [String: CustomSignalValue?]) async throws {
return try await withUnsafeThrowingContinuation { continuation in
let customSignals = customSignals.mapValues { $0?.toNSObject() ?? NSNull() }
self.setCustomSignalsImpl(customSignals) { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
}

@available(swift 1000.0) // Objective-C only API
@objc(setCustomSignals:withCompletion:) public func __setCustomSignals(_ customSignals: [
String: Any
]?,
withCompletion completionHandler: (
@Sendable (Error?) -> Void
)?) {
setCustomSignalsImpl(customSignals, withCompletion: completionHandler)
}

private func setCustomSignalsImpl(_ customSignals: [String: Any]?,
withCompletion completionHandler: (
@Sendable (Error?) -> Void
)?) {
queue.async { [weak self] in
guard let self else { return }
guard let customSignals = customSignals else {
if let completionHandler {
DispatchQueue.main.async {
completionHandler(nil)
}
}
return
}

// Validate value type, and key and value length
for (key, value) in customSignals {
if !(value is NSNull || value is NSString || value is NSNumber) {
let error = NSError(
domain: ConfigConstants.remoteConfigCustomSignalsErrorDomain,
code: RemoteConfigCustomSignalsError.invalidValueType.rawValue,
userInfo: [
NSLocalizedDescriptionKey: "Invalid value type. Must be NSString, NSNumber, or NSNull.",
]
)
if let completionHandler {
DispatchQueue.main.async {
completionHandler(error)
}
}
return
}

if key.count > customSignalsMaxKeyLength ||
(value is NSString && (value as! NSString).length > customSignalsMaxStringValueLength) {
if let completionHandler {
let error = NSError(
domain: ConfigConstants.remoteConfigCustomSignalsErrorDomain,
code: RemoteConfigCustomSignalsError.limitExceeded.rawValue,
userInfo: [
NSLocalizedDescriptionKey:
"Custom signal keys and string values must be " +
"\(customSignalsMaxKeyLength) and " +
"\(customSignalsMaxStringValueLength) " +
"characters or less respectively.",
]
)
DispatchQueue.main.async {
completionHandler(error)
}
}
return
}
}

// Merge new signals with existing ones, overwriting existing keys.
// Also, remove entries where the new value is null.
var newCustomSignals = self.settings.customSignals

for (key, value) in customSignals {
if !(value is NSNull) {
let stringValue = value is NSNumber ? (value as! NSNumber).stringValue : value as! String
newCustomSignals[key] = stringValue
} else {
newCustomSignals.removeValue(forKey: key)
}
}

// Check the size limit.
if newCustomSignals.count > customSignalsMaxCount {
if let completionHandler {
let error = NSError(
domain: ConfigConstants.remoteConfigCustomSignalsErrorDomain,
code: RemoteConfigCustomSignalsError.limitExceeded.rawValue,
userInfo: [
NSLocalizedDescriptionKey:
"Custom signals count exceeds the limit of \(customSignalsMaxCount).",
]
)
DispatchQueue.main.async {
completionHandler(error)
}
}
return
}

// Update only if there are changes.
if newCustomSignals != self.settings.customSignals {
self.settings.customSignals = newCustomSignals
}

// Log the keys of the updated custom signals using RCLog.debug
RCLog.debug("I-RCN000078",
"Keys of updated custom signals: \(newCustomSignals.keys.sorted())")

DispatchQueue.main.async {
completionHandler?(nil)
}
}
}
}

// MARK: - Rollout Notification
Expand Down
8 changes: 8 additions & 0 deletions FirebaseRemoteConfig/SwiftNew/UserDefaultsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public class UserDefaultsManager: NSObject {
let kRCNUserDefaultsKeyNameCurrentRealtimeThrottlingRetryInterval =
"currentRealtimeThrottlingRetryInterval"
let kRCNUserDefaultsKeyNameRealtimeRetryCount = "realtimeRetryCount"
let kRCNUserDefaultsKeyCustomSignals = "customSignals"

// Delete when ObjC tests are gone.
@objc public convenience init(appName: String, bundleID: String, namespace: String) {
Expand Down Expand Up @@ -111,6 +112,13 @@ public class UserDefaultsManager: NSObject {
return "\(kRCNGroupPrefix).\(bundleIdentifier).\(kRCNGroupSuffix)"
}

@objc public var customSignals: [String: String] {
get { instanceUserDefaults[kRCNUserDefaultsKeyCustomSignals] as? [String: String] ?? [:] }
set {
setInstanceUserDefaultsValue(newValue, forKey: kRCNUserDefaultsKeyCustomSignals)
}
}

/// The last ETag received from the server.
@objc public var lastETag: String? {
get { instanceUserDefaults[kRCNUserDefaultsKeyNamelastETag] as? String }
Expand Down
2 changes: 1 addition & 1 deletion FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -1949,7 +1949,7 @@ - (void)testSetCustomSignalsMultipleTimes {
[_configInstances[i] setCustomSignals:testSignals1
withCompletion:^(NSError *_Nullable error) {
XCTAssertNil(error);
[_configInstances[i]
[self->_configInstances[i]
setCustomSignals:testSignals2
withCompletion:^(NSError *_Nullable error) {
XCTAssertNil(error);
Expand Down
Loading