Skip to content

Commit 221d6bc

Browse files
authored
Add the SuperTokens lazy user migration flow (#124)
* feat: Add the SuperTokens lazy user migration flow * fix: Code review fixes
1 parent 01aa55e commit 221d6bc

3 files changed

Lines changed: 128 additions & 0 deletions

File tree

Sources/Rownd/Models/RowndConfig.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,64 @@
77

88
import Foundation
99

10+
11+
public struct SuperTokensAppInfo: Encodable {
12+
public var appName: String
13+
public var apiDomain: String
14+
public var apiBasePath: String
15+
16+
internal var normalizedApiDomain: String? {
17+
let domain = apiDomain.trimmingCharacters(in: .whitespacesAndNewlines)
18+
.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
19+
20+
guard let url = URL(string: domain),
21+
let scheme = url.scheme?.lowercased(),
22+
scheme == "http" || scheme == "https",
23+
url.host != nil
24+
else {
25+
return nil
26+
}
27+
28+
return url.absoluteString.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
29+
}
30+
31+
internal var normalizedApiBasePath: String {
32+
let segments = apiBasePath
33+
.trimmingCharacters(in: .whitespacesAndNewlines)
34+
.split(separator: "/")
35+
.map(String.init)
36+
37+
return segments.isEmpty ? "" : "/" + segments.joined(separator: "/")
38+
}
39+
40+
internal var migrationURL: URL? {
41+
guard let normalizedApiDomain else {
42+
return nil
43+
}
44+
45+
guard var components = URLComponents(string: normalizedApiDomain) else {
46+
return nil
47+
}
48+
49+
components.path = normalizedApiBasePath + "/plugin/rownd/migrate"
50+
return components.url
51+
}
52+
53+
public init(appName: String, apiDomain: String, apiBasePath: String = "/auth") {
54+
self.appName = appName
55+
self.apiDomain = apiDomain
56+
self.apiBasePath = apiBasePath
57+
}
58+
}
59+
60+
public struct SuperTokensConfig: Encodable {
61+
public var appInfo: SuperTokensAppInfo
62+
63+
public init(appInfo: SuperTokensAppInfo) {
64+
self.appInfo = appInfo
65+
}
66+
}
67+
1068
public struct RowndConfig: Encodable {
1169
internal init() {}
1270

@@ -21,6 +79,7 @@ public struct RowndConfig: Encodable {
2179
public var customizations: RowndCustomizations = RowndCustomizations()
2280

2381
// These will not be encoded
82+
public var supertokens: SuperTokensConfig? = nil
2483
public var appGroupPrefix: String?
2584
public var enableSmartLinkPasteBehavior: Bool = true
2685
public var signInLinkPattern: String = ".*\\.rownd\\.link$"

Sources/Rownd/Rownd.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public class Rownd: NSObject {
4848
config.appKey = _appKey
4949
}
5050

51+
registerSuperTokensSyncEventHandler()
52+
5153
let state = await inst.inflateStoreCache()
5254

5355
// Skip the rest within app extensions
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import Foundation
2+
import OSLog
3+
4+
private let log = Logger(subsystem: "io.rownd.sdk", category: "supertokens-sync")
5+
6+
private final class SuperTokensSyncEventHandler: RowndEventHandlerDelegate {
7+
func handleRowndEvent(_ event: RowndEvent) {
8+
let userType = event.data?["user_type"] ?? nil
9+
10+
guard event.event == .signInCompleted,
11+
userType?.value as? String == "new_user",
12+
let appInfo = Rownd.config.supertokens?.appInfo
13+
else {
14+
return
15+
}
16+
17+
Task {
18+
do {
19+
guard let accessToken = try await Rownd.getAccessToken() else {
20+
return
21+
}
22+
23+
await syncUserToSuperTokens(accessToken: accessToken, appInfo: appInfo)
24+
} catch {
25+
log.error("[Rownd->ST] failed to read access token for migration: \(error.localizedDescription)")
26+
}
27+
}
28+
}
29+
}
30+
31+
private let superTokensSyncEventHandler = SuperTokensSyncEventHandler()
32+
33+
func registerSuperTokensSyncEventHandler() {
34+
let alreadyRegistered = Context.currentContext.eventListeners.contains { listener in
35+
listener === superTokensSyncEventHandler
36+
}
37+
38+
if !alreadyRegistered {
39+
Context.currentContext.eventListeners.append(superTokensSyncEventHandler)
40+
}
41+
}
42+
43+
func syncUserToSuperTokens(
44+
accessToken: String,
45+
appInfo: SuperTokensAppInfo
46+
) async {
47+
guard let url = appInfo.migrationURL else {
48+
log.error(
49+
"[Rownd->ST] invalid migration URL constructed from apiDomain=\(appInfo.apiDomain) apiBasePath=\(appInfo.apiBasePath)"
50+
)
51+
return
52+
}
53+
54+
do {
55+
var request = URLRequest(url: url)
56+
request.httpMethod = "POST"
57+
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
58+
request.httpShouldHandleCookies = true
59+
60+
let (_, response) = try await URLSession.shared.data(for: request)
61+
if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
62+
log.error("[Rownd->ST] migrate failed with status: \(http.statusCode)")
63+
}
64+
} catch {
65+
log.error("[Rownd->ST] migrate failed (non-fatal): \(error.localizedDescription)")
66+
}
67+
}

0 commit comments

Comments
 (0)