Skip to content
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

feat: init SwiftUI library for FirebaseAuthSwiftUI #1237

Draft
wants to merge 160 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
160 commits
Select commit Hold shift + click to select a range
f173131
feat: init SwiftUI library for FirebaseAuthSwiftUI
russellwheatley Feb 18, 2025
51eeeef
chore: add SwiftUI example app
russellwheatley Feb 18, 2025
ce32551
chore: move library spec to root Package.swift
russellwheatley Feb 18, 2025
d0e0b13
chore: swift example app uses package locally
russellwheatley Feb 18, 2025
bb8866b
chore: init auth provider as base class
russellwheatley Feb 18, 2025
f5bef03
chore: stub providerId
russellwheatley Feb 18, 2025
37a1bd3
chore: update types
russellwheatley Feb 18, 2025
59e4741
feat: initial email provider
russellwheatley Feb 18, 2025
ecc7866
chore: started work on sign in scene
russellwheatley Feb 28, 2025
3dd1117
chore: ignore build files
russellwheatley Mar 4, 2025
3f1d3b3
refactor: update implementation to more idiomatic Swift code
russellwheatley Mar 4, 2025
f0a3811
chore: code comment
russellwheatley Mar 4, 2025
17f3bd3
initial view for email auth button
russellwheatley Mar 5, 2025
e658b54
swift example app for deving
russellwheatley Mar 5, 2025
7f02ddb
setup formatting for swiftui code
russellwheatley Mar 5, 2025
3128742
chore: initial format of all swiftui code
russellwheatley Mar 5, 2025
daa2ca3
delete initial scene not needed
russellwheatley Mar 5, 2025
1bf121b
xcode and pod config
russellwheatley Mar 5, 2025
ab18e4f
update protocol for auth provider
russellwheatley Mar 5, 2025
1d2b996
chore: update auth provider protocol
russellwheatley Mar 5, 2025
812b090
create email auth provider
russellwheatley Mar 5, 2025
20b01d5
delete unused code from example
russellwheatley Mar 5, 2025
c3ae034
format
russellwheatley Mar 5, 2025
e478b79
todo
russellwheatley Mar 6, 2025
b257f40
clean up db setup in example app
russellwheatley Mar 6, 2025
febdafc
refactor: allow users to customise content of button
russellwheatley Mar 6, 2025
2db25df
format
russellwheatley Mar 6, 2025
413578e
todo notes
russellwheatley Mar 6, 2025
fdd798c
refactor: use base class as argument
russellwheatley Mar 6, 2025
84da3e4
rename to EmailEntryView
russellwheatley Mar 6, 2025
827023b
make title customisable
russellwheatley Mar 6, 2025
76ca053
allow AuthViewPicker to modify VSStack
russellwheatley Mar 6, 2025
da4ea8f
make FUIAuth observable, update naming and throw assertion instead of…
russellwheatley Mar 6, 2025
59621e9
todo
russellwheatley Mar 6, 2025
06aa0f9
correct provider name
russellwheatley Mar 6, 2025
63a7fbb
create EmailAuthProvider
russellwheatley Mar 6, 2025
fe33ff8
warning view
russellwheatley Mar 6, 2025
27ba4d7
configureable warning view
russellwheatley Mar 6, 2025
0afd033
format
russellwheatley Mar 6, 2025
335dad7
todo
russellwheatley Mar 6, 2025
0ea278d
rm Error type
russellwheatley Mar 6, 2025
664f938
refactor: move src code into separate files
russellwheatley Mar 6, 2025
e798f99
import auth swift ui
russellwheatley Mar 7, 2025
98107e5
format
russellwheatley Mar 7, 2025
2f31c89
a configurable class is more appropriate for styling UI
russellwheatley Mar 7, 2025
7e51026
message should be configurable
russellwheatley Mar 7, 2025
2d17a9e
refactor auth picker view
russellwheatley Mar 7, 2025
224a001
refactor: use base class to configure UI
russellwheatley Mar 7, 2025
7246cd2
email auth button to be configurable
russellwheatley Mar 7, 2025
b731611
rm code moved to email package
russellwheatley Mar 7, 2025
b042566
format
russellwheatley Mar 7, 2025
e0f10d3
format
russellwheatley Mar 7, 2025
ae3bcf8
EmailAuth class
russellwheatley Mar 7, 2025
33c7832
FUIAuthFlowBase trial
russellwheatley Mar 7, 2025
114f9b3
rm button
russellwheatley Mar 7, 2025
c070be6
refactor config file
russellwheatley Mar 7, 2025
fd0aaf7
chore: format
russellwheatley Mar 7, 2025
a4ddba4
refactor email button config
russellwheatley Mar 7, 2025
9411aee
delete auth flow base
russellwheatley Mar 7, 2025
4e55802
refactor warning config
russellwheatley Mar 7, 2025
edd8319
scrub note
russellwheatley Mar 7, 2025
6917a8b
rm todo
russellwheatley Mar 7, 2025
afab989
rm unused file
russellwheatley Mar 7, 2025
7c96c31
refactor: way we render auth views
russellwheatley Mar 7, 2025
43ceb9b
import email auth ui into example
russellwheatley Mar 10, 2025
14f810a
refactor:applying styles via view modifiers
russellwheatley Mar 10, 2025
9d30841
import email auth swift
russellwheatley Mar 10, 2025
fa05d02
fix(email): imports, move util to email auth and public init
russellwheatley Mar 10, 2025
b947d7b
refactor: warning to use modifiers
russellwheatley Mar 10, 2025
2324f52
refactor: email auth button styling
russellwheatley Mar 10, 2025
223c07e
update email auth button
russellwheatley Mar 10, 2025
df83e9a
optional button modifier
russellwheatley Mar 10, 2025
fe64e28
refactor: EmailAuthButton modifiers
russellwheatley Mar 10, 2025
fb077ed
refactor: invoking auth picker
russellwheatley Mar 10, 2025
59a0dae
update example app
russellwheatley Mar 10, 2025
b927f9c
example of using callback to set view modifier
russellwheatley Mar 10, 2025
8117414
fix: stop breaking view identity
russellwheatley Mar 11, 2025
3d6b7b1
refactor: vStackStyle using ViewBuilder
russellwheatley Mar 11, 2025
04289a3
refactor: remove VStack from button, add button styling
russellwheatley Mar 11, 2025
ad0372c
feat: allow custom styling of Text within button
russellwheatley Mar 11, 2025
d71f78d
add nav back to email auth button
russellwheatley Mar 11, 2025
1d6d390
rm VStack modification until decide on how best to approach custom mo…
russellwheatley Mar 11, 2025
fa05691
refactor: use environment object to manage internal state
russellwheatley Mar 11, 2025
d612c70
chore: ensure email auth has access to global state
russellwheatley Mar 11, 2025
ac83bfb
emailentry - render View based on email validation
russellwheatley Mar 11, 2025
90edccd
inject env vars for fuiauth and global state
russellwheatley Mar 11, 2025
4f770c6
chore: example
russellwheatley Mar 11, 2025
d337532
allow default provider flows
russellwheatley Mar 11, 2025
3eb4436
refactor: move closer to preliminary design
russellwheatley Mar 21, 2025
526e760
chore: rm email package, moving to core
russellwheatley Mar 21, 2025
bbf9f7a
chore: rm Email auth package
russellwheatley Mar 21, 2025
9a73fee
fix: email utils
russellwheatley Mar 21, 2025
4596f3b
chore: tidy up auth screen
russellwheatley Mar 21, 2025
3ef9459
chore: fix up example app
russellwheatley Mar 21, 2025
20b9d47
chore: move auth flow to authEnvironment + init email provider
russellwheatley Mar 21, 2025
f80d6dd
EmailPasswordView
russellwheatley Mar 21, 2025
d85fda4
chore: pass observable to provider
russellwheatley Mar 24, 2025
b15292c
update email provider to pass authEnv and to use auth instance
russellwheatley Mar 24, 2025
393c238
update auth env to use auth instance
russellwheatley Mar 24, 2025
8278f04
chore: auth listener manager to enable removal of listener
russellwheatley Mar 24, 2025
09c8c00
chore: update email auth logic
russellwheatley Mar 24, 2025
7d7a039
update email provider to create user
russellwheatley Mar 24, 2025
712f75f
chore: create AuthConfiguration and adjust
russellwheatley Mar 25, 2025
6af2805
refactor: change provider name, and add 2 views
russellwheatley Mar 25, 2025
6bf0308
initial password recovery View
russellwheatley Mar 25, 2025
392fc14
chore: add password recovery func
russellwheatley Mar 25, 2025
c69360f
rm AuthenticationScreen
russellwheatley Mar 25, 2025
28fb71a
PasswordRecoveryView implementation
russellwheatley Mar 25, 2025
82a82b0
error message should be on local state
russellwheatley Mar 25, 2025
427d9c5
add modal after password recovery email sent
russellwheatley Mar 25, 2025
d9658c0
chore: typo in name
russellwheatley Mar 26, 2025
09b0ae0
chore: rm dismiss as not a modal
russellwheatley Mar 26, 2025
489b170
implement SignedInView
russellwheatley Mar 26, 2025
a05cf86
implement AuthPickerView logic for sign-up & login
russellwheatley Mar 26, 2025
fadaafa
change state based on login/sign up flow state
russellwheatley Mar 26, 2025
8a1e8ba
chore: remove unneeded namespace
russellwheatley Mar 26, 2025
daba588
chore: rm unneeded label
russellwheatley Mar 26, 2025
3ee359f
implement Email Sign-in View + some minor modifications
russellwheatley Mar 26, 2025
5aca980
chore: slight UI tweak
russellwheatley Mar 26, 2025
d822050
VerifyEmailView
russellwheatley Mar 26, 2025
77f151b
sign-in link correct API and rm progress loaders for now on api were …
russellwheatley Mar 27, 2025
168ee9d
chore: lower min deploy target on example app
russellwheatley Mar 27, 2025
5ee6ba5
chore: rename AuthEnvironment to AuthService
russellwheatley Mar 28, 2025
4921864
refactor: rm EmailAuthProvider
russellwheatley Mar 28, 2025
96677fa
refactor: make email auth inline with AuthPicker. rm email button
russellwheatley Mar 28, 2025
69429d4
feat: localized strings init
russellwheatley Mar 31, 2025
e0de2a1
refactor: rm unneeded additional directory
russellwheatley Mar 31, 2025
144784d
move localizable strings to correct directory
russellwheatley Mar 31, 2025
68b2c00
chore: rm Email auth as separate package
russellwheatley Mar 31, 2025
a08ec8c
chore: rm gitignore from email auth
russellwheatley Mar 31, 2025
2bfc8fb
chore: string helper functions
russellwheatley Mar 31, 2025
0de4d0e
chore: update localized strings for errors
russellwheatley Mar 31, 2025
58c1be6
update error message with localised strings
russellwheatley Apr 1, 2025
fe59877
rename to StringUtils
russellwheatley Apr 1, 2025
80a127c
chore: update swiftformatter
russellwheatley Apr 1, 2025
2a41c31
feat: init of google auth provider
russellwheatley Apr 1, 2025
69ea14d
setup provider protocol
russellwheatley Apr 1, 2025
c59aa5e
make config properties final
russellwheatley Apr 1, 2025
c37f1d4
initi google provider in auth service
russellwheatley Apr 1, 2025
1a9ba5b
format
russellwheatley Apr 1, 2025
aa8268a
google provider - init tweak
russellwheatley Apr 1, 2025
e43248f
google provider - add google signin as dep
russellwheatley Apr 1, 2025
008684d
google provider - allow no provider to be passed
russellwheatley Apr 1, 2025
7b251fe
add client id to example app
russellwheatley Apr 1, 2025
4576e40
add app delegate url handling
russellwheatley Apr 1, 2025
1456910
google provider handleUrl API
russellwheatley Apr 1, 2025
c623deb
chore: fix package.swift
russellwheatley Apr 1, 2025
98237d6
update package.swift so it validates
russellwheatley Apr 1, 2025
e3704a5
add FirebaseGoogleSwiftUI as lib to package.swift
russellwheatley Apr 1, 2025
df0f896
app project config - add google provider lib
russellwheatley Apr 1, 2025
c74a354
google provider setup for handling opening url
russellwheatley Apr 1, 2025
c580003
google provider -restructure files
russellwheatley Apr 1, 2025
fb7d515
sign in with google init
russellwheatley Apr 1, 2025
f1fcaeb
initial setup of google auth sign-in
russellwheatley Apr 1, 2025
10a6d90
google auth provider update
russellwheatley Apr 1, 2025
e922d74
google auth provider formatting
russellwheatley Apr 1, 2025
3214349
signing in with google provider
russellwheatley Apr 1, 2025
0537108
refactor: expose string utils on auth service
russellwheatley Apr 1, 2025
21423e4
facebook provider init
russellwheatley Apr 3, 2025
db2910d
facebook auth add base + add to example project
russellwheatley Apr 3, 2025
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ DerivedData
*.hmap
*.ipa
*.xcuserstate

.build/
# Third Party
/sdk

Expand Down
5 changes: 5 additions & 0 deletions .swiftformat
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Formatting Options
--indent 2
--maxwidth 100
--wrapparameters afterfirst
--disable wrapMultilineStatementBraces
8 changes: 8 additions & 0 deletions FirebaseSwiftUI/FirebaseAuthSwiftUI/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Foundation

public final class AuthConfiguration {
let shouldHideCancelButton: Bool
let interactiveDismissEnabled: Bool
let shouldAutoUpgradeAnonymousUsers: Bool
let customStringsBundle: Bundle?
let tosUrl: URL
let privacyPolicyUrl: URL

public init(shouldHideCancelButton: Bool = false,
interactiveDismissEnabled: Bool = true,
shouldAutoUpgradeAnonymousUsers: Bool = false,
customStringsBundle: Bundle? = nil,
tosUrl: URL = URL(string: "https://example.com/tos")!,
privacyPolicyUrl: URL = URL(string: "https://example.com/privacy")!) {
self.shouldHideCancelButton = shouldHideCancelButton
self.interactiveDismissEnabled = interactiveDismissEnabled
self.shouldAutoUpgradeAnonymousUsers = shouldAutoUpgradeAnonymousUsers
self.customStringsBundle = customStringsBundle
self.tosUrl = tosUrl
self.privacyPolicyUrl = privacyPolicyUrl
}
}
205 changes: 205 additions & 0 deletions FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
@preconcurrency import FirebaseAuth
import SwiftUI

public protocol GoogleProviderProtocol {
func handleUrl(_ url: URL) -> Bool
@MainActor func signInWithGoogle(clientID: String) async throws -> AuthCredential
}

public protocol FacebookProviderProtocol {}

public enum AuthenticationProvider {
case email
case google
}

public enum AuthenticationOperationType: String {
case signIn
case signUp
case deleteAccount
}

public enum AuthenticationState {
case unauthenticated
case authenticating
case authenticated
}

public enum AuthenticationFlow {
case login
case signUp
}

@MainActor
final class AuthListenerManager {
private var authStateHandle: AuthStateDidChangeListenerHandle?
private let auth: Auth
private weak var authEnvironment: AuthService?

init(auth: Auth, authEnvironment: AuthService) {
self.auth = auth
self.authEnvironment = authEnvironment
setupAuthenticationListener()
}

deinit {
if let handle = authStateHandle {
auth.removeStateDidChangeListener(handle)
}
}

private func setupAuthenticationListener() {
authStateHandle = auth.addStateDidChangeListener { [weak self] _, user in
self?.authEnvironment?.currentUser = user
self?.authEnvironment?.updateAuthenticationState()
}
}
}

@MainActor
@Observable
public final class AuthService {
public let configuration: AuthConfiguration
public let auth: Auth
private var listenerManager: AuthListenerManager?
private let googleProvider: GoogleProviderProtocol?
private let facebookProvider: FacebookProviderProtocol?
public let string: StringUtils

public init(configuration: AuthConfiguration = AuthConfiguration(), auth: Auth = Auth.auth(),
googleProvider: GoogleProviderProtocol? = nil,
facebookProvider: FacebookProviderProtocol? = nil) {
self.auth = auth
self.configuration = configuration
self.googleProvider = googleProvider
self.facebookProvider = facebookProvider
string = StringUtils(bundle: configuration.customStringsBundle ?? Bundle.module)
listenerManager = AuthListenerManager(auth: auth, authEnvironment: self)
}

public var currentUser: User?
public var authenticationState: AuthenticationState = .unauthenticated
public var authenticationFlow: AuthenticationFlow = .login

private var safeGoogleProvider: GoogleProviderProtocol {
get throws {
guard let provider = googleProvider else {
throw NSError(
domain: "AuthEnvironmentErrorDomain",
code: 1,
userInfo: [
NSLocalizedDescriptionKey: "`GoogleProviderSwift` has not been configured",
]
)
}
return provider
}
}

private var safeFacebookProvider: FacebookProviderProtocol {
get throws {
guard let provider = facebookProvider else {
throw NSError(
domain: "AuthEnvironmentErrorDomain",
code: 1,
userInfo: [
NSLocalizedDescriptionKey: "`FacebookProviderSwift` has not been configured",
]
)
}
return provider
}
}

func updateAuthenticationState() {
authenticationState =
(currentUser == nil || currentUser?.isAnonymous == true)
? .unauthenticated
: .authenticated
}

public func signOut() async throws {
try await auth.signOut()
updateAuthenticationState()
}

public func signInWithGoogle() async throws {
authenticationState = .authenticating
do {
guard let clientID = auth.app?.options.clientID else { throw NSError(
domain: "AuthServiceErrorDomain",
code: 2,
userInfo: [
NSLocalizedDescriptionKey: "OAuth client ID not found. Please make sure Google Sign-In is enabled in the Firebase console. You may have to download a new GoogleService-Info.plist file after enabling Google Sign-In.",
]
) }
let credential = try await safeGoogleProvider.signInWithGoogle(clientID: clientID)

try await signIn(with: credential)
updateAuthenticationState()
} catch {
authenticationState = .unauthenticated
throw error
}
}

func signIn(with credentials: AuthCredential) async throws {
authenticationState = .authenticating
do {
try await auth.signIn(with: credentials)
updateAuthenticationState()
} catch {
authenticationState = .unauthenticated
throw error
}
}

func sendEmailVerification() async throws {
if currentUser != nil {
do {
// TODO: - can use set user action code settings?
try await currentUser!.sendEmailVerification()
} catch {
throw error
}
}
}

func signIn(withEmail email: String, password: String) async throws {
let credential = EmailAuthProvider.credential(withEmail: email, password: password)
try await auth.signIn(with: credential)
}

func createUser(withEmail email: String, password: String) async throws {
authenticationState = .authenticating
do {
try await auth.createUser(withEmail: email, password: password)
updateAuthenticationState()
} catch {
authenticationState = .unauthenticated
throw error
}
}

func sendPasswordRecoveryEmail(to email: String) async throws {
do {
try await auth.sendPasswordReset(withEmail: email)
} catch {
throw error
}
}

func sendEmailSignInLink(to email: String) async throws {
do {
// TODO: - how does user set action code settings? Needs configuring
let actionCodeSettings = ActionCodeSettings()
actionCodeSettings.handleCodeInApp = true
try await auth.sendSignInLink(
toEmail: email,
actionCodeSettings: actionCodeSettings
)
} catch {
throw error
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* Error message displayed when there's no account matching the email address. Use short/abbreviated translation for 'email' which is less than 15 chars. */
"UserNotFoundError" = "That email address doesn’t match an existing account.";

/* Error message displayed when the email address is already in use. Use short/abbreviated translation for 'email' which is less than 15 chars. */
"EmailAlreadyInUseError" = "The email address is already in use by another account.";

/* Error message displayed when user enters an invalid email address. Use short/abbreviated translation for 'email' which is less than 15 chars. */
"InvalidEmailError" = "That email address isn't correct.";

/* Error message displayed when the password is too weak. */
"WeakPasswordError" = "Password must be at least 6 characters long.";

/* Error message displayed when many accounts have been created from same IP address. */
"SignUpTooManyTimesError" = "Too many account requests are coming from your IP address. Try again in a few minutes.";

/* Error message displayed when the email and password don't match. Use short/abbreviated translation for 'email' which is less than 15 chars. */
"WrongPasswordError" = "The email and password you entered don't match.";

/* Error message displayed when the account is disabled. Use short/abbreviated translation for 'email' which is less than 15 chars. */
"AccountDisabledError" = "That email address is for an account that has been disabled.";

/* Error message displayed when after re-authorization current user's email and re-authorized user's email doesn't match. Use short/abbreviated translation for 'email' which is less than 15 chars. */
"EmailsDoNotMatchError" = "Emails don't match";
10 changes: 10 additions & 0 deletions FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Utils/EmailUtils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation

class EmailUtils {
static let emailRegex = ".+@([a-zA-Z0-9\\-]+\\.)+[a-zA-Z0-9]{2,63}"

static func isValidEmail(_ email: String) -> Bool {
let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
return emailPredicate.evaluate(with: email)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import FirebaseAuth
import SwiftUI

let kKeyNotFound = "Key not found"

let kUsersNotFoundError = "UserNotFoundError"
let kEmailAlreadyInUseError = "EmailAlreadyInUseError"
let kInvalidEmailError = "InvalidEmailError"
let kWeakPasswordError = "WeakPasswordError"
let kSignUpTooManyTimesError = "SignUpTooManyTimesError"
let kWrongPasswordError = "WrongPasswordError"
let kAccountDisabledError = "AccountDisabledError"
let kEmailsDoNotMatchError = "EmailsDoNotMatchError"
let kUnknownError = "UnknownError"

public class StringUtils {
let bundle: Bundle
init(bundle: Bundle) {
self.bundle = bundle
}

public func localizedString(forKey key: String) -> String {
return bundle.localizedString(forKey: key, value: nil, table: nil)
}

public func localizedErrorMessage(for error: Error) -> String {
let authError = error as NSError
let errorCode = AuthErrorCode(rawValue: authError.code)
switch errorCode {
case .emailAlreadyInUse:
return localizedString(
forKey: kEmailAlreadyInUseError
)
case .invalidEmail:
return localizedString(forKey: kInvalidEmailError)
case .weakPassword:
return localizedString(forKey: kWeakPasswordError)
case .tooManyRequests:
return localizedString(
forKey: kSignUpTooManyTimesError
)
case .wrongPassword:
return localizedString(
forKey: kWrongPasswordError
)
case .userNotFound:
return localizedString(
forKey: kUsersNotFoundError
)
case .userDisabled:
return localizedString(
forKey: kAccountDisabledError
)
default:
return error.localizedDescription
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import SwiftUI

@MainActor
public struct AuthPickerView<Content: View> {
@Environment(AuthService.self) private var authService
let providerButtons: () -> Content

public init(@ViewBuilder providerButtons: @escaping () -> Content) {
self.providerButtons = providerButtons
}

private func switchFlow() {
authService.authenticationFlow = authService
.authenticationFlow == .login ? .signUp : .login
}
}

extension AuthPickerView: View {
public var body: some View {
VStack {
if authService.authenticationState == .authenticated {
SignedInView()
} else {
Text(authService.authenticationFlow == .login ? "Login" : "Sign up")
VStack { Divider() }
EmailAuthView()
providerButtons()
VStack { Divider() }
HStack {
Text(authService
.authenticationFlow == .login ? "Don't have an account yet?" :
"Already have an account?")
Button(action: {
withAnimation {
switchFlow()
}
}) {
Text(authService.authenticationFlow == .signUp ? "Log in" : "Sign up")
.fontWeight(.semibold)
.foregroundColor(.blue)
}
}
}
}
}
}
Loading