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.