Skip to content

Commit cb015e4

Browse files
Merge pull request #1255 from firebase/ui-tweaks
2 parents ef70a9d + 5ced7b3 commit cb015e4

File tree

8 files changed

+198
-82
lines changed

8 files changed

+198
-82
lines changed

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,45 @@ public final class AuthService {
8383
public var authenticationFlow: AuthenticationFlow = .login
8484
public var errorMessage = ""
8585
public let passwordPrompt: PasswordPromptCoordinator = .init()
86+
87+
// MARK: - AuthPickerView Modal APIs
88+
89+
public var isShowingAuthModal = false
90+
91+
public enum AuthModalContentType {
92+
case phoneAuth
93+
}
94+
95+
public var currentModal: AuthModalContentType?
96+
97+
public var authModalViewBuilderRegistry: [AuthModalContentType: () -> AnyView] = [:]
98+
99+
public func registerModalView(for type: AuthModalContentType,
100+
@ViewBuilder builder: @escaping () -> AnyView) {
101+
authModalViewBuilderRegistry[type] = builder
102+
}
103+
104+
public func viewForCurrentModal() -> AnyView? {
105+
guard let type = currentModal,
106+
let builder = authModalViewBuilderRegistry[type] else {
107+
return nil
108+
}
109+
return builder()
110+
}
111+
112+
public func presentModal(for type: AuthModalContentType) {
113+
currentModal = type
114+
isShowingAuthModal = true
115+
}
116+
117+
public func dismissModal() {
118+
isShowingAuthModal = false
119+
}
120+
121+
// MARK: - End AuthPickerView Modal APIs
122+
123+
// MARK: - Provider APIs
124+
86125
private var unsafeGoogleProvider: (any GoogleProviderAuthUIProtocol)?
87126
private var unsafeFacebookProvider: (any FacebookProviderAuthUIProtocol)?
88127
private var unsafePhoneAuthProvider: (any PhoneAuthProviderAuthUIProtocol)?
@@ -146,6 +185,8 @@ public final class AuthService {
146185
}
147186
}
148187

188+
// MARK: - End Provider APIs
189+
149190
private func safeActionCodeSettings() throws -> ActionCodeSettings {
150191
// email sign-in requires action code settings
151192
guard let actionCodeSettings = configuration

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Utils/StringUtils.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ public class StringUtils {
271271
/// found in:
272272
/// - SignInWithFacebookButton
273273
public var facebookLoginButtonLabel: String {
274-
return localizedString(for: "Continue with Facebook")
274+
return localizedString(for: "Sign in with Facebook")
275275
}
276276

277277
/// Facebook provider
@@ -319,8 +319,8 @@ public class StringUtils {
319319
/// Phone provider
320320
/// found in:
321321
/// - PhoneAuthButtonView
322-
public var smsCodeSentLabel: String {
323-
return localizedString(for: "SMS code sent")
322+
public var smsCodeSendButtonLabel: String {
323+
return localizedString(for: "Send SMS code")
324324
}
325325

326326
/// Phone provider

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ public struct AuthPickerView {
1212
.authenticationFlow == .login ? .signUp : .login
1313
}
1414

15+
private var isAuthModalPresented: Binding<Bool> {
16+
Binding(
17+
get: { authService.isShowingAuthModal },
18+
set: { authService.isShowingAuthModal = $0 }
19+
)
20+
}
21+
1522
@ViewBuilder
1623
private var authPickerTitleView: some View {
1724
if authService.authView == .authPicker {
@@ -71,6 +78,33 @@ extension AuthPickerView: View {
7178
EmptyView()
7279
}
7380
}
81+
}.sheet(isPresented: isAuthModalPresented) {
82+
VStack(spacing: 0) {
83+
HStack {
84+
Button(action: {
85+
authService.dismissModal()
86+
}) {
87+
HStack(spacing: 4) {
88+
Image(systemName: "chevron.left")
89+
.font(.system(size: 17, weight: .medium))
90+
Text(authService.string.backButtonLabel)
91+
.font(.system(size: 17))
92+
}
93+
.foregroundColor(.blue)
94+
}
95+
Spacer()
96+
}
97+
.padding()
98+
.background(Color(.systemBackground))
99+
100+
Divider()
101+
102+
if let view = authService.viewForCurrentModal() {
103+
view
104+
.frame(maxWidth: .infinity, maxHeight: .infinity)
105+
.padding()
106+
}
107+
}
74108
}
75109
}
76110
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ extension EmailAuthView: View {
123123
}
124124
}
125125
.disabled(!isValid)
126-
.padding([.top, .bottom], 8)
126+
.padding([.top, .bottom, .horizontal], 8)
127127
.frame(maxWidth: .infinity)
128128
.buttonStyle(.borderedProminent)
129129
Button(action: {

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/VerifyEmailView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ extension VerifyEmailView: View {
2525
.padding(.vertical, 8)
2626
.frame(maxWidth: .infinity)
2727
}
28-
.padding([.top, .bottom], 8)
28+
.padding([.top, .bottom, .horizontal], 8)
2929
.frame(maxWidth: .infinity)
3030
.buttonStyle(.borderedProminent)
3131
}.sheet(isPresented: $showModal) {

FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthButtonView.swift

Lines changed: 11 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -5,90 +5,25 @@ import SwiftUI
55
@MainActor
66
public struct PhoneAuthButtonView {
77
@Environment(AuthService.self) private var authService
8-
@State private var errorMessage = ""
9-
@State private var phoneNumber = ""
10-
@State private var showVerificationCodeInput = false
11-
@State private var verificationCode = ""
12-
@State private var verificationID = ""
138

149
public init() {}
1510
}
1611

1712
extension PhoneAuthButtonView: View {
1813
public var body: some View {
19-
if authService.authenticationState != .authenticating {
20-
VStack {
21-
LabeledContent {
22-
TextField(authService.string.enterPhoneNumberLabel, text: $phoneNumber)
23-
.textInputAutocapitalization(.never)
24-
.disableAutocorrection(true)
25-
.submitLabel(.next)
26-
} label: {
27-
Image(systemName: "at")
28-
}.padding(.vertical, 6)
29-
.background(Divider(), alignment: .bottom)
30-
.padding(.bottom, 4)
31-
Button(action: {
32-
Task {
33-
do {
34-
let id = try await authService.verifyPhoneNumber(phoneNumber: phoneNumber)
35-
verificationID = id
36-
showVerificationCodeInput = true
37-
} catch {
38-
errorMessage = authService.string.localizedErrorMessage(
39-
for: error
40-
)
41-
}
42-
}
43-
}) {
44-
Text(authService.string.smsCodeSentLabel)
45-
.padding(.vertical, 8)
46-
.frame(maxWidth: .infinity)
47-
}
48-
.disabled(!PhoneUtils.isValidPhoneNumber(phoneNumber))
49-
.padding([.top, .bottom], 8)
50-
.frame(maxWidth: .infinity)
51-
.buttonStyle(.borderedProminent)
52-
Text(errorMessage).foregroundColor(.red)
53-
}.sheet(isPresented: $showVerificationCodeInput) {
54-
TextField(authService.string.phoneNumberVerificationCodeLabel, text: $verificationCode)
55-
.keyboardType(.numberPad)
56-
.padding()
57-
.background(Color(.systemGray6))
58-
.cornerRadius(8)
59-
.padding(.horizontal)
60-
61-
Button(action: {
62-
Task {
63-
do {
64-
try await authService.signInWithPhoneNumber(
65-
verificationID: verificationID,
66-
verificationCode: verificationCode
67-
)
68-
} catch {
69-
errorMessage = authService.string.localizedErrorMessage(for: error)
70-
}
71-
showVerificationCodeInput = false
72-
}
73-
}) {
74-
Text(authService.string.verifyPhoneNumberAndSignInLabel)
75-
.foregroundColor(.white)
76-
.padding()
77-
.frame(maxWidth: .infinity)
78-
.background(Color.green)
79-
.cornerRadius(8)
80-
.padding(.horizontal)
81-
}
82-
}.onOpenURL { url in
83-
authService.auth.canHandle(url)
14+
Button(action: {
15+
authService.registerModalView(for: .phoneAuth) {
16+
AnyView(PhoneAuthView().environment(authService))
8417
}
85-
} else {
86-
ProgressView()
87-
.progressViewStyle(CircularProgressViewStyle(tint: .white))
88-
.padding(.vertical, 8)
89-
.frame(maxWidth: .infinity)
18+
authService.presentModal(for: .phoneAuth)
19+
}) {
20+
Label("Sign in with Phone", systemImage: "phone.fill")
21+
.foregroundColor(.white)
22+
.padding()
23+
.frame(maxWidth: .infinity, alignment: .leading)
24+
.background(Color.green.opacity(0.8)) // Light green
25+
.cornerRadius(8)
9026
}
91-
Text(errorMessage).foregroundColor(.red)
9227
}
9328
}
9429

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
//
2+
// PhoneAuthView.swift
3+
// FirebaseUI
4+
//
5+
// Created by Russell Wheatley on 14/05/2025.
6+
//
7+
8+
import FirebaseAuthSwiftUI
9+
import FirebaseCore
10+
import SwiftUI
11+
12+
@MainActor
13+
public struct PhoneAuthView {
14+
@Environment(AuthService.self) private var authService
15+
@State private var errorMessage = ""
16+
@State private var phoneNumber = ""
17+
@State private var showVerificationCodeInput = false
18+
@State private var verificationCode = ""
19+
@State private var verificationID = ""
20+
21+
public init() {}
22+
}
23+
24+
extension PhoneAuthView: View {
25+
public var body: some View {
26+
if authService.authenticationState != .authenticating {
27+
VStack {
28+
LabeledContent {
29+
TextField(authService.string.enterPhoneNumberLabel, text: $phoneNumber)
30+
.textInputAutocapitalization(.never)
31+
.disableAutocorrection(true)
32+
.submitLabel(.next)
33+
} label: {
34+
Image(systemName: "at")
35+
}.padding(.vertical, 6)
36+
.background(Divider(), alignment: .bottom)
37+
.padding(.bottom, 4)
38+
Button(action: {
39+
Task {
40+
do {
41+
let id = try await authService.verifyPhoneNumber(phoneNumber: phoneNumber)
42+
verificationID = id
43+
showVerificationCodeInput = true
44+
} catch {
45+
errorMessage = authService.string.localizedErrorMessage(
46+
for: error
47+
)
48+
}
49+
}
50+
}) {
51+
Text(authService.string.smsCodeSendButtonLabel)
52+
.padding(.vertical, 8)
53+
.frame(maxWidth: .infinity)
54+
}
55+
.disabled(!PhoneUtils.isValidPhoneNumber(phoneNumber))
56+
.padding([.top, .bottom], 8)
57+
.frame(maxWidth: .infinity)
58+
.buttonStyle(.borderedProminent)
59+
Text(errorMessage).foregroundColor(.red)
60+
}.sheet(isPresented: $showVerificationCodeInput) {
61+
TextField(authService.string.phoneNumberVerificationCodeLabel, text: $verificationCode)
62+
.keyboardType(.numberPad)
63+
.padding()
64+
.background(Color(.systemGray6))
65+
.cornerRadius(8)
66+
.padding(.horizontal)
67+
68+
Button(action: {
69+
Task {
70+
do {
71+
try await authService.signInWithPhoneNumber(
72+
verificationID: verificationID,
73+
verificationCode: verificationCode
74+
)
75+
} catch {
76+
errorMessage = authService.string.localizedErrorMessage(for: error)
77+
}
78+
showVerificationCodeInput = false
79+
authService.dismissModal()
80+
}
81+
}) {
82+
Text(authService.string.verifyPhoneNumberAndSignInLabel)
83+
.foregroundColor(.white)
84+
.padding()
85+
.frame(maxWidth: .infinity)
86+
.background(Color.green)
87+
.cornerRadius(8)
88+
.padding(.horizontal)
89+
}
90+
}.onOpenURL { url in
91+
authService.auth.canHandle(url)
92+
}
93+
} else {
94+
ProgressView()
95+
.progressViewStyle(CircularProgressViewStyle(tint: .white))
96+
.padding(.vertical, 8)
97+
.frame(maxWidth: .infinity)
98+
}
99+
}
100+
}
101+
102+
#Preview {
103+
FirebaseOptions.dummyConfigurationForPreview()
104+
return PhoneAuthView()
105+
.environment(AuthService())
106+
}

samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ struct ContentView: View {
3434
configuration: configuration
3535
)
3636
.withGoogleSignIn()
37-
.withFacebookSignIn()
3837
.withPhoneSignIn()
38+
.withFacebookSignIn()
3939
.withEmailSignIn()
4040
}
4141

0 commit comments

Comments
 (0)