Skip to content

Commit

Permalink
feat(auth): link identity (#274)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
grdsdev authored Mar 22, 2024
1 parent 77e5c3d commit b805cdc
Show file tree
Hide file tree
Showing 15 changed files with 497 additions and 59 deletions.
20 changes: 20 additions & 0 deletions Examples/Examples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
793E030B2B2CEDDA00AC7DED /* ActionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793E030A2B2CEDDA00AC7DED /* ActionState.swift */; };
793E030D2B2DAB5700AC7DED /* SignInWithApple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793E030C2B2DAB5700AC7DED /* SignInWithApple.swift */; };
7940E3152B36187A0089BEE1 /* GoogleSignInWithWebFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7940E3142B36187A0089BEE1 /* GoogleSignInWithWebFlow.swift */; };
794C61D62BAD1E12000E6B0F /* UserIdentityList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C61D52BAD1E12000E6B0F /* UserIdentityList.swift */; };
794EF1222955F26A008C9526 /* AddTodoListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794EF1212955F26A008C9526 /* AddTodoListView.swift */; };
794EF1242955F3DE008C9526 /* TodoListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794EF1232955F3DE008C9526 /* TodoListRow.swift */; };
7956405E2954ADE00088A06F /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7956405D2954ADE00088A06F /* Secrets.swift */; };
Expand All @@ -39,6 +40,8 @@
79AF04812B2CE261008761AD /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79AF04802B2CE261008761AD /* AuthView.swift */; };
79AF04842B2CE408008761AD /* AuthWithMagicLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79AF04832B2CE408008761AD /* AuthWithMagicLink.swift */; };
79AF04862B2CE586008761AD /* Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79AF04852B2CE586008761AD /* Debug.swift */; };
79B1C80C2BABFF8000D991AA /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79B1C80B2BABFF8000D991AA /* ProfileView.swift */; };
79B1C80E2BAC017C00D991AA /* AnyJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79B1C80D2BAC017C00D991AA /* AnyJSONView.swift */; };
79B8F4242B5FED7C0000E839 /* IdentifiedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 79B8F4232B5FED7C0000E839 /* IdentifiedCollections */; };
79B8F4262B602F640000E839 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79B8F4252B602F640000E839 /* Logger.swift */; };
79BD76772B59C3E300CA3D68 /* UserStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD76762B59C3E300CA3D68 /* UserStore.swift */; };
Expand Down Expand Up @@ -80,6 +83,7 @@
793E030A2B2CEDDA00AC7DED /* ActionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionState.swift; sourceTree = "<group>"; };
793E030C2B2DAB5700AC7DED /* SignInWithApple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInWithApple.swift; sourceTree = "<group>"; };
7940E3142B36187A0089BEE1 /* GoogleSignInWithWebFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleSignInWithWebFlow.swift; sourceTree = "<group>"; };
794C61D52BAD1E12000E6B0F /* UserIdentityList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIdentityList.swift; sourceTree = "<group>"; };
794EF1212955F26A008C9526 /* AddTodoListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTodoListView.swift; sourceTree = "<group>"; };
794EF1232955F3DE008C9526 /* TodoListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoListRow.swift; sourceTree = "<group>"; };
7956405D2954ADE00088A06F /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = "<group>"; };
Expand All @@ -102,6 +106,8 @@
79AF04802B2CE261008761AD /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = "<group>"; };
79AF04832B2CE408008761AD /* AuthWithMagicLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthWithMagicLink.swift; sourceTree = "<group>"; };
79AF04852B2CE586008761AD /* Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debug.swift; sourceTree = "<group>"; };
79B1C80B2BABFF8000D991AA /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
79B1C80D2BAC017C00D991AA /* AnyJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyJSONView.swift; sourceTree = "<group>"; };
79B8F4252B602F640000E839 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
79BD76762B59C3E300CA3D68 /* UserStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStore.swift; sourceTree = "<group>"; };
79BD76782B59C53900CA3D68 /* ChannelStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelStore.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -190,6 +196,7 @@
793895C82954ABFF0044F2B8 /* Examples */ = {
isa = PBXGroup;
children = (
79B1C80A2BABFF6F00D991AA /* Profile */,
797EFB642BABD7FF00098D6B /* Storage */,
79AF04822B2CE3BD008761AD /* Auth */,
7962989A2AEBBD9F000AA957 /* Info.plist */,
Expand All @@ -211,6 +218,7 @@
793E030A2B2CEDDA00AC7DED /* ActionState.swift */,
79E2B55B2B97A2310042CD21 /* UIApplicationExtensions.swift */,
797EFB672BABD90500098D6B /* Stringfy.swift */,
79B1C80D2BAC017C00D991AA /* AnyJSONView.swift */,
);
path = Examples;
sourceTree = "<group>";
Expand Down Expand Up @@ -254,6 +262,15 @@
path = Auth;
sourceTree = "<group>";
};
79B1C80A2BABFF6F00D991AA /* Profile */ = {
isa = PBXGroup;
children = (
79B1C80B2BABFF8000D991AA /* ProfileView.swift */,
794C61D52BAD1E12000E6B0F /* UserIdentityList.swift */,
);
path = Profile;
sourceTree = "<group>";
};
79D884C82B3C18830009EA4A /* SlackClone */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -466,8 +483,10 @@
796298992AEBBA77000AA957 /* MFAFlow.swift in Sources */,
79AF04862B2CE586008761AD /* Debug.swift in Sources */,
79AF04842B2CE408008761AD /* AuthWithMagicLink.swift in Sources */,
79B1C80E2BAC017C00D991AA /* AnyJSONView.swift in Sources */,
79E2B5552B9788BF0042CD21 /* GoogleSignInSDKFlow.swift in Sources */,
793E03092B2CED5D00AC7DED /* Contants.swift in Sources */,
794C61D62BAD1E12000E6B0F /* UserIdentityList.swift in Sources */,
793895CC2954ABFF0044F2B8 /* RootView.swift in Sources */,
7956406A2955AFBD0088A06F /* ErrorText.swift in Sources */,
79AF04812B2CE261008761AD /* AuthView.swift in Sources */,
Expand All @@ -477,6 +496,7 @@
794EF1222955F26A008C9526 /* AddTodoListView.swift in Sources */,
7956405E2954ADE00088A06F /* Secrets.swift in Sources */,
795640682955AEB30088A06F /* Models.swift in Sources */,
79B1C80C2BABFF8000D991AA /* ProfileView.swift in Sources */,
795640662955AE9C0088A06F /* TodoListView.swift in Sources */,
795640602954AE140088A06F /* AuthController.swift in Sources */,
79AF047F2B2CE207008761AD /* AuthWithEmailAndPassword.swift in Sources */,
Expand Down
47 changes: 47 additions & 0 deletions Examples/Examples/ActionState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,57 @@

import CasePaths
import Foundation
import SwiftUI

@CasePathable
enum ActionState<Success, Failure: Error> {
case idle
case inFlight
case result(Result<Success, Failure>)

var success: Success? {
if case let .result(.success(success)) = self { return success }
return nil
}
}

struct ActionStateView<Success: Sendable, SuccessContent: View>: View {
@Binding var state: ActionState<Success, any Error>

let action: () async throws -> Success
@ViewBuilder var content: (Success) -> SuccessContent

var body: some View {
Group {
switch state {
case .idle:
Color.clear
case .inFlight:
ProgressView()
case let .result(.success(value)):
content(value)
case let .result(.failure(error)):
VStack {
ErrorText(error)
Button("Retry") {
Task { await load() }
}
}
}
}
.task {
await load()
}
}

@MainActor
private func load() async {
state = .inFlight
do {
let value = try await action()
state = .result(.success(value))
} catch {
state = .result(.failure(error))
}
}
}
109 changes: 109 additions & 0 deletions Examples/Examples/AnyJSONView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//
// AnyJSONView.swift
// Examples
//
// Created by Guilherme Souza on 21/03/24.
//

import Supabase
import SwiftUI

struct AnyJSONView: View {
let value: AnyJSON

var body: some View {
switch value {
case .null: Text("<nil>")
case let .bool(value): Text(value.description)
case let .double(value): Text(value.description)
case let .integer(value): Text(value.description)
case let .string(value): Text(value)
case let .array(value):
ForEach(0 ..< value.count, id: \.self) { index in
if value[index].isPrimitive {
LabeledContent("\(index)") {
AnyJSONView(value: value[index])
}
} else {
NavigationLink("\(index)") {
List {
AnyJSONView(value: value[index])
}
.navigationTitle("\(index)")
}
}
}

case let .object(object):
let elements = Array(object).sorted(by: { $0.key < $1.key })
ForEach(elements, id: \.key) { element in
if element.value.isPrimitive {
LabeledContent(element.key) {
AnyJSONView(value: element.value)
}
} else {
NavigationLink(element.key) {
List {
AnyJSONView(value: element.value)
}
.navigationTitle(element.key)
}
}
}
}
}
}

extension AnyJSON {
var isPrimitive: Bool {
switch self {
case .null, .bool, .integer, .double, .string:
return true
case .object, .array:
return false
}
}
}

#Preview {
NavigationStack {
AnyJSONView(
value: [
"app_metadata": [
"provider": "email",
"providers": [
"email",
],
],
"aud": "authenticated",
"confirmed_at": "2024-03-21T03:19:10.147869Z",
"created_at": "2024-03-21T03:19:10.142559Z",
"email": "[email protected]",
"email_confirmed_at": "2024-03-21T03:19:10.147869Z",
"id": "06f83324-e553-4d39-a609-fd30682ee127",
"identities": [
[
"created_at": "2024-03-21T03:19:10.146262Z",
"email": "[email protected]",
"id": "06f83324-e553-4d39-a609-fd30682ee127",
"identity_data": [
"email": "[email protected]",
"email_verified": false,
"phone_verified": false,
"sub": "06f83324-e553-4d39-a609-fd30682ee127",
],
"identity_id": "35aafcdf-f12e-4e3d-8302-63ff587c041c",
"last_sign_in_at": "2024-03-21T03:19:10.146245Z",
"provider": "email",
"updated_at": "2024-03-21T03:19:10.146262Z",
"user_id": "06f83324-e553-4d39-a609-fd30682ee127",
],
],
"last_sign_in_at": "2024-03-21T03:19:10.149557Z",
"phone": "",
"role": "authenticated",
"updated_at": "2024-03-21T05:37:40.596682Z",
]
)
}
}
25 changes: 10 additions & 15 deletions Examples/Examples/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,18 @@ struct HomeView: View {
@State private var mfaStatus: MFAStatus?

var body: some View {
NavigationStack {
BucketList()
.navigationDestination(for: Bucket.self, destination: BucketDetailView.init)
}
.toolbar {
ToolbarItemGroup(placement: .cancellationAction) {
Button("Sign out") {
Task {
try! await supabase.auth.signOut()
}
TabView {
ProfileView()
.tabItem {
Label("Profile", systemImage: "person.circle")
}

Button("Reauthenticate") {
Task {
try! await supabase.auth.reauthenticate()
}
}
NavigationStack {
BucketList()
.navigationDestination(for: Bucket.self, destination: BucketDetailView.init)
}
.tabItem {
Label("Storage", systemImage: "externaldrive")
}
}
.task {
Expand Down
72 changes: 72 additions & 0 deletions Examples/Examples/Profile/ProfileView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// ProfileView.swift
// Examples
//
// Created by Guilherme Souza on 21/03/24.
//

import Supabase
import SwiftUI

struct ProfileView: View {
@State var user: User?

var identities: [UserIdentity] {
user?.identities ?? []
}

var body: some View {
NavigationStack {
List {
if let user = user.flatMap({ try? AnyJSON($0) }) {
Section {
AnyJSONView(value: user)
}
}

NavigationLink("Identities") {
UserIdentityList()
.navigationTitle("Identities")
}

Button("Reauthenticate") {
Task {
try! await supabase.auth.reauthenticate()
}
}

Menu("Unlink identity") {
ForEach(identities) { identity in
Button(identity.provider) {
Task {
do {
try await supabase.auth.unlinkIdentity(identity)
} catch {
debug("Fail to unlink identity: \(error)")
}
}
}
}
}

Button("Sign out", role: .destructive) {
Task {
try! await supabase.auth.signOut()
}
}
}
.navigationTitle("Profile")
}
.task {
do {
user = try await supabase.auth.user()
} catch {
debug("Fail to fetch user: \(error)")
}
}
}
}

#Preview {
ProfileView()
}
Loading

0 comments on commit b805cdc

Please sign in to comment.