Skip to content

Commit b805cdc

Browse files
authored
feat(auth): link identity (#274)
* feat: add userIdentities method * Update dependencies * wip * fix corrupted Package.resolved * wip * Fix typo * Add example for unlink identity * test: add snapshot test to unlinkIdentity method * Fix unlink identity and add example * fix examples build * Hide add button when unsupported
1 parent 77e5c3d commit b805cdc

File tree

15 files changed

+497
-59
lines changed

15 files changed

+497
-59
lines changed

Examples/Examples.xcodeproj/project.pbxproj

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
793E030B2B2CEDDA00AC7DED /* ActionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793E030A2B2CEDDA00AC7DED /* ActionState.swift */; };
1616
793E030D2B2DAB5700AC7DED /* SignInWithApple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793E030C2B2DAB5700AC7DED /* SignInWithApple.swift */; };
1717
7940E3152B36187A0089BEE1 /* GoogleSignInWithWebFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7940E3142B36187A0089BEE1 /* GoogleSignInWithWebFlow.swift */; };
18+
794C61D62BAD1E12000E6B0F /* UserIdentityList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C61D52BAD1E12000E6B0F /* UserIdentityList.swift */; };
1819
794EF1222955F26A008C9526 /* AddTodoListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794EF1212955F26A008C9526 /* AddTodoListView.swift */; };
1920
794EF1242955F3DE008C9526 /* TodoListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794EF1232955F3DE008C9526 /* TodoListRow.swift */; };
2021
7956405E2954ADE00088A06F /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7956405D2954ADE00088A06F /* Secrets.swift */; };
@@ -39,6 +40,8 @@
3940
79AF04812B2CE261008761AD /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79AF04802B2CE261008761AD /* AuthView.swift */; };
4041
79AF04842B2CE408008761AD /* AuthWithMagicLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79AF04832B2CE408008761AD /* AuthWithMagicLink.swift */; };
4142
79AF04862B2CE586008761AD /* Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79AF04852B2CE586008761AD /* Debug.swift */; };
43+
79B1C80C2BABFF8000D991AA /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79B1C80B2BABFF8000D991AA /* ProfileView.swift */; };
44+
79B1C80E2BAC017C00D991AA /* AnyJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79B1C80D2BAC017C00D991AA /* AnyJSONView.swift */; };
4245
79B8F4242B5FED7C0000E839 /* IdentifiedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 79B8F4232B5FED7C0000E839 /* IdentifiedCollections */; };
4346
79B8F4262B602F640000E839 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79B8F4252B602F640000E839 /* Logger.swift */; };
4447
79BD76772B59C3E300CA3D68 /* UserStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD76762B59C3E300CA3D68 /* UserStore.swift */; };
@@ -80,6 +83,7 @@
8083
793E030A2B2CEDDA00AC7DED /* ActionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionState.swift; sourceTree = "<group>"; };
8184
793E030C2B2DAB5700AC7DED /* SignInWithApple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInWithApple.swift; sourceTree = "<group>"; };
8285
7940E3142B36187A0089BEE1 /* GoogleSignInWithWebFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleSignInWithWebFlow.swift; sourceTree = "<group>"; };
86+
794C61D52BAD1E12000E6B0F /* UserIdentityList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIdentityList.swift; sourceTree = "<group>"; };
8387
794EF1212955F26A008C9526 /* AddTodoListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTodoListView.swift; sourceTree = "<group>"; };
8488
794EF1232955F3DE008C9526 /* TodoListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoListRow.swift; sourceTree = "<group>"; };
8589
7956405D2954ADE00088A06F /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = "<group>"; };
@@ -102,6 +106,8 @@
102106
79AF04802B2CE261008761AD /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = "<group>"; };
103107
79AF04832B2CE408008761AD /* AuthWithMagicLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthWithMagicLink.swift; sourceTree = "<group>"; };
104108
79AF04852B2CE586008761AD /* Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debug.swift; sourceTree = "<group>"; };
109+
79B1C80B2BABFF8000D991AA /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
110+
79B1C80D2BAC017C00D991AA /* AnyJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyJSONView.swift; sourceTree = "<group>"; };
105111
79B8F4252B602F640000E839 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
106112
79BD76762B59C3E300CA3D68 /* UserStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStore.swift; sourceTree = "<group>"; };
107113
79BD76782B59C53900CA3D68 /* ChannelStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelStore.swift; sourceTree = "<group>"; };
@@ -190,6 +196,7 @@
190196
793895C82954ABFF0044F2B8 /* Examples */ = {
191197
isa = PBXGroup;
192198
children = (
199+
79B1C80A2BABFF6F00D991AA /* Profile */,
193200
797EFB642BABD7FF00098D6B /* Storage */,
194201
79AF04822B2CE3BD008761AD /* Auth */,
195202
7962989A2AEBBD9F000AA957 /* Info.plist */,
@@ -211,6 +218,7 @@
211218
793E030A2B2CEDDA00AC7DED /* ActionState.swift */,
212219
79E2B55B2B97A2310042CD21 /* UIApplicationExtensions.swift */,
213220
797EFB672BABD90500098D6B /* Stringfy.swift */,
221+
79B1C80D2BAC017C00D991AA /* AnyJSONView.swift */,
214222
);
215223
path = Examples;
216224
sourceTree = "<group>";
@@ -254,6 +262,15 @@
254262
path = Auth;
255263
sourceTree = "<group>";
256264
};
265+
79B1C80A2BABFF6F00D991AA /* Profile */ = {
266+
isa = PBXGroup;
267+
children = (
268+
79B1C80B2BABFF8000D991AA /* ProfileView.swift */,
269+
794C61D52BAD1E12000E6B0F /* UserIdentityList.swift */,
270+
);
271+
path = Profile;
272+
sourceTree = "<group>";
273+
};
257274
79D884C82B3C18830009EA4A /* SlackClone */ = {
258275
isa = PBXGroup;
259276
children = (
@@ -466,8 +483,10 @@
466483
796298992AEBBA77000AA957 /* MFAFlow.swift in Sources */,
467484
79AF04862B2CE586008761AD /* Debug.swift in Sources */,
468485
79AF04842B2CE408008761AD /* AuthWithMagicLink.swift in Sources */,
486+
79B1C80E2BAC017C00D991AA /* AnyJSONView.swift in Sources */,
469487
79E2B5552B9788BF0042CD21 /* GoogleSignInSDKFlow.swift in Sources */,
470488
793E03092B2CED5D00AC7DED /* Contants.swift in Sources */,
489+
794C61D62BAD1E12000E6B0F /* UserIdentityList.swift in Sources */,
471490
793895CC2954ABFF0044F2B8 /* RootView.swift in Sources */,
472491
7956406A2955AFBD0088A06F /* ErrorText.swift in Sources */,
473492
79AF04812B2CE261008761AD /* AuthView.swift in Sources */,
@@ -477,6 +496,7 @@
477496
794EF1222955F26A008C9526 /* AddTodoListView.swift in Sources */,
478497
7956405E2954ADE00088A06F /* Secrets.swift in Sources */,
479498
795640682955AEB30088A06F /* Models.swift in Sources */,
499+
79B1C80C2BABFF8000D991AA /* ProfileView.swift in Sources */,
480500
795640662955AE9C0088A06F /* TodoListView.swift in Sources */,
481501
795640602954AE140088A06F /* AuthController.swift in Sources */,
482502
79AF047F2B2CE207008761AD /* AuthWithEmailAndPassword.swift in Sources */,

Examples/Examples/ActionState.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,57 @@
77

88
import CasePaths
99
import Foundation
10+
import SwiftUI
1011

1112
@CasePathable
1213
enum ActionState<Success, Failure: Error> {
1314
case idle
1415
case inFlight
1516
case result(Result<Success, Failure>)
17+
18+
var success: Success? {
19+
if case let .result(.success(success)) = self { return success }
20+
return nil
21+
}
22+
}
23+
24+
struct ActionStateView<Success: Sendable, SuccessContent: View>: View {
25+
@Binding var state: ActionState<Success, any Error>
26+
27+
let action: () async throws -> Success
28+
@ViewBuilder var content: (Success) -> SuccessContent
29+
30+
var body: some View {
31+
Group {
32+
switch state {
33+
case .idle:
34+
Color.clear
35+
case .inFlight:
36+
ProgressView()
37+
case let .result(.success(value)):
38+
content(value)
39+
case let .result(.failure(error)):
40+
VStack {
41+
ErrorText(error)
42+
Button("Retry") {
43+
Task { await load() }
44+
}
45+
}
46+
}
47+
}
48+
.task {
49+
await load()
50+
}
51+
}
52+
53+
@MainActor
54+
private func load() async {
55+
state = .inFlight
56+
do {
57+
let value = try await action()
58+
state = .result(.success(value))
59+
} catch {
60+
state = .result(.failure(error))
61+
}
62+
}
1663
}

Examples/Examples/AnyJSONView.swift

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//
2+
// AnyJSONView.swift
3+
// Examples
4+
//
5+
// Created by Guilherme Souza on 21/03/24.
6+
//
7+
8+
import Supabase
9+
import SwiftUI
10+
11+
struct AnyJSONView: View {
12+
let value: AnyJSON
13+
14+
var body: some View {
15+
switch value {
16+
case .null: Text("<nil>")
17+
case let .bool(value): Text(value.description)
18+
case let .double(value): Text(value.description)
19+
case let .integer(value): Text(value.description)
20+
case let .string(value): Text(value)
21+
case let .array(value):
22+
ForEach(0 ..< value.count, id: \.self) { index in
23+
if value[index].isPrimitive {
24+
LabeledContent("\(index)") {
25+
AnyJSONView(value: value[index])
26+
}
27+
} else {
28+
NavigationLink("\(index)") {
29+
List {
30+
AnyJSONView(value: value[index])
31+
}
32+
.navigationTitle("\(index)")
33+
}
34+
}
35+
}
36+
37+
case let .object(object):
38+
let elements = Array(object).sorted(by: { $0.key < $1.key })
39+
ForEach(elements, id: \.key) { element in
40+
if element.value.isPrimitive {
41+
LabeledContent(element.key) {
42+
AnyJSONView(value: element.value)
43+
}
44+
} else {
45+
NavigationLink(element.key) {
46+
List {
47+
AnyJSONView(value: element.value)
48+
}
49+
.navigationTitle(element.key)
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}
56+
57+
extension AnyJSON {
58+
var isPrimitive: Bool {
59+
switch self {
60+
case .null, .bool, .integer, .double, .string:
61+
return true
62+
case .object, .array:
63+
return false
64+
}
65+
}
66+
}
67+
68+
#Preview {
69+
NavigationStack {
70+
AnyJSONView(
71+
value: [
72+
"app_metadata": [
73+
"provider": "email",
74+
"providers": [
75+
"email",
76+
],
77+
],
78+
"aud": "authenticated",
79+
"confirmed_at": "2024-03-21T03:19:10.147869Z",
80+
"created_at": "2024-03-21T03:19:10.142559Z",
81+
"email": "[email protected]",
82+
"email_confirmed_at": "2024-03-21T03:19:10.147869Z",
83+
"id": "06f83324-e553-4d39-a609-fd30682ee127",
84+
"identities": [
85+
[
86+
"created_at": "2024-03-21T03:19:10.146262Z",
87+
"email": "[email protected]",
88+
"id": "06f83324-e553-4d39-a609-fd30682ee127",
89+
"identity_data": [
90+
"email": "[email protected]",
91+
"email_verified": false,
92+
"phone_verified": false,
93+
"sub": "06f83324-e553-4d39-a609-fd30682ee127",
94+
],
95+
"identity_id": "35aafcdf-f12e-4e3d-8302-63ff587c041c",
96+
"last_sign_in_at": "2024-03-21T03:19:10.146245Z",
97+
"provider": "email",
98+
"updated_at": "2024-03-21T03:19:10.146262Z",
99+
"user_id": "06f83324-e553-4d39-a609-fd30682ee127",
100+
],
101+
],
102+
"last_sign_in_at": "2024-03-21T03:19:10.149557Z",
103+
"phone": "",
104+
"role": "authenticated",
105+
"updated_at": "2024-03-21T05:37:40.596682Z",
106+
]
107+
)
108+
}
109+
}

Examples/Examples/HomeView.swift

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,18 @@ struct HomeView: View {
1414
@State private var mfaStatus: MFAStatus?
1515

1616
var body: some View {
17-
NavigationStack {
18-
BucketList()
19-
.navigationDestination(for: Bucket.self, destination: BucketDetailView.init)
20-
}
21-
.toolbar {
22-
ToolbarItemGroup(placement: .cancellationAction) {
23-
Button("Sign out") {
24-
Task {
25-
try! await supabase.auth.signOut()
26-
}
17+
TabView {
18+
ProfileView()
19+
.tabItem {
20+
Label("Profile", systemImage: "person.circle")
2721
}
2822

29-
Button("Reauthenticate") {
30-
Task {
31-
try! await supabase.auth.reauthenticate()
32-
}
33-
}
23+
NavigationStack {
24+
BucketList()
25+
.navigationDestination(for: Bucket.self, destination: BucketDetailView.init)
26+
}
27+
.tabItem {
28+
Label("Storage", systemImage: "externaldrive")
3429
}
3530
}
3631
.task {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//
2+
// ProfileView.swift
3+
// Examples
4+
//
5+
// Created by Guilherme Souza on 21/03/24.
6+
//
7+
8+
import Supabase
9+
import SwiftUI
10+
11+
struct ProfileView: View {
12+
@State var user: User?
13+
14+
var identities: [UserIdentity] {
15+
user?.identities ?? []
16+
}
17+
18+
var body: some View {
19+
NavigationStack {
20+
List {
21+
if let user = user.flatMap({ try? AnyJSON($0) }) {
22+
Section {
23+
AnyJSONView(value: user)
24+
}
25+
}
26+
27+
NavigationLink("Identities") {
28+
UserIdentityList()
29+
.navigationTitle("Identities")
30+
}
31+
32+
Button("Reauthenticate") {
33+
Task {
34+
try! await supabase.auth.reauthenticate()
35+
}
36+
}
37+
38+
Menu("Unlink identity") {
39+
ForEach(identities) { identity in
40+
Button(identity.provider) {
41+
Task {
42+
do {
43+
try await supabase.auth.unlinkIdentity(identity)
44+
} catch {
45+
debug("Fail to unlink identity: \(error)")
46+
}
47+
}
48+
}
49+
}
50+
}
51+
52+
Button("Sign out", role: .destructive) {
53+
Task {
54+
try! await supabase.auth.signOut()
55+
}
56+
}
57+
}
58+
.navigationTitle("Profile")
59+
}
60+
.task {
61+
do {
62+
user = try await supabase.auth.user()
63+
} catch {
64+
debug("Fail to fetch user: \(error)")
65+
}
66+
}
67+
}
68+
}
69+
70+
#Preview {
71+
ProfileView()
72+
}

0 commit comments

Comments
 (0)