Skip to content

feat(auth): link identity #274

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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