From 8f61141002e0aeddffaf0dc1219b9b4172bc64f6 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 1 Oct 2024 09:08:41 -0300 Subject: [PATCH] chore(docs): add password recovery example (#548) --- Examples/Examples.xcodeproj/project.pbxproj | 4 ++ Examples/Examples/Auth/AuthController.swift | 9 +++- .../Auth/AuthWithEmailAndPassword.swift | 13 +++++ Examples/Examples/Contants.swift | 2 +- Examples/Examples/HomeView.swift | 31 +++++++++-- .../Examples/Profile/ResetPasswordView.swift | 52 +++++++++++++++++++ .../Examples/Profile/UpdateProfileView.swift | 9 +++- Examples/supabase/config.toml | 2 +- Sources/Auth/AuthClient.swift | 2 + Sources/Auth/Internal/EventEmitter.swift | 3 ++ Sources/Helpers/HTTP/LoggerInterceptor.swift | 5 +- 11 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 Examples/Examples/Profile/ResetPasswordView.swift diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 95c39f3c..fb095065 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 7928145D2CAB2CE2000B4ADB /* ResetPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7928145C2CAB2CDE000B4ADB /* ResetPasswordView.swift */; }; 793895CA2954ABFF0044F2B8 /* ExamplesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793895C92954ABFF0044F2B8 /* ExamplesApp.swift */; }; 793895CC2954ABFF0044F2B8 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793895CB2954ABFF0044F2B8 /* RootView.swift */; }; 793895CE2954AC000044F2B8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 793895CD2954AC000044F2B8 /* Assets.xcassets */; }; @@ -77,6 +78,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 7928145C2CAB2CDE000B4ADB /* ResetPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetPasswordView.swift; sourceTree = ""; }; 793895C62954ABFF0044F2B8 /* Examples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Examples.app; sourceTree = BUILT_PRODUCTS_DIR; }; 793895C92954ABFF0044F2B8 /* ExamplesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExamplesApp.swift; sourceTree = ""; }; 793895CB2954ABFF0044F2B8 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; @@ -276,6 +278,7 @@ 79B1C80A2BABFF6F00D991AA /* Profile */ = { isa = PBXGroup; children = ( + 7928145C2CAB2CDE000B4ADB /* ResetPasswordView.swift */, 79B1C80B2BABFF8000D991AA /* ProfileView.swift */, 794C61D52BAD1E12000E6B0F /* UserIdentityList.swift */, 79B3261B2BF359A50023661C /* UpdateProfileView.swift */, @@ -510,6 +513,7 @@ 797EFB662BABD82A00098D6B /* BucketList.swift in Sources */, 79E2B55C2B97A2310042CD21 /* UIApplicationExtensions.swift in Sources */, 794EF1222955F26A008C9526 /* AddTodoListView.swift in Sources */, + 7928145D2CAB2CE2000B4ADB /* ResetPasswordView.swift in Sources */, 7956405E2954ADE00088A06F /* Secrets.swift in Sources */, 795640682955AEB30088A06F /* Models.swift in Sources */, 79B1C80C2BABFF8000D991AA /* ProfileView.swift in Sources */, diff --git a/Examples/Examples/Auth/AuthController.swift b/Examples/Examples/Auth/AuthController.swift index a5f66c28..a83d08cc 100644 --- a/Examples/Examples/Auth/AuthController.swift +++ b/Examples/Examples/Auth/AuthController.swift @@ -12,6 +12,7 @@ import SwiftUI @MainActor final class AuthController { var session: Session? + var isPasswordRecoveryFlow: Bool = false var currentUserID: UUID { guard let id = session?.user.id else { @@ -27,9 +28,13 @@ final class AuthController { init() { observeAuthStateChangesTask = Task { for await (event, session) in supabase.auth.authStateChanges { - guard [.initialSession, .signedIn, .signedOut].contains(event) else { return } + if [.initialSession, .signedIn, .signedOut].contains(event) { + self.session = session + } - self.session = session + if event == .passwordRecovery { + self.isPasswordRecoveryFlow = true + } } } } diff --git a/Examples/Examples/Auth/AuthWithEmailAndPassword.swift b/Examples/Examples/Auth/AuthWithEmailAndPassword.swift index 5a0fe82f..f0687906 100644 --- a/Examples/Examples/Auth/AuthWithEmailAndPassword.swift +++ b/Examples/Examples/Auth/AuthWithEmailAndPassword.swift @@ -23,6 +23,8 @@ struct AuthWithEmailAndPassword: View { @State var mode: Mode = .signIn @State var actionState = ActionState.idle + @State var isPresentingResetPassword: Bool = false + var body: some View { Form { Section { @@ -73,6 +75,14 @@ struct AuthWithEmailAndPassword: View { actionState = .idle } } + + if mode == .signIn { + Section { + Button("Forgot password? Reset it.") { + isPresentingResetPassword = true + } + } + } } .onOpenURL { url in Task { @@ -80,6 +90,9 @@ struct AuthWithEmailAndPassword: View { } } .animation(.default, value: mode) + .sheet(isPresented: $isPresentingResetPassword) { + ResetPasswordView() + } } @MainActor diff --git a/Examples/Examples/Contants.swift b/Examples/Examples/Contants.swift index 1891770c..3dcfdfb1 100644 --- a/Examples/Examples/Contants.swift +++ b/Examples/Examples/Contants.swift @@ -8,7 +8,7 @@ import Foundation enum Constants { - static let redirectToURL = URL(scheme: "com.supabase.swift-examples")! + static let redirectToURL = URL(string: "com.supabase.swift-examples://")! } extension URL { diff --git a/Examples/Examples/HomeView.swift b/Examples/Examples/HomeView.swift index 29caa1e5..bbac599f 100644 --- a/Examples/Examples/HomeView.swift +++ b/Examples/Examples/HomeView.swift @@ -14,6 +14,8 @@ struct HomeView: View { @State private var mfaStatus: MFAStatus? var body: some View { + @Bindable var auth = auth + TabView { ProfileView() .tabItem { @@ -28,11 +30,8 @@ struct HomeView: View { Label("Storage", systemImage: "externaldrive") } } - .task { -// mfaStatus = await verifyMFAStatus() - } - .sheet(unwrapping: $mfaStatus) { $mfaStatus in - MFAFlow(status: mfaStatus) + .sheet(isPresented: $auth.isPasswordRecoveryFlow) { + UpdatePasswordView() } } @@ -55,6 +54,28 @@ struct HomeView: View { return nil } } + + struct UpdatePasswordView: View { + @Environment(\.dismiss) var dismiss + + @State var password: String = "" + + var body: some View { + Form { + SecureField("Password", text: $password) + .textContentType(.newPassword) + + Button("Update password") { + Task { + do { + try await supabase.auth.update(user: UserAttributes(password: password)) + dismiss() + } catch {} + } + } + } + } + } } struct HomeView_Previews: PreviewProvider { diff --git a/Examples/Examples/Profile/ResetPasswordView.swift b/Examples/Examples/Profile/ResetPasswordView.swift new file mode 100644 index 00000000..ef02e593 --- /dev/null +++ b/Examples/Examples/Profile/ResetPasswordView.swift @@ -0,0 +1,52 @@ +// +// ResetPasswordView.swift +// Examples +// +// Created by Guilherme Souza on 30/09/24. +// + +import SwiftUI +import SwiftUINavigation + +struct ResetPasswordView: View { + @State private var email: String = "" + @State private var showAlert = false + @State private var alertMessage = "" + + var body: some View { + VStack(spacing: 20) { + Text("Reset Password") + .font(.largeTitle) + .fontWeight(.bold) + + TextField("Enter your email", text: $email) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocapitalization(.none) + .keyboardType(.emailAddress) + + Button(action: resetPassword) { + Text("Send Reset Link") + .foregroundColor(.white) + .padding() + .background(Color.blue) + .cornerRadius(10) + } + } + .padding() + .alert("Password reset", isPresented: $showAlert, actions: {}, message: { + Text(alertMessage) + }) + } + + func resetPassword() { + Task { + do { + try await supabase.auth.resetPasswordForEmail(email) + alertMessage = "Password reset email sent successfully" + } catch { + alertMessage = "Error sending password reset email: \(error.localizedDescription)" + } + showAlert = true + } + } +} diff --git a/Examples/Examples/Profile/UpdateProfileView.swift b/Examples/Examples/Profile/UpdateProfileView.swift index 49ecf14f..11e5e408 100644 --- a/Examples/Examples/Profile/UpdateProfileView.swift +++ b/Examples/Examples/Profile/UpdateProfileView.swift @@ -13,12 +13,13 @@ struct UpdateProfileView: View { @State var email = "" @State var phone = "" + @State var password = "" @State var otp = "" @State var showTokenField = false var formUpdated: Bool { - emailChanged || phoneChanged + emailChanged || phoneChanged || !password.isEmpty } var emailChanged: Bool { @@ -42,6 +43,8 @@ struct UpdateProfileView: View { .keyboardType(.phonePad) .autocorrectionDisabled() .textInputAutocapitalization(.never) + SecureField("New password", text: $password) + .textContentType(.newPassword) } Section { @@ -81,6 +84,10 @@ struct UpdateProfileView: View { attributes.phone = phone } + if password.isEmpty == false { + attributes.password = password + } + do { try await supabase.auth.update(user: attributes, redirectTo: Constants.redirectToURL) diff --git a/Examples/supabase/config.toml b/Examples/supabase/config.toml index 1d72d505..b5b6313e 100644 --- a/Examples/supabase/config.toml +++ b/Examples/supabase/config.toml @@ -85,7 +85,7 @@ enabled = true account_sid = "account sid" message_service_sid = "account service sid" # DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: -auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" +auth_token = "auth token" # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, # `discord`, `facebook`, `github`, `gitlab`, `google`, `twitch`, `twitter`, `slack`, `spotify`. diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index d775eb55..71299056 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -670,6 +670,8 @@ public final class AuthClient: Sendable { /// Gets the session data from a OAuth2 callback URL. @discardableResult public func session(from url: URL) async throws -> Session { + logger?.debug("received \(url)") + let params = extractParams(from: url) if configuration.flowType == .implicit, !isImplicitGrantFlow(params: params) { diff --git a/Sources/Auth/Internal/EventEmitter.swift b/Sources/Auth/Internal/EventEmitter.swift index 0c2aedf2..53b66b76 100644 --- a/Sources/Auth/Internal/EventEmitter.swift +++ b/Sources/Auth/Internal/EventEmitter.swift @@ -4,11 +4,14 @@ import Helpers struct AuthStateChangeEventEmitter { var emitter = EventEmitter<(AuthChangeEvent, Session?)?>(initialEvent: nil, emitsLastEventWhenAttaching: false) + var logger: (any SupabaseLogger)? func attach(_ listener: @escaping AuthStateChangeListener) -> ObservationToken { emitter.attach { event in guard let event else { return } listener(event.0, event.1) + + logger?.verbose("Auth state changed: \(event)") } } diff --git a/Sources/Helpers/HTTP/LoggerInterceptor.swift b/Sources/Helpers/HTTP/LoggerInterceptor.swift index 05e196f4..e5881953 100644 --- a/Sources/Helpers/HTTP/LoggerInterceptor.swift +++ b/Sources/Helpers/HTTP/LoggerInterceptor.swift @@ -20,10 +20,11 @@ package struct LoggerInterceptor: HTTPClientInterceptor { ) async throws -> HTTPResponse { let id = UUID().uuidString return try await SupabaseLoggerTaskLocal.$additionalContext.withValue(merging: ["requestID": .string(id)]) { + let urlRequest = request.urlRequest + logger.verbose( """ - Request: \(request.method.rawValue) \(request.url.absoluteString - .removingPercentEncoding ?? "") + Request: \(urlRequest.httpMethod ?? "") \(urlRequest.url?.absoluteString.removingPercentEncoding ?? "") Body: \(stringfy(request.body)) """ )