Skip to content

Commit

Permalink
api fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
paulb777 committed Jan 9, 2025
1 parent e173fcb commit d0c0c98
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 39 deletions.
129 changes: 116 additions & 13 deletions FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ public class RemoteConfig: NSObject, NSFastEnumeration {

@objc public var settings: ConfigSettings

private let configFetch: ConfigFetch
let configFetch: ConfigFetch

private let configExperiment: ConfigExperiment

Expand Down Expand Up @@ -201,8 +201,9 @@ public class RemoteConfig: NSObject, NSFastEnumeration {
// Use the provider to generate and return instances of FIRRemoteConfig for this specific app and
// namespace. This will ensure the app is configured before Remote Config can return an instance.
@objc(remoteConfigWithFIRNamespace:app:)
public static func remoteConfig(withFIRNamespace firebaseNamespace: String,
app: FirebaseApp) -> RemoteConfig {
public static func remoteConfig(withFIRNamespace firebaseNamespace: String = RemoteConfigConstants
.NamespaceGoogleMobilePlatform,
app: FirebaseApp) -> RemoteConfig {
let provider = ComponentType<RemoteConfigInterop>
.instance(
for: RemoteConfigInterop.self,
Expand Down Expand Up @@ -366,10 +367,23 @@ public class RemoteConfig: NSObject, NSFastEnumeration {
}
}

/// Ensures initialization is complete and clients can begin querying for Remote Config values.
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
public func ensureInitialized() async throws {
return try await withCheckedThrowingContinuation { continuation in
self.ensureInitialized { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
}

/// Ensures initialization is complete and clients can begin querying for Remote Config values.
/// - Parameter completionHandler: Initialization complete callback with error parameter.
@objc public func ensureInitialized(withCompletionHandler completionHandler: @escaping (Error?)
-> Void) {
@objc public func ensureInitialized(completionHandler: @escaping (Error?) -> Void) {
DispatchQueue.global(qos: .utility).async { [weak self] in
guard let self = self else { return }
let initializationSuccess = self.configContent.initializationSuccessful()
Expand Down Expand Up @@ -402,6 +416,27 @@ public class RemoteConfig: NSObject, NSFastEnumeration {

// MARK: fetch

/// Fetches Remote Config data with a callback. Call `activate()` to make fetched data
/// available to your app.
///
/// Note: This method uses a Firebase Installations token to identify the app instance, and once
/// it's called, it periodically sends data to the Firebase backend. (see
/// `Installations.authToken(completion:)`).
/// To stop the periodic sync, call `Installations.delete(completion:)`
/// and avoid calling this method again.
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
public func fetch() async throws -> RemoteConfigFetchStatus {
return try await withUnsafeThrowingContinuation() { continuation in
self.fetch { status, error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: status)
}
}
}
}

/// Fetches Remote Config data with a callback. Call `activate()` to make fetched data
/// available to your app.
///
Expand All @@ -419,6 +454,32 @@ public class RemoteConfig: NSObject, NSFastEnumeration {
}
}

/// Fetches Remote Config data and sets a duration that specifies how long config data lasts.
/// Call `activateWithCompletion:` to make fetched data available to your app.
///
/// - Parameter expirationDuration Override the (default or optionally set `minimumFetchInterval`
/// property in RemoteConfigSettings) `minimumFetchInterval` for only the current request, in
/// seconds. Setting a value of 0 seconds will force a fetch to the backend.
///
/// Note: This method uses a Firebase Installations token to identify the app instance, and once
/// it's called, it periodically sends data to the Firebase backend. (see
/// `Installations.authToken(completion:)`).
/// To stop the periodic sync, call `Installations.delete(completion:)`
/// and avoid calling this method again.
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
public func fetch(withExpirationDuration expirationDuration: TimeInterval) async throws
-> RemoteConfigFetchStatus {
return try await withCheckedThrowingContinuation { continuation in
self.fetch(withExpirationDuration: expirationDuration) { status, error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: status)
}
}
}
}

/// Fetches Remote Config data and sets a duration that specifies how long config data lasts.
/// Call `activateWithCompletion:` to make fetched data available to your app.
///
Expand All @@ -440,6 +501,28 @@ public class RemoteConfig: NSObject, NSFastEnumeration {

// MARK: fetchAndActivate

/// Fetches Remote Config data and if successful, activates fetched data. Optional completion
/// handler callback is invoked after the attempted activation of data, if the fetch call
/// succeeded.
///
/// Note: This method uses a Firebase Installations token to identify the app instance, and once
/// it's called, it periodically sends data to the Firebase backend. (see
/// `Installations.authToken(completion:)`).
/// To stop the periodic sync, call `Installations.delete(completion:)`
/// and avoid calling this method again.
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
public func fetchAndActivate() async throws -> RemoteConfigFetchAndActivateStatus {
return try await withCheckedThrowingContinuation { continuation in
self.fetchAndActivate { status, error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: status)
}
}
}
}

/// Fetches Remote Config data and if successful, activates fetched data. Optional completion
/// handler callback is invoked after the attempted activation of data, if the fetch call
/// succeeded.
Expand All @@ -451,8 +534,9 @@ public class RemoteConfig: NSObject, NSFastEnumeration {
/// and avoid calling this method again.
///
/// - Parameter completionHandler Fetch operation callback with status and error parameters.
@objc public func fetchAndActivate(withCompletionHandler completionHandler:
@escaping (RemoteConfigFetchAndActivateStatus, Error?) -> Void) {
@objc public func fetchAndActivate(completionHandler:
((RemoteConfigFetchAndActivateStatus, Error?) -> Void)? =
nil) {
fetch { [weak self] status, error in
guard let self = self else { return }
// Fetch completed. We are being called on the main queue.
Expand All @@ -461,11 +545,13 @@ public class RemoteConfig: NSObject, NSFastEnumeration {
self.activate { changed, error in
let status: RemoteConfigFetchAndActivateStatus = error == nil ?
.successFetchedFromRemote : .successUsingPreFetchedData
DispatchQueue.main.async {
completionHandler(status, nil)
if let completionHandler {
DispatchQueue.main.async {
completionHandler(status, nil)
}
}
}
} else {
} else if let completionHandler {
DispatchQueue.main.async {
completionHandler(.error, error)
}
Expand All @@ -475,10 +561,26 @@ public class RemoteConfig: NSObject, NSFastEnumeration {

// MARK: activate

/// 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).
/// - Returns A Bool indicating whether or not a change occurred.
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
public func activate() async throws -> Bool {
return try await withCheckedThrowingContinuation { continuation in

Check failure on line 569 in FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift

View workflow job for this annotation

GitHub Actions / remoteconfig (iOS)

-[FirebaseRemoteConfig_Unit_fake_console_tests.AsyncAwaitTests testFetchThenActivate] : failed: caught error: "Error Domain=com.google.abtesting Code=1 "Failed to get conditional user properties from Firebase Analytics." UserInfo={NSLocalizedDescription=Failed to get conditional user properties from Firebase Analytics.}"

Check failure on line 569 in FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift

View workflow job for this annotation

GitHub Actions / remoteconfig (iOS)

-[FirebaseRemoteConfig_Unit_fake_console_tests.AsyncAwaitTests testFetchWithExpirationThenActivate] : failed: caught error: "Error Domain=com.google.abtesting Code=1 "Failed to get conditional user properties from Firebase Analytics." UserInfo={NSLocalizedDescription=Failed to get conditional user properties from Firebase Analytics.}"
self.activate { updated, error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: updated)
}
}
}
}

/// 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(withCompletion completion: ((Bool, Error?) -> Void)?) {
@objc public func activate(completion: ((Bool, Error?) -> Void)? = nil) {
queue.async { [weak self] in
guard let self = self else {
let error = NSError(
Expand Down Expand Up @@ -759,8 +861,9 @@ public class RemoteConfig: NSObject, NSFastEnumeration {
/// - Returns Returns a registration representing the listener. The registration
/// contains a remove method, which can be used to stop receiving updates for the provided
/// listener.
@objc public func addOnConfigUpdateListener(_ listener: @Sendable @escaping (RemoteConfigUpdate?,
Error?) -> Void)
@objc public func addOnConfigUpdateListener(remoteConfigUpdateCompletion listener: @Sendable @escaping (RemoteConfigUpdate?,
Error?)
-> Void)
-> ConfigUpdateListenerRegistration {
return configRealtime.addConfigUpdateListener(listener)
}
Expand Down
13 changes: 9 additions & 4 deletions FirebaseRemoteConfig/Tests/Swift/FakeUtils/FakeConsole.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

@testable import FirebaseRemoteConfig
#if SWIFT_PACKAGE
import RemoteConfigFakeConsoleObjC
#endif
Expand All @@ -31,13 +32,17 @@ class FakeConsole {
func get() -> [String: AnyHashable] {
if config.count == 0 {
last = config
return [RCNFetchResponseKeyState: RCNFetchResponseKeyStateEmptyConfig]
return [ConfigConstants.fetchResponseKeyState: ConfigConstants
.fetchResponseKeyStateEmptyConfig]
}
var state = RCNFetchResponseKeyStateNoChange
var state = ConfigConstants.fetchResponseKeyStateNoChange
if last != config {
state = RCNFetchResponseKeyStateUpdate
state = ConfigConstants.fetchResponseKeyStateUpdate
}
last = config
return [RCNFetchResponseKeyState: state, RCNFetchResponseKeyEntries: config]
return [
ConfigConstants.fetchResponseKeyState: state,
ConfigConstants.fetchResponseKeyEntries: config,
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class URLSessionMock: URLSession, @unchecked Sendable {
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void)
-> URLSessionDataTask {
let consoleValues = fakeConsole.get()
if etag == "" || consoleValues["state"] as! String == RCNFetchResponseKeyStateUpdate {
if etag == "" || consoleValues["state"] as! String == "UPDATE" {
// Time string in microseconds to insure a different string from previous change.
etag = String(NSDate().timeIntervalSince1970)
}
Expand Down
4 changes: 2 additions & 2 deletions FirebaseRemoteConfig/Tests/Swift/SwiftAPI/APITestBase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import FirebaseCore
import FirebaseInstallations
import FirebaseRemoteConfig
@testable import FirebaseRemoteConfig

#if SWIFT_PACKAGE
import RemoteConfigFakeConsoleObjC
Expand Down Expand Up @@ -114,7 +114,7 @@ class APITestBase: XCTestCase {
}

// Uncomment for verbose debug logging.
// FirebaseConfiguration.shared.setLoggerLevel(FirebaseLoggerLevel.debug)
FirebaseConfiguration.shared.setLoggerLevel(FirebaseLoggerLevel.debug)
}

override func tearDown() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import FirebaseRemoteConfig
import FirebaseRemoteConfigInterop

final class FirebaseRemoteConfig_APIBuildTests: XCTestCase {
func usage() throws {
func usage(code: FirebaseRemoteConfig.RemoteConfigError,
updateErrorCode: FirebaseRemoteConfig.RemoteConfigUpdateError) throws {
// MARK: - FirebaseRemoteConfig

// TODO(ncooke3): These global constants should be lowercase.
Expand Down Expand Up @@ -51,13 +52,14 @@ final class FirebaseRemoteConfig_APIBuildTests: XCTestCase {
let nsError = NSError(domain: "", code: 0, userInfo: nil)

Check warning on line 52 in FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift

View workflow job for this annotation

GitHub Actions / remoteconfig (iOS)

initialization of immutable value 'nsError' was never used; consider replacing with assignment to '_' or removing it

Check warning on line 52 in FirebaseRemoteConfig/Tests/Swift/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift

View workflow job for this annotation

GitHub Actions / remoteconfig (iOS)

initialization of immutable value 'nsError' was never used; consider replacing with assignment to '_' or removing it

// TODO(ncooke3): Global constants should be lowercase.
let _: String = FirebaseRemoteConfig.RemoteConfigErrorDomain
let _ = FirebaseRemoteConfig.RemoteConfigError(_nsError: nsError)
let _: FirebaseRemoteConfig.RemoteConfigError.Code._ErrorType = FirebaseRemoteConfig
.RemoteConfigError(_nsError: nsError)
let _: String = FirebaseRemoteConfig.RemoteConfigError.errorDomain
let code: FirebaseRemoteConfig.RemoteConfigError.Code? = nil
switch code! {
// TODO(paulb777): Decide if ok to break and add release note.
// let _: String = FirebaseRemoteConfig.RemoteConfigErrorDomain
// let _ = FirebaseRemoteConfig.RemoteConfigError(_nsError: nsError)
// let _: FirebaseRemoteConfig.RemoteConfigError.Code._ErrorType = FirebaseRemoteConfig
// .RemoteConfigError(_nsError: nsError)
// let _: String = FirebaseRemoteConfig.RemoteConfigError.errorDomain
// let code: FirebaseRemoteConfig.RemoteConfigError
switch code {
case .unknown: break
case .throttled: break
case .internalError: break
Expand All @@ -68,13 +70,14 @@ final class FirebaseRemoteConfig_APIBuildTests: XCTestCase {
_ = FirebaseRemoteConfig.RemoteConfigError.internalError

// TODO(ncooke3): Global constants should be lowercase.
let _: String = FirebaseRemoteConfig.RemoteConfigUpdateErrorDomain
let _ = FirebaseRemoteConfig.RemoteConfigUpdateError(_nsError: nsError)
let _: FirebaseRemoteConfig.RemoteConfigUpdateError.Code._ErrorType = FirebaseRemoteConfig
.RemoteConfigUpdateError(_nsError: nsError)
let _: String = FirebaseRemoteConfig.RemoteConfigUpdateError.errorDomain
let updateErrorCode: FirebaseRemoteConfig.RemoteConfigUpdateError.Code? = nil
switch updateErrorCode! {
// TODO(paulb777): Decide if ok to break and add release note.
// let _: String = FirebaseRemoteConfig.RemoteConfigUpdateErrorDomain
// let _ = FirebaseRemoteConfig.RemoteConfigUpdateError(_nsError: nsError)
// let _: FirebaseRemoteConfig.RemoteConfigUpdateError.Code._ErrorType = FirebaseRemoteConfig
// .RemoteConfigUpdateError(_nsError: nsError)
// let _: String = FirebaseRemoteConfig.RemoteConfigUpdateError.errorDomain
// let updateErrorCode: FirebaseRemoteConfig.RemoteConfigUpdateError
switch updateErrorCode {
case .streamError: break
case .notFetched: break
case .messageInvalid: break
Expand Down Expand Up @@ -160,13 +163,16 @@ final class FirebaseRemoteConfig_APIBuildTests: XCTestCase {
let _: FirebaseRemoteConfig.RemoteConfigValue = config["key"]
let _: FirebaseRemoteConfig.RemoteConfigValue = config.configValue(forKey: "key")
// TODO(ncooke3): Should `nil` be acceptable here in a Swift context?
let _: FirebaseRemoteConfig.RemoteConfigValue = config.configValue(forKey: nil)
let _: FirebaseRemoteConfig.RemoteConfigValue = config.configValue(forKey: "key")
let _: FirebaseRemoteConfig.RemoteConfigValue = config.configValue(
forKey: "key",
source: source
)
// TODO(ncooke3): Should `nil` be acceptable here in a Swift context?
let _: FirebaseRemoteConfig.RemoteConfigValue = config.configValue(forKey: nil, source: source)
let _: FirebaseRemoteConfig.RemoteConfigValue = config.configValue(
forKey: "key",
source: source
)

let _: [String] = config.allKeys(from: source)

Expand All @@ -184,8 +190,7 @@ final class FirebaseRemoteConfig_APIBuildTests: XCTestCase {
config.setDefaults(fromPlist: nil)

let _: FirebaseRemoteConfig.RemoteConfigValue? = config.defaultValue(forKey: "")
// TODO(ncooke3): Should `nil` be acceptable here in a Swift context?
let _: FirebaseRemoteConfig.RemoteConfigValue? = config.defaultValue(forKey: nil)
let _: FirebaseRemoteConfig.RemoteConfigValue? = config.defaultValue(forKey: "key")

let _: FirebaseRemoteConfig.ConfigUpdateListenerRegistration = config
.addOnConfigUpdateListener(
Expand Down

0 comments on commit d0c0c98

Please sign in to comment.