diff --git a/README.md b/README.md index 73c8c0a..5e85d28 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,182 @@ StableID.configure(idGenerator: MyCustomIDGenerator()) ## 📚 Examples -_Coming soon_ +
+Example 1: Basic Setup with RevenueCat + +Configure StableID and use it to configure RevenueCat with a consistent user identifier: + +```swift +import StableID +import RevenueCat + +class AppDelegate: UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + // Configure StableID first + if StableID.hasStoredID { + StableID.configure() + + // Configure RevenueCat with StableID + Purchases.configure(withAPIKey: "your_api_key", appUserID: StableID.id) + } else { + Task { + // Try to fetch AppTransactionID, fallback to generated ID if it fails + if let id = try? await StableID.fetchAppTransactionID() { + StableID.configure(id: id) + } else { + StableID.configure() + } + + // Configure RevenueCat after StableID is ready + Purchases.configure(withAPIKey: "your_api_key", appUserID: StableID.id) + } + } + + return true + } +} +``` + +
+ +
+Example 2: Handling ID Changes with RevenueCat + +Use the delegate pattern to update RevenueCat when the StableID changes (e.g., from another device via iCloud): + +```swift +import StableID +import RevenueCat + +class AppCoordinator: StableIDDelegate { + init() { + // Set up StableID delegate + StableID.set(delegate: self) + } + + func willChangeID(currentID: String, candidateID: String) -> String? { + // Optional: validate or modify the candidate ID + return nil + } + + func didChangeID(newID: String) { + // Update RevenueCat with the new ID + Purchases.shared.logIn(newID) { customerInfo, created, error in + if let error = error { + print("Error updating RevenueCat user: \(error)") + } else { + print("Successfully updated RevenueCat user to: \(newID)") + } + } + } +} +``` + +
+ +
+Example 3: User Login Flow + +Handle the case where a user logs into your app with their own account: + +```swift +import StableID +import RevenueCat + +func userDidLogin(userID: String) { + // Update StableID to use the user's account ID + StableID.identify(id: userID) + + // Update RevenueCat to match + Purchases.shared.logIn(userID) { customerInfo, created, error in + if let error = error { + print("Error logging in to RevenueCat: \(error)") + } else { + print("Successfully logged in to RevenueCat") + } + } +} + +func userDidLogout() { + // Generate a new anonymous ID + StableID.generateNewID() + + // Switch RevenueCat to the new anonymous ID + Purchases.shared.logIn(StableID.id) { customerInfo, created, error in + if let error = error { + print("Error switching to anonymous ID: \(error)") + } else { + print("Switched to anonymous ID: \(StableID.id)") + } + } +} +``` + +
+ +
+Example 4: SwiftUI App with Async Configuration + +For SwiftUI apps, configure StableID and RevenueCat in your App struct: + +```swift +import SwiftUI +import StableID +import RevenueCat + +@main +struct MyApp: App { + init() { + // Configure StableID with .preferStored policy + Task { + do { + let id = try await StableID.fetchAppTransactionID() + StableID.configure(id: id, policy: .preferStored) + + // Configure RevenueCat after StableID is ready + Purchases.configure(withAPIKey: "your_api_key", appUserID: StableID.id) + } catch { + print("Error configuring StableID: \(error)") + // Fallback to generated ID + StableID.configure() + Purchases.configure(withAPIKey: "your_api_key", appUserID: StableID.id) + } + } + } + + var body: some Scene { + WindowGroup { + ContentView() + } + } +} +``` + +
+ +
+Example 5: Custom ID Generator for Testing + +Use a custom ID generator for testing or specific formatting requirements: + +```swift +import StableID + +struct TestIDGenerator: IDGenerator { + func generateID() -> String { + return "test-user-\(UUID().uuidString.prefix(8))" + } +} + +#if DEBUG +StableID.configure(idGenerator: TestIDGenerator()) +#else +StableID.configure() +#endif +``` + +
## 📙 License diff --git a/Sources/StableID/Delegate/Delegate.swift b/Sources/StableID/Delegate/Delegate.swift index b3c6926..8fb3837 100644 --- a/Sources/StableID/Delegate/Delegate.swift +++ b/Sources/StableID/Delegate/Delegate.swift @@ -9,9 +9,19 @@ import Foundation public protocol StableIDDelegate { /// Called when StableID is about to change the identified user ID. - /// Return `nil` to prevent the change. + /// + /// Use this to validate or modify the candidate ID before it's set. + /// + /// - Parameters: + /// - currentID: The current user ID + /// - candidateID: The proposed new user ID + /// - Returns: An adjusted ID to use instead, or `nil` to use the candidate ID as-is func willChangeID(currentID: String, candidateID: String) -> String? /// Called after StableID changes the identified user ID. + /// + /// Use this to sync the new ID with other services like RevenueCat. + /// + /// - Parameter newID: The new user ID that was set func didChangeID(newID: String) } diff --git a/Sources/StableID/StableID.swift b/Sources/StableID/StableID.swift index cfb4a50..8b7edd9 100644 --- a/Sources/StableID/StableID.swift +++ b/Sources/StableID/StableID.swift @@ -34,6 +34,30 @@ public class StableID { private static var remoteStore = NSUbiquitousKeyValueStore.default private static var localStore = UserDefaults(suiteName: Constants.StableID_Key_DefaultsSuiteName) + /// Configures StableID with an optional user identifier. + /// + /// This method initializes the StableID system and must be called before accessing any other StableID methods. + /// It can only be called once per app session - subsequent calls will be ignored. + /// + /// - Parameters: + /// - id: An optional identifier to use. If nil, will use a stored ID (from iCloud or local storage) or generate a new one. + /// - idGenerator: The generator to use for creating new IDs. Defaults to `StandardGenerator()` which produces UUIDs. + /// - policy: Controls how the provided ID is handled. Defaults to `.forceUpdate`. + /// - `.forceUpdate`: Always uses the provided ID and updates storage + /// - `.preferStored`: Uses stored ID if available, otherwise uses the provided ID + /// + /// Example usage: + /// ```swift + /// // Basic configuration with auto-generated ID + /// StableID.configure() + /// + /// // With AppTransactionID + /// if let id = try? await StableID.fetchAppTransactionID() { + /// StableID.configure(id: id, policy: .preferStored) + /// } else { + /// StableID.configure() + /// } + /// ``` public static func configure(id: String? = nil, idGenerator: IDGenerator = StandardGenerator(), policy: IDPolicy = .forceUpdate) { guard isConfigured == false else { Self.logger.log(type: .error, message: "StableID has already been configured! Call `identify` to change the identifier.") @@ -135,12 +159,12 @@ public class StableID { private func didChangeExternally(_ notification: Notification) { if let newId = Self.remoteStore.string(forKey: Constants.StableID_Key_Identifier) { if newId != id { - Self.logger.log(type: .info, message: "Detected new StableID: \(newId)") + Self.logger.log(type: .info, message: "Detected new StableID in iCloud: \(newId)") self.setIdentity(value: newId) } else { // the identifier was updated remotely, but it's the same identifier - Self.logger.log(type: .info, message: "No change to StableID.") + Self.logger.log(type: .info, message: "StableID was updated in iCloud, but it's the same identifier that's already configured.") } } else { @@ -156,22 +180,118 @@ public class StableID { /// Public methods extension StableID { + /// Returns whether StableID has been configured. + /// + /// Check this property before calling `configure()` to avoid double-configuration errors. + /// + /// Example usage: + /// ```swift + /// if !StableID.isConfigured { + /// StableID.configure() + /// } + /// ``` public static var isConfigured: Bool { instance != nil } + /// The current stable identifier. + /// + /// This property returns the active user ID. It persists across app launches and syncs via iCloud. + /// + /// Example usage: + /// ```swift + /// let userID = StableID.id + /// Purchases.configure(withAPIKey: "key", appUserID: userID) + /// ``` + /// + /// - Warning: Accessing this property before calling `configure()` will result in a fatal error. public static var id: String { return Self.shared.id } + /// Changes the current identifier to a new value. + /// + /// Use this method to update the user's identifier, for example when a user logs in with their account. + /// The new ID will be persisted to both local storage and iCloud, and any configured delegates will be notified. + /// + /// - Parameter id: The new identifier to use. + /// + /// Example usage: + /// ```swift + /// // When user logs in + /// StableID.identify(id: "user-account-123") + /// + /// // Update RevenueCat + /// Purchases.shared.logIn("user-account-123") { _, _, _ in } + /// ``` + /// + /// - Warning: Calling this method before `configure()` will result in a fatal error. public static func identify(id: String) { Self.shared.setIdentity(value: id) } + /// Generates and sets a new random identifier. + /// + /// This method creates a new ID using the configured ID generator and updates both local and iCloud storage. + /// Use this when you need to create a new anonymous user, for example after a user logs out. + /// + /// Example usage: + /// ```swift + /// // When user logs out + /// StableID.generateNewID() + /// + /// // Update RevenueCat with new anonymous ID + /// Purchases.shared.logOut { _ in + /// Purchases.shared.logIn(StableID.id) { _, _, _ in } + /// } + /// ``` + /// + /// - Warning: Calling this method before `configure()` will result in a fatal error. public static func generateNewID() { Self.shared.generateID() } + /// Sets a delegate to receive notifications when the ID changes. + /// + /// The delegate receives callbacks before and after ID changes, allowing you to validate + /// or respond to ID updates from any source (manual, iCloud sync, or generation). + /// + /// - Parameter delegate: An object conforming to `StableIDDelegate`. + /// + /// Example usage: + /// ```swift + /// class MyDelegate: StableIDDelegate { + /// func willChangeID(currentID: String, candidateID: String) -> String? { + /// // Optional: validate or modify the candidate ID + /// return nil + /// } + /// + /// func didChangeID(newID: String) { + /// // Sync with your backend or RevenueCat + /// Purchases.shared.logIn(newID) { _, _, _ in } + /// } + /// } + /// + /// StableID.set(delegate: MyDelegate()) + /// ``` + /// + /// - Warning: Calling this method before `configure()` will result in a fatal error. public static func set(delegate: any StableIDDelegate) { Self.shared.delegate = delegate } - + + /// Returns whether an ID is stored in iCloud or local storage. + /// + /// Use this property to determine if you need to fetch a new ID (like AppTransactionID) + /// or if you can use the existing stored value. + /// + /// Example usage: + /// ```swift + /// if StableID.hasStoredID { + /// StableID.configure() + /// } else { + /// // Fetch AppTransactionID only if needed + /// if let id = try? await StableID.fetchAppTransactionID() { + /// StableID.configure(id: id) + /// } + /// } + /// ``` public static var hasStoredID: Bool { fetchStoredID() != nil } /// Resets the StableID instance. For testing purposes only.