diff --git a/Sources/Rownd/Models/RowndConfig.swift b/Sources/Rownd/Models/RowndConfig.swift index b2d72b5..6919001 100644 --- a/Sources/Rownd/Models/RowndConfig.swift +++ b/Sources/Rownd/Models/RowndConfig.swift @@ -7,6 +7,64 @@ import Foundation + +public struct SuperTokensAppInfo: Encodable { + public var appName: String + public var apiDomain: String + public var apiBasePath: String + + internal var normalizedApiDomain: String? { + let domain = apiDomain.trimmingCharacters(in: .whitespacesAndNewlines) + .trimmingCharacters(in: CharacterSet(charactersIn: "/")) + + guard let url = URL(string: domain), + let scheme = url.scheme?.lowercased(), + scheme == "http" || scheme == "https", + url.host != nil + else { + return nil + } + + return url.absoluteString.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + } + + internal var normalizedApiBasePath: String { + let segments = apiBasePath + .trimmingCharacters(in: .whitespacesAndNewlines) + .split(separator: "/") + .map(String.init) + + return segments.isEmpty ? "" : "/" + segments.joined(separator: "/") + } + + internal var migrationURL: URL? { + guard let normalizedApiDomain else { + return nil + } + + guard var components = URLComponents(string: normalizedApiDomain) else { + return nil + } + + components.path = normalizedApiBasePath + "/plugin/rownd/migrate" + return components.url + } + + public init(appName: String, apiDomain: String, apiBasePath: String = "/auth") { + self.appName = appName + self.apiDomain = apiDomain + self.apiBasePath = apiBasePath + } +} + +public struct SuperTokensConfig: Encodable { + public var appInfo: SuperTokensAppInfo + + public init(appInfo: SuperTokensAppInfo) { + self.appInfo = appInfo + } +} + public struct RowndConfig: Encodable { internal init() {} @@ -21,6 +79,7 @@ public struct RowndConfig: Encodable { public var customizations: RowndCustomizations = RowndCustomizations() // These will not be encoded + public var supertokens: SuperTokensConfig? = nil public var appGroupPrefix: String? public var enableSmartLinkPasteBehavior: Bool = true public var signInLinkPattern: String = ".*\\.rownd\\.link$" diff --git a/Sources/Rownd/Rownd.swift b/Sources/Rownd/Rownd.swift index c702ddf..bb65f84 100644 --- a/Sources/Rownd/Rownd.swift +++ b/Sources/Rownd/Rownd.swift @@ -48,6 +48,8 @@ public class Rownd: NSObject { config.appKey = _appKey } + registerSuperTokensSyncEventHandler() + let state = await inst.inflateStoreCache() // Skip the rest within app extensions diff --git a/Sources/Rownd/framework/SuperTokensSync.swift b/Sources/Rownd/framework/SuperTokensSync.swift new file mode 100644 index 0000000..44db1f0 --- /dev/null +++ b/Sources/Rownd/framework/SuperTokensSync.swift @@ -0,0 +1,67 @@ +import Foundation +import OSLog + +private let log = Logger(subsystem: "io.rownd.sdk", category: "supertokens-sync") + +private final class SuperTokensSyncEventHandler: RowndEventHandlerDelegate { + func handleRowndEvent(_ event: RowndEvent) { + let userType = event.data?["user_type"] ?? nil + + guard event.event == .signInCompleted, + userType?.value as? String == "new_user", + let appInfo = Rownd.config.supertokens?.appInfo + else { + return + } + + Task { + do { + guard let accessToken = try await Rownd.getAccessToken() else { + return + } + + await syncUserToSuperTokens(accessToken: accessToken, appInfo: appInfo) + } catch { + log.error("[Rownd->ST] failed to read access token for migration: \(error.localizedDescription)") + } + } + } +} + +private let superTokensSyncEventHandler = SuperTokensSyncEventHandler() + +func registerSuperTokensSyncEventHandler() { + let alreadyRegistered = Context.currentContext.eventListeners.contains { listener in + listener === superTokensSyncEventHandler + } + + if !alreadyRegistered { + Context.currentContext.eventListeners.append(superTokensSyncEventHandler) + } +} + +func syncUserToSuperTokens( + accessToken: String, + appInfo: SuperTokensAppInfo +) async { + guard let url = appInfo.migrationURL else { + log.error( + "[Rownd->ST] invalid migration URL constructed from apiDomain=\(appInfo.apiDomain) apiBasePath=\(appInfo.apiBasePath)" + ) + return + } + + do { + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") + request.httpShouldHandleCookies = true + + let (_, response) = try await URLSession.shared.data(for: request) + if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) { + log.error("[Rownd->ST] migrate failed with status: \(http.statusCode)") + } + } catch { + log.error("[Rownd->ST] migrate failed (non-fatal): \(error.localizedDescription)") + } +}