diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 173eca3..6b849a1 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 080C254329DF159E0013DEFA /* AuthWithEmailAndOTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080C254229DF159E0013DEFA /* AuthWithEmailAndOTP.swift */; }; + 08BA8D5929E4540F00BD340E /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08BA8D5829E4540F00BD340E /* LoadingView.swift */; }; 1820720E286B449B0009D0BF /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1820720D286B449B0009D0BF /* Secrets.swift */; }; 18DE58AE286B2C78005F7FF2 /* ExamplesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18DE589E286B2C77005F7FF2 /* ExamplesApp.swift */; }; 18DE58B2286B2C78005F7FF2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 18DE58A0286B2C78005F7FF2 /* Assets.xcassets */; }; @@ -17,6 +19,9 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 080C254229DF159E0013DEFA /* AuthWithEmailAndOTP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthWithEmailAndOTP.swift; sourceTree = ""; }; + 089F9ED029DDB5B100197CC2 /* gotrue-swift-community */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "gotrue-swift-community"; path = ..; sourceTree = ""; }; + 08BA8D5829E4540F00BD340E /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; 1820720D286B449B0009D0BF /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = ""; }; 18AD3626286B33AE00855046 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 18AD3627286B35D300855046 /* Examples.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Examples.entitlements; sourceTree = ""; }; @@ -40,6 +45,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 089F9ECF29DDB5B100197CC2 /* Packages */ = { + isa = PBXGroup; + children = ( + 089F9ED029DDB5B100197CC2 /* gotrue-swift-community */, + ); + name = Packages; + sourceTree = ""; + }; 18AD3622286B304300855046 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -50,6 +63,7 @@ 18DE5898286B2C77005F7FF2 = { isa = PBXGroup; children = ( + 089F9ECF29DDB5B100197CC2 /* Packages */, 18DE589D286B2C77005F7FF2 /* Shared */, 18DE58A6286B2C78005F7FF2 /* Products */, 18AD3622286B304300855046 /* Frameworks */, @@ -83,6 +97,8 @@ 18DE589E286B2C77005F7FF2 /* ExamplesApp.swift */, 1820720D286B449B0009D0BF /* Secrets.swift */, 798AD5222907309C001B4801 /* SessionView.swift */, + 080C254229DF159E0013DEFA /* AuthWithEmailAndOTP.swift */, + 08BA8D5829E4540F00BD340E /* LoadingView.swift */, ); path = Sources; sourceTree = ""; @@ -159,10 +175,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 080C254329DF159E0013DEFA /* AuthWithEmailAndOTP.swift in Sources */, 1820720E286B449B0009D0BF /* Secrets.swift in Sources */, 18DE58AE286B2C78005F7FF2 /* ExamplesApp.swift in Sources */, 798AD52129072C22001B4801 /* AppView.swift in Sources */, 798AD525290731A0001B4801 /* AuthWithEmailAndPasswordView.swift in Sources */, + 08BA8D5929E4540F00BD340E /* LoadingView.swift in Sources */, 798AD5232907309C001B4801 /* SessionView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -287,7 +305,7 @@ CODE_SIGN_ENTITLEMENTS = Shared/Examples.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ELTTE7K8TT; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Shared/Info.plist; @@ -321,7 +339,7 @@ CODE_SIGN_ENTITLEMENTS = Shared/Examples.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ELTTE7K8TT; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Shared/Info.plist; diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index edd534b..43b36df 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,61 +1,41 @@ { - "object": { - "pins": [ - { - "package": "Get", - "repositoryURL": "https://github.com/kean/Get", - "state": { - "branch": null, - "revision": "d2ded8526d647a8ca25905025cd5503dfe51e874", - "version": "2.0.0" - } - }, - { - "package": "KeychainAccess", - "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess", - "state": { - "branch": null, - "revision": "84e546727d66f1adc5439debad16270d0fdd04e7", - "version": "4.2.2" - } - }, - { - "package": "Mocker", - "repositoryURL": "https://github.com/WeTransfer/Mocker", - "state": { - "branch": null, - "revision": "e7165fb0378193c6784f1d46a9ab2b6184c384fa", - "version": "2.7.0" - } - }, - { - "package": "ComposableKeychain", - "repositoryURL": "https://github.com/binaryscraping/swift-composable-keychain", - "state": { - "branch": null, - "revision": "3ebddfe9e9232218bada99ab6d6b3ca1fd94f112", - "version": "0.0.2" - } - }, - { - "package": "URLQueryEncoder", - "repositoryURL": "https://github.com/kean/URLQueryEncoder", - "state": { - "branch": null, - "revision": "4cc975d4d075d0e62699a796a08284e217d4ee93", - "version": "0.2.0" - } - }, - { - "package": "xctest-dynamic-overlay", - "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", - "state": { - "branch": null, - "revision": "38bc9242e4388b80bd23ddfdf3071428859e3260", - "version": "0.4.0" - } + "pins" : [ + { + "identity" : "get", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kean/Get", + "state" : { + "revision" : "12830cc64f31789ae6f4352d2d51d03a25fc3741", + "version" : "2.1.6" } - ] - }, - "version": 1 + }, + { + "identity" : "keychainaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kishikawakatsumi/KeychainAccess", + "state" : { + "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", + "version" : "4.2.2" + } + }, + { + "identity" : "mocker", + "kind" : "remoteSourceControl", + "location" : "https://github.com/WeTransfer/Mocker", + "state" : { + "revision" : "4384e015cae4916a6828252467a4437173c7ae17", + "version" : "3.0.1" + } + }, + { + "identity" : "urlqueryencoder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kean/URLQueryEncoder", + "state" : { + "revision" : "4ce950479707ea109f229d7230ec074a133b15d7", + "version" : "0.2.1" + } + } + ], + "version" : 2 } diff --git a/Examples/Package.swift b/Examples/Package.swift index 6637d28..814edf2 100644 --- a/Examples/Package.swift +++ b/Examples/Package.swift @@ -2,10 +2,25 @@ import PackageDescription +// let package = Package( +// name: "Examples", +// platforms: [], +// products: [], +// dependencies: [], +// targets: [] +// ) + +let packageName = "Shared" // <-- Change this to yours let package = Package( - name: "Examples", - platforms: [], - products: [], - dependencies: [], - targets: [] + name: "", + // platforms: [.iOS("9.0")], + products: [ + .library(name: packageName, targets: [packageName]) + ], + targets: [ + .target( + name: packageName, + path: packageName + ) + ] ) diff --git a/Examples/Shared/Info.plist b/Examples/Shared/Info.plist index ca032e9..8f19397 100644 --- a/Examples/Shared/Info.plist +++ b/Examples/Shared/Info.plist @@ -13,7 +13,7 @@ com.supabase.gotrue-swift.Examples CFBundleURLSchemes - {REDIRECT_URL} + supabase:// diff --git a/Examples/Shared/Sources/AppView.swift b/Examples/Shared/Sources/AppView.swift index a0cf5db..2ba14d8 100644 --- a/Examples/Shared/Sources/AppView.swift +++ b/Examples/Shared/Sources/AppView.swift @@ -1,23 +1,79 @@ import GoTrue import SwiftUI +import AuthenticationServices +import Combine -struct AppView: View { - @Environment(\.goTrueClient) private var client - @State private var session: Session? - @State private var clientInitialized = false +class SignInViewModel: NSObject, ObservableObject, ASWebAuthenticationPresentationContextProviding { + + func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + return ASPresentationAnchor() + } + + func signInGoogle(client: GoTrueClient) { + + /* Config callback url from Google Cloud Console with following guide + https://supabase.com/docs/guides/auth/social-login/auth-google#find-your-callback-url + */ + + // Need to config Url Type in Info.plist match with below Scheme + let schemeUrl = "supabase" + + do { + let url = try client.getOAuthSignInURL( + provider: Provider.google + + // Need to config scheme url in Supabase Auth console panel + ,redirectTo: URL(string: "\(schemeUrl)://auth") + ) + let handler: ASWebAuthenticationSession.CompletionHandler = { (url, error) in + if let error = error { + NSLog("Error \(error)") + } else if let url = url { + Task { + try await client.session(from: url) + } + } + } + let session = ASWebAuthenticationSession( + url: url, + callbackURLScheme: schemeUrl, + completionHandler: handler + ) + session.presentationContextProvider = self + session.prefersEphemeralWebBrowserSession = true + session.start() + } catch { + NSLog("Error \(error)") + } + } +} +struct AppView: View { + @Environment(\.goTrueClient) private var client + @State private var session: Session? + @State private var clientInitialized = false + @StateObject var signInModel = SignInViewModel() + var body: some View { if clientInitialized { NavigationView { if let session { SessionView(session: session) } else { - List { - NavigationLink("Auth with Email and Password") { - AuthWithEmailAndPasswordView() + Section { + List { + NavigationLink("Auth with Email and Password") { + AuthWithEmailAndPasswordView() + } + NavigationLink("Auth with Email and OTP") { + AuthWithEmailAndOTP() + } + Button("Google Sign In") { + signInModel.signInGoogle(client: client) + } + } + .listStyle(.plain) } - } - .listStyle(.plain) .navigationTitle("Examples") } } @@ -36,6 +92,7 @@ struct AppView: View { session = try? await client.session } } + } func stringfy(_ value: some Codable) -> String { diff --git a/Examples/Shared/Sources/AuthWithEmailAndOTP.swift b/Examples/Shared/Sources/AuthWithEmailAndOTP.swift new file mode 100644 index 0000000..09483c2 --- /dev/null +++ b/Examples/Shared/Sources/AuthWithEmailAndOTP.swift @@ -0,0 +1,69 @@ +// +// AuthWithPhoneAndOTP.swift +// Examples +// +// Created by Danh Nguyen on 06/04/2023. +// + +import SwiftUI + +struct AuthWithEmailAndOTP: View { + @Environment(\.goTrueClient) private var client + @State private var phoneNumber: String = "" + @State private var email: String = "" + @State private var otpNumber: String = "" + @State private var isLoading: Bool = false + + var body: some View { + LoadingView(isShowing: $isLoading) { + Form { + Section { + TextField("Email address", text: $email) + .keyboardType(.emailAddress) + .textContentType(.emailAddress) + .autocorrectionDisabled() + TextField("OTP", text: $otpNumber) + .keyboardType(.phonePad) + .textContentType(.oneTimeCode) + } + + Section { + Button("Request OTP") { + requestOTP() + } + Button("Confirm OTP") { + confirmOtp() + } + } + } + } + } + + private func requestOTP() { + isLoading = true + Task { + // Remember to config Supabase Email templates -> Magic Link -> {{ .Token }} + // https://supabase.com/docs/guides/auth/auth-email-templates + try await client.signInWithOTP(email: email) + isLoading = false + } + } + + private func confirmOtp() { + isLoading = true + Task { + do { + try await client.verifyOTP(email: email, token: otpNumber, type: .magiclink) + } catch { + NSLog("Error \(error)") + } + isLoading = false + } + } +} + +struct AuthWithPhoneAndOTP_Previews: PreviewProvider { + static var previews: some View { + AuthWithEmailAndOTP() + } +} diff --git a/Examples/Shared/Sources/AuthWithEmailAndPasswordView.swift b/Examples/Shared/Sources/AuthWithEmailAndPasswordView.swift index 1defcb4..0f9be2a 100644 --- a/Examples/Shared/Sources/AuthWithEmailAndPasswordView.swift +++ b/Examples/Shared/Sources/AuthWithEmailAndPasswordView.swift @@ -14,39 +14,43 @@ struct AuthWithEmailAndPasswordView: View { @State private var email = "" @State private var password = "" @State private var error: Error? + @State private var isLoading: Bool = false var body: some View { - Form { - Section { - TextField("Email", text: $email) - .keyboardType(.emailAddress) - .textContentType(.emailAddress) - .autocorrectionDisabled() - .textInputAutocapitalization(.never) - SecureField("Password", text: $password) - .textContentType(.password) + LoadingView(isShowing: $isLoading) { + Form { + Section { + TextField("Email", text: $email) + .keyboardType(.emailAddress) + .textContentType(.emailAddress) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + SecureField("Password", text: $password) + .textContentType(.password) + } + + Section { + Button("Sign in") { + signInButtonTapped() + } + + Button("Sign up") { + signUpButtonTapped() + } + } + + if let error { + Section { + Text(error.localizedDescription) + .foregroundColor(.red) + } + } + } } - - Section { - Button("Sign in") { - signInButtonTapped() - } - - Button("Sign up") { - signUpButtonTapped() - } - } - - if let error { - Section { - Text(error.localizedDescription) - .foregroundColor(.red) - } - } - } } private func signInButtonTapped() { + isLoading = true Task { do { error = nil @@ -54,10 +58,12 @@ struct AuthWithEmailAndPasswordView: View { } catch { self.error = error } + isLoading = false } } private func signUpButtonTapped() { + isLoading = true Task { do { error = nil @@ -65,6 +71,7 @@ struct AuthWithEmailAndPasswordView: View { } catch { self.error = error } + isLoading = false } } } diff --git a/Examples/Shared/Sources/LoadingView.swift b/Examples/Shared/Sources/LoadingView.swift new file mode 100644 index 0000000..9761019 --- /dev/null +++ b/Examples/Shared/Sources/LoadingView.swift @@ -0,0 +1,53 @@ +// +// LoadingView.swift +// Examples +// +// Created by Danh Nguyen on 10/04/2023. +// + +import Foundation +import SwiftUI + +struct ActivityIndicator: UIViewRepresentable { + + @Binding var isAnimating: Bool + let style: UIActivityIndicatorView.Style + + func makeUIView(context: UIViewRepresentableContext) -> UIActivityIndicatorView { + return UIActivityIndicatorView(style: style) + } + + func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext) { + isAnimating ? uiView.startAnimating() : uiView.stopAnimating() + } +} + +struct LoadingView: View where Content: View { + + @Binding var isShowing: Bool + var content: () -> Content + + var body: some View { + GeometryReader { geometry in + ZStack(alignment: .center) { + + self.content() + .disabled(self.isShowing) + .blur(radius: self.isShowing ? 3 : 0) + + VStack { + Text("Loading...") + ActivityIndicator(isAnimating: .constant(true), style: .large) + } + .frame(width: geometry.size.width / 2, + height: geometry.size.height / 5) + .background(Color.secondary.colorInvert()) + .foregroundColor(Color.primary) + .cornerRadius(20) + .opacity(self.isShowing ? 1 : 0) + + } + } + } + +}