RevenueCat / purchases-ios

In-app purchases and subscriptions made easy. Support for iOS, watchOS, tvOS, macOS, and visionOS.
https://www.revenuecat.com/
MIT License
2.35k stars 317 forks source link

Custom `UserDefaults` Compatibility #4427

Open lucamegh opened 2 hours ago

lucamegh commented 2 hours ago

Our app uses a custom UserDefaultsClient interface to manage UserDefaults, defined as follows:

@DependencyClient
public struct UserDefaultsClient: Sendable {
  public var bool: @Sendable (_ forKey: String) -> Bool = { _ in false }

  public var setBool: @Sendable (_ forKey: String) -> Void

  ...
}

The SDK currently lacks support for abstractions like this, causing a compile error when trying to use UserDefaultsClient instead of UserDefaults:

@Dependency(UserDefaultsClient.self) var userDefaults

Purchases.configure(
  with: Configuration
      .builder(withAPIKey: apiKey)
      .with(userDefaults: userDefaults) // Compiler Error!
)

To work around this limitation, I am forced to add a new dependency endpoint that provides direct access to the wrapped UserDefaults instance.

@DependencyClient
public struct UserDefaultsClient: Sendable {
  ...

  public var wrapped: @Sendable () -> UserDefaults
}

However, this workaround defeats the purpose of abstraction, as it forces us back to using a concrete UserDefaults instance.

Solution

  1. Introduce UserDefaultsProtocol: Add a protocol to the SDK that mirrors the UserDefaults methods used within the SDK:
public protocol UserDefaultsProtcol {
  func bool(forKey defaultName: String) -> Bool

  func set(_ value: Bool, forKey defaultName: String)

  ...
}
  1. Extend Configuration.Builder: Add support for UserDefaultsProtocol in the Configuration.Builder:

    @objc public func with(userDefaults: UserDefaultsProtocol) -> Builder {
    self.userDefaults = userDefaults
    return self
    }
  2. Conform UserDefaults (and UserDefaultsClient) to UserDefaultsProtocol.

The solution enables custom UserDefaults implementations, such as UserDefaultsClient, to integrate seamlessly with the SDK, enhancing flexibility without introducing breaking changes to the public API.

RCGitBot commented 2 hours ago

👀 We've just linked this issue to our internal tracker and notified the team. Thank you for reporting, we're checking this out!