@capgo/capacitor-supabase 8.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,628 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import Supabase
4
+
5
+ @objc(CapacitorSupabasePlugin)
6
+ public class CapacitorSupabasePlugin: CAPPlugin, CAPBridgedPlugin {
7
+ private let pluginVersion: String = "8.0.4"
8
+ public let identifier = "CapacitorSupabasePlugin"
9
+ public let jsName = "CapacitorSupabase"
10
+ public let pluginMethods: [CAPPluginMethod] = [
11
+ CAPPluginMethod(name: "initialize", returnType: CAPPluginReturnPromise),
12
+ CAPPluginMethod(name: "signInWithPassword", returnType: CAPPluginReturnPromise),
13
+ CAPPluginMethod(name: "signUp", returnType: CAPPluginReturnPromise),
14
+ CAPPluginMethod(name: "signInWithOAuth", returnType: CAPPluginReturnPromise),
15
+ CAPPluginMethod(name: "signInWithOtp", returnType: CAPPluginReturnPromise),
16
+ CAPPluginMethod(name: "verifyOtp", returnType: CAPPluginReturnPromise),
17
+ CAPPluginMethod(name: "signOut", returnType: CAPPluginReturnPromise),
18
+ CAPPluginMethod(name: "getSession", returnType: CAPPluginReturnPromise),
19
+ CAPPluginMethod(name: "refreshSession", returnType: CAPPluginReturnPromise),
20
+ CAPPluginMethod(name: "getUser", returnType: CAPPluginReturnPromise),
21
+ CAPPluginMethod(name: "setSession", returnType: CAPPluginReturnPromise),
22
+ CAPPluginMethod(name: "select", returnType: CAPPluginReturnPromise),
23
+ CAPPluginMethod(name: "insert", returnType: CAPPluginReturnPromise),
24
+ CAPPluginMethod(name: "update", returnType: CAPPluginReturnPromise),
25
+ CAPPluginMethod(name: "delete", returnType: CAPPluginReturnPromise),
26
+ CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise)
27
+ ]
28
+
29
+ private var supabaseClient: SupabaseClient?
30
+ private var authStateTask: Task<Void, Never>?
31
+
32
+ deinit {
33
+ authStateTask?.cancel()
34
+ }
35
+
36
+ @objc func initialize(_ call: CAPPluginCall) {
37
+ guard let supabaseUrl = call.getString("supabaseUrl"),
38
+ let supabaseKey = call.getString("supabaseKey"),
39
+ let url = URL(string: supabaseUrl) else {
40
+ call.reject("Missing or invalid supabaseUrl or supabaseKey")
41
+ return
42
+ }
43
+
44
+ supabaseClient = SupabaseClient(
45
+ supabaseURL: url,
46
+ supabaseKey: supabaseKey
47
+ )
48
+
49
+ setupAuthStateListener()
50
+ call.resolve()
51
+ }
52
+
53
+ private func setupAuthStateListener() {
54
+ authStateTask?.cancel()
55
+ authStateTask = Task { [weak self] in
56
+ guard let client = self?.supabaseClient else { return }
57
+ for await (event, session) in client.auth.authStateChanges {
58
+ guard !Task.isCancelled else { break }
59
+ let eventName: String
60
+ switch event {
61
+ case .initialSession:
62
+ eventName = "INITIAL_SESSION"
63
+ case .signedIn:
64
+ eventName = "SIGNED_IN"
65
+ case .signedOut:
66
+ eventName = "SIGNED_OUT"
67
+ case .tokenRefreshed:
68
+ eventName = "TOKEN_REFRESHED"
69
+ case .userUpdated:
70
+ eventName = "USER_UPDATED"
71
+ case .passwordRecovery:
72
+ eventName = "PASSWORD_RECOVERY"
73
+ default:
74
+ continue
75
+ }
76
+
77
+ var data: [String: Any] = ["event": eventName]
78
+ if let session = session {
79
+ data["session"] = self?.sessionToDict(session)
80
+ } else {
81
+ data["session"] = NSNull()
82
+ }
83
+
84
+ self?.notifyListeners("authStateChange", data: data)
85
+ }
86
+ }
87
+ }
88
+
89
+ @objc func signInWithPassword(_ call: CAPPluginCall) {
90
+ guard let client = supabaseClient else {
91
+ call.reject("Supabase client not initialized. Call initialize() first.")
92
+ return
93
+ }
94
+
95
+ guard let email = call.getString("email"),
96
+ let password = call.getString("password") else {
97
+ call.reject("Missing email or password")
98
+ return
99
+ }
100
+
101
+ Task {
102
+ do {
103
+ let session = try await client.auth.signIn(email: email, password: password)
104
+ call.resolve(authResultToDict(session: session, user: session.user))
105
+ } catch {
106
+ call.reject("Sign in failed: \(error.localizedDescription)")
107
+ }
108
+ }
109
+ }
110
+
111
+ @objc func signUp(_ call: CAPPluginCall) {
112
+ guard let client = supabaseClient else {
113
+ call.reject("Supabase client not initialized. Call initialize() first.")
114
+ return
115
+ }
116
+
117
+ guard let email = call.getString("email"),
118
+ let password = call.getString("password") else {
119
+ call.reject("Missing email or password")
120
+ return
121
+ }
122
+
123
+ let userData = call.getObject("data")
124
+
125
+ Task {
126
+ do {
127
+ let response: AuthResponse
128
+ if let userData = userData {
129
+ let jsonData = try JSONSerialization.data(withJSONObject: userData)
130
+ let decodedData = try JSONDecoder().decode([String: AnyJSON].self, from: jsonData)
131
+ response = try await client.auth.signUp(email: email, password: password, data: decodedData)
132
+ } else {
133
+ response = try await client.auth.signUp(email: email, password: password)
134
+ }
135
+ call.resolve(authResultToDict(session: response.session, user: response.user))
136
+ } catch {
137
+ call.reject("Sign up failed: \(error.localizedDescription)")
138
+ }
139
+ }
140
+ }
141
+
142
+ @objc func signInWithOAuth(_ call: CAPPluginCall) {
143
+ guard let client = supabaseClient else {
144
+ call.reject("Supabase client not initialized. Call initialize() first.")
145
+ return
146
+ }
147
+
148
+ guard let providerString = call.getString("provider"),
149
+ let provider = oauthProviderFromString(providerString) else {
150
+ call.reject("Missing or invalid provider")
151
+ return
152
+ }
153
+
154
+ let redirectTo = call.getString("redirectTo")
155
+ let scopes = call.getString("scopes")
156
+
157
+ Task {
158
+ do {
159
+ let url = try await client.auth.getOAuthSignInURL(
160
+ provider: provider,
161
+ scopes: scopes,
162
+ redirectTo: redirectTo.flatMap { URL(string: $0) }
163
+ )
164
+ await MainActor.run {
165
+ UIApplication.shared.open(url)
166
+ }
167
+ call.resolve()
168
+ } catch {
169
+ call.reject("OAuth sign in failed: \(error.localizedDescription)")
170
+ }
171
+ }
172
+ }
173
+
174
+ @objc func signInWithOtp(_ call: CAPPluginCall) {
175
+ guard let client = supabaseClient else {
176
+ call.reject("Supabase client not initialized. Call initialize() first.")
177
+ return
178
+ }
179
+
180
+ let email = call.getString("email")
181
+ let phone = call.getString("phone")
182
+
183
+ guard email != nil || phone != nil else {
184
+ call.reject("Either email or phone is required")
185
+ return
186
+ }
187
+
188
+ Task {
189
+ do {
190
+ if let email = email {
191
+ try await client.auth.signInWithOTP(email: email)
192
+ } else if let phone = phone {
193
+ try await client.auth.signInWithOTP(phone: phone)
194
+ }
195
+ call.resolve()
196
+ } catch {
197
+ call.reject("OTP sign in failed: \(error.localizedDescription)")
198
+ }
199
+ }
200
+ }
201
+
202
+ @objc func verifyOtp(_ call: CAPPluginCall) {
203
+ guard let client = supabaseClient else {
204
+ call.reject("Supabase client not initialized. Call initialize() first.")
205
+ return
206
+ }
207
+
208
+ guard let token = call.getString("token"),
209
+ let typeString = call.getString("type") else {
210
+ call.reject("Missing token or type")
211
+ return
212
+ }
213
+
214
+ let email = call.getString("email")
215
+ let phone = call.getString("phone")
216
+
217
+ guard email != nil || phone != nil else {
218
+ call.reject("Either email or phone is required")
219
+ return
220
+ }
221
+
222
+ Task {
223
+ do {
224
+ let response: AuthResponse
225
+ if let email = email {
226
+ let type = emailOtpTypeFromString(typeString)
227
+ response = try await client.auth.verifyOTP(email: email, token: token, type: type)
228
+ } else if let phone = phone {
229
+ let type = phoneOtpTypeFromString(typeString)
230
+ response = try await client.auth.verifyOTP(phone: phone, token: token, type: type)
231
+ } else {
232
+ call.reject("Either email or phone is required")
233
+ return
234
+ }
235
+ call.resolve(authResultToDict(session: response.session, user: response.user))
236
+ } catch {
237
+ call.reject("OTP verification failed: \(error.localizedDescription)")
238
+ }
239
+ }
240
+ }
241
+
242
+ @objc func signOut(_ call: CAPPluginCall) {
243
+ guard let client = supabaseClient else {
244
+ call.reject("Supabase client not initialized. Call initialize() first.")
245
+ return
246
+ }
247
+
248
+ Task {
249
+ do {
250
+ try await client.auth.signOut()
251
+ call.resolve()
252
+ } catch {
253
+ call.reject("Sign out failed: \(error.localizedDescription)")
254
+ }
255
+ }
256
+ }
257
+
258
+ @objc func getSession(_ call: CAPPluginCall) {
259
+ guard let client = supabaseClient else {
260
+ call.reject("Supabase client not initialized. Call initialize() first.")
261
+ return
262
+ }
263
+
264
+ Task {
265
+ do {
266
+ let session = try await client.auth.session
267
+ call.resolve(["session": sessionToDict(session)])
268
+ } catch {
269
+ call.resolve(["session": NSNull()])
270
+ }
271
+ }
272
+ }
273
+
274
+ @objc func refreshSession(_ call: CAPPluginCall) {
275
+ guard let client = supabaseClient else {
276
+ call.reject("Supabase client not initialized. Call initialize() first.")
277
+ return
278
+ }
279
+
280
+ Task {
281
+ do {
282
+ let session = try await client.auth.refreshSession()
283
+ call.resolve(["session": sessionToDict(session)])
284
+ } catch {
285
+ call.reject("Session refresh failed: \(error.localizedDescription)")
286
+ }
287
+ }
288
+ }
289
+
290
+ @objc func getUser(_ call: CAPPluginCall) {
291
+ guard let client = supabaseClient else {
292
+ call.reject("Supabase client not initialized. Call initialize() first.")
293
+ return
294
+ }
295
+
296
+ Task {
297
+ do {
298
+ let user = try await client.auth.user()
299
+ call.resolve(["user": userToDict(user)])
300
+ } catch {
301
+ call.resolve(["user": NSNull()])
302
+ }
303
+ }
304
+ }
305
+
306
+ @objc func setSession(_ call: CAPPluginCall) {
307
+ guard let client = supabaseClient else {
308
+ call.reject("Supabase client not initialized. Call initialize() first.")
309
+ return
310
+ }
311
+
312
+ guard let accessToken = call.getString("accessToken"),
313
+ let refreshToken = call.getString("refreshToken") else {
314
+ call.reject("Missing accessToken or refreshToken")
315
+ return
316
+ }
317
+
318
+ Task {
319
+ do {
320
+ let session = try await client.auth.setSession(accessToken: accessToken, refreshToken: refreshToken)
321
+ call.resolve(["session": sessionToDict(session)])
322
+ } catch {
323
+ call.reject("Set session failed: \(error.localizedDescription)")
324
+ }
325
+ }
326
+ }
327
+
328
+ // MARK: - Database Operations
329
+
330
+ @objc func select(_ call: CAPPluginCall) {
331
+ guard let client = supabaseClient else {
332
+ call.reject("Supabase client not initialized. Call initialize() first.")
333
+ return
334
+ }
335
+
336
+ guard let table = call.getString("table") else {
337
+ call.reject("Missing table name")
338
+ return
339
+ }
340
+
341
+ let columns = call.getString("columns") ?? "*"
342
+ let filter = call.getObject("filter")
343
+ let limit = call.getInt("limit")
344
+ let offset = call.getInt("offset")
345
+ let orderBy = call.getString("orderBy")
346
+ let ascending = call.getBool("ascending") ?? true
347
+ let single = call.getBool("single") ?? false
348
+
349
+ Task {
350
+ do {
351
+ var filterQuery = client.from(table).select(columns)
352
+
353
+ if let filter = filter {
354
+ for (key, value) in filter {
355
+ if let stringValue = value as? String {
356
+ filterQuery = filterQuery.eq(key, value: stringValue)
357
+ } else if let intValue = value as? Int {
358
+ filterQuery = filterQuery.eq(key, value: intValue)
359
+ } else if let doubleValue = value as? Double {
360
+ filterQuery = filterQuery.eq(key, value: doubleValue)
361
+ } else if let boolValue = value as? Bool {
362
+ filterQuery = filterQuery.eq(key, value: boolValue)
363
+ }
364
+ }
365
+ }
366
+
367
+ var transformQuery = filterQuery.order(orderBy ?? "id", ascending: ascending)
368
+
369
+ if let limit = limit {
370
+ transformQuery = transformQuery.limit(limit)
371
+ }
372
+
373
+ if let offset = offset {
374
+ transformQuery = transformQuery.range(from: offset, to: offset + (limit ?? 1000) - 1)
375
+ }
376
+
377
+ if single {
378
+ let result: [String: AnyJSON] = try await transformQuery.single().execute().value
379
+ let jsonData = try JSONEncoder().encode(result)
380
+ let dict = try JSONSerialization.jsonObject(with: jsonData)
381
+ call.resolve(["data": dict, "error": NSNull()])
382
+ } else {
383
+ let result: [[String: AnyJSON]] = try await transformQuery.execute().value
384
+ let jsonData = try JSONEncoder().encode(result)
385
+ let array = try JSONSerialization.jsonObject(with: jsonData)
386
+ call.resolve(["data": array, "error": NSNull()])
387
+ }
388
+ } catch {
389
+ call.resolve(["data": NSNull(), "error": error.localizedDescription])
390
+ }
391
+ }
392
+ }
393
+
394
+ @objc func insert(_ call: CAPPluginCall) {
395
+ guard let client = supabaseClient else {
396
+ call.reject("Supabase client not initialized. Call initialize() first.")
397
+ return
398
+ }
399
+
400
+ guard let table = call.getString("table") else {
401
+ call.reject("Missing table name")
402
+ return
403
+ }
404
+
405
+ guard let values = call.getObject("values") else {
406
+ call.reject("Missing values to insert")
407
+ return
408
+ }
409
+
410
+ Task {
411
+ do {
412
+ let jsonData = try JSONSerialization.data(withJSONObject: values)
413
+ let decodedValues = try JSONDecoder().decode([String: AnyJSON].self, from: jsonData)
414
+ let result: [String: AnyJSON] = try await client.from(table)
415
+ .insert(decodedValues)
416
+ .select()
417
+ .single()
418
+ .execute()
419
+ .value
420
+ let resultData = try JSONEncoder().encode(result)
421
+ let dict = try JSONSerialization.jsonObject(with: resultData)
422
+ call.resolve(["data": dict, "error": NSNull()])
423
+ } catch {
424
+ call.resolve(["data": NSNull(), "error": error.localizedDescription])
425
+ }
426
+ }
427
+ }
428
+
429
+ @objc func update(_ call: CAPPluginCall) {
430
+ guard let client = supabaseClient else {
431
+ call.reject("Supabase client not initialized. Call initialize() first.")
432
+ return
433
+ }
434
+
435
+ guard let table = call.getString("table") else {
436
+ call.reject("Missing table name")
437
+ return
438
+ }
439
+
440
+ guard let values = call.getObject("values") else {
441
+ call.reject("Missing values to update")
442
+ return
443
+ }
444
+
445
+ guard let filter = call.getObject("filter"), !filter.isEmpty else {
446
+ call.reject("Missing filter for update operation")
447
+ return
448
+ }
449
+
450
+ Task {
451
+ do {
452
+ let jsonData = try JSONSerialization.data(withJSONObject: values)
453
+ let decodedValues = try JSONDecoder().decode([String: AnyJSON].self, from: jsonData)
454
+ var query = try client.from(table).update(decodedValues)
455
+
456
+ for (key, value) in filter {
457
+ if let stringValue = value as? String {
458
+ query = query.eq(key, value: stringValue)
459
+ } else if let intValue = value as? Int {
460
+ query = query.eq(key, value: intValue)
461
+ } else if let doubleValue = value as? Double {
462
+ query = query.eq(key, value: doubleValue)
463
+ } else if let boolValue = value as? Bool {
464
+ query = query.eq(key, value: boolValue)
465
+ }
466
+ }
467
+
468
+ let result: [[String: AnyJSON]] = try await query.select().execute().value
469
+ let resultData = try JSONEncoder().encode(result)
470
+ let array = try JSONSerialization.jsonObject(with: resultData)
471
+ call.resolve(["data": array, "error": NSNull()])
472
+ } catch {
473
+ call.resolve(["data": NSNull(), "error": error.localizedDescription])
474
+ }
475
+ }
476
+ }
477
+
478
+ @objc func delete(_ call: CAPPluginCall) {
479
+ guard let client = supabaseClient else {
480
+ call.reject("Supabase client not initialized. Call initialize() first.")
481
+ return
482
+ }
483
+
484
+ guard let table = call.getString("table") else {
485
+ call.reject("Missing table name")
486
+ return
487
+ }
488
+
489
+ guard let filter = call.getObject("filter"), !filter.isEmpty else {
490
+ call.reject("Missing filter for delete operation")
491
+ return
492
+ }
493
+
494
+ Task {
495
+ do {
496
+ var query = try client.from(table).delete()
497
+
498
+ for (key, value) in filter {
499
+ if let stringValue = value as? String {
500
+ query = query.eq(key, value: stringValue)
501
+ } else if let intValue = value as? Int {
502
+ query = query.eq(key, value: intValue)
503
+ } else if let doubleValue = value as? Double {
504
+ query = query.eq(key, value: doubleValue)
505
+ } else if let boolValue = value as? Bool {
506
+ query = query.eq(key, value: boolValue)
507
+ }
508
+ }
509
+
510
+ let result: [[String: AnyJSON]] = try await query.select().execute().value
511
+ let resultData = try JSONEncoder().encode(result)
512
+ let array = try JSONSerialization.jsonObject(with: resultData)
513
+ call.resolve(["data": array, "error": NSNull()])
514
+ } catch {
515
+ call.resolve(["data": NSNull(), "error": error.localizedDescription])
516
+ }
517
+ }
518
+ }
519
+
520
+ @objc func getPluginVersion(_ call: CAPPluginCall) {
521
+ call.resolve(["version": self.pluginVersion])
522
+ }
523
+
524
+ // MARK: - Helper Methods
525
+
526
+ private func sessionToDict(_ session: Session) -> [String: Any] {
527
+ return [
528
+ "accessToken": session.accessToken,
529
+ "refreshToken": session.refreshToken,
530
+ "tokenType": session.tokenType,
531
+ "expiresIn": session.expiresIn,
532
+ "expiresAt": session.expiresAt,
533
+ "user": userToDict(session.user)
534
+ ]
535
+ }
536
+
537
+ private func userToDict(_ user: User) -> [String: Any] {
538
+ var dict: [String: Any] = [
539
+ "id": user.id.uuidString,
540
+ "createdAt": ISO8601DateFormatter().string(from: user.createdAt)
541
+ ]
542
+
543
+ if let email = user.email {
544
+ dict["email"] = email
545
+ }
546
+
547
+ if let phone = user.phone {
548
+ dict["phone"] = phone
549
+ }
550
+
551
+ if let lastSignInAt = user.lastSignInAt {
552
+ dict["lastSignInAt"] = ISO8601DateFormatter().string(from: lastSignInAt)
553
+ }
554
+
555
+ let userMetadata = user.userMetadata
556
+ if let data = try? JSONEncoder().encode(userMetadata),
557
+ let dict2 = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
558
+ dict["userMetadata"] = dict2
559
+ }
560
+
561
+ let appMetadata = user.appMetadata
562
+ if let data = try? JSONEncoder().encode(appMetadata),
563
+ let dict2 = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
564
+ dict["appMetadata"] = dict2
565
+ }
566
+
567
+ return dict
568
+ }
569
+
570
+ private func authResultToDict(session: Session?, user: User?) -> [String: Any] {
571
+ var result: [String: Any] = [:]
572
+ if let session = session {
573
+ result["session"] = sessionToDict(session)
574
+ } else {
575
+ result["session"] = NSNull()
576
+ }
577
+ if let user = user {
578
+ result["user"] = userToDict(user)
579
+ } else {
580
+ result["user"] = NSNull()
581
+ }
582
+ return result
583
+ }
584
+
585
+ private func oauthProviderFromString(_ provider: String) -> Provider? {
586
+ switch provider.lowercased() {
587
+ case "apple": return .apple
588
+ case "azure": return .azure
589
+ case "bitbucket": return .bitbucket
590
+ case "discord": return .discord
591
+ case "facebook": return .facebook
592
+ case "figma": return .figma
593
+ case "github": return .github
594
+ case "gitlab": return .gitlab
595
+ case "google": return .google
596
+ case "kakao": return .kakao
597
+ case "keycloak": return .keycloak
598
+ case "linkedin": return .linkedin
599
+ case "linkedin_oidc": return .linkedinOIDC
600
+ case "notion": return .notion
601
+ case "slack": return .slack
602
+ case "slack_oidc": return .slackOIDC
603
+ case "spotify": return .spotify
604
+ case "twitch": return .twitch
605
+ case "twitter": return .twitter
606
+ case "workos": return .workos
607
+ case "zoom": return .zoom
608
+ default: return nil
609
+ }
610
+ }
611
+
612
+ private func emailOtpTypeFromString(_ type: String) -> EmailOTPType {
613
+ switch type.lowercased() {
614
+ case "signup": return .signup
615
+ case "magiclink": return .magiclink
616
+ case "recovery": return .recovery
617
+ case "email": return .email
618
+ default: return .email
619
+ }
620
+ }
621
+
622
+ private func phoneOtpTypeFromString(_ type: String) -> MobileOTPType {
623
+ switch type.lowercased() {
624
+ case "sms": return .sms
625
+ default: return .sms
626
+ }
627
+ }
628
+ }
@@ -0,0 +1,9 @@
1
+ import XCTest
2
+ @testable import CapacitorSupabasePlugin
3
+
4
+ final class CapacitorSupabasePluginTests: XCTestCase {
5
+ func testExample() throws {
6
+ // This is an example of a functional test case.
7
+ XCTAssertTrue(true)
8
+ }
9
+ }