Skip to content

Commit ad72bcc

Browse files
committed
add providerBuilder for sign in and register screen layout customization
1 parent 5d91871 commit ad72bcc

File tree

8 files changed

+247
-39
lines changed

8 files changed

+247
-39
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,4 @@ firebase-debug.log
7878
firestore-debug.log
7979
database-debug.log
8080
ui-debug.log
81+
conductor/

packages/firebase_ui_auth/lib/src/screens/internal/login_screen.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ class LoginScreen extends StatelessWidget {
5757
/// {@macro ui.auth.screens.responsive_page.max_width}
5858
final double? maxWidth;
5959

60+
/// A builder that allows to customize the order and appearance of the providers.
61+
final ProvidersBuilder? providersBuilder;
62+
6063
const LoginScreen({
6164
super.key,
6265
required this.action,
@@ -77,6 +80,7 @@ class LoginScreen extends StatelessWidget {
7780
this.styles,
7881
this.showPasswordVisibilityToggle = false,
7982
this.maxWidth,
83+
this.providersBuilder,
8084
});
8185

8286
@override
@@ -96,6 +100,7 @@ class LoginScreen extends StatelessWidget {
96100
subtitleBuilder: subtitleBuilder,
97101
footerBuilder: footerBuilder,
98102
showPasswordVisibilityToggle: showPasswordVisibilityToggle,
103+
providersBuilder: providersBuilder,
99104
),
100105
),
101106
);

packages/firebase_ui_auth/lib/src/screens/register_screen.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ class RegisterScreen extends MultiProviderScreen {
9393
/// {@macro ui.auth.screens.responsive_page.max_width}
9494
final double? maxWidth;
9595

96+
/// A builder that allows to customize the order and appearance of the providers.
97+
///
98+
/// If not provided, the default explicit order is used:
99+
/// Email, Phone, Email Link, OAuth.
100+
final ProvidersBuilder? providersBuilder;
101+
96102
const RegisterScreen({
97103
super.key,
98104
super.auth,
@@ -112,6 +118,7 @@ class RegisterScreen extends MultiProviderScreen {
112118
this.styles,
113119
this.showPasswordVisibilityToggle = false,
114120
this.maxWidth,
121+
this.providersBuilder,
115122
});
116123

117124
@override
@@ -136,6 +143,7 @@ class RegisterScreen extends MultiProviderScreen {
136143
breakpoint: breakpoint,
137144
showPasswordVisibilityToggle: showPasswordVisibilityToggle,
138145
maxWidth: maxWidth,
146+
providersBuilder: providersBuilder,
139147
),
140148
);
141149
}

packages/firebase_ui_auth/lib/src/screens/sign_in_screen.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ class SignInScreen extends MultiProviderScreen {
101101
/// {@macro ui.auth.screens.responsive_page.max_width}
102102
final double? maxWidth;
103103

104+
/// A builder that allows to customize the order and appearance of the providers.
105+
///
106+
/// If not provided, the default explicit order is used:
107+
/// Email, Phone, Email Link, OAuth.
108+
final ProvidersBuilder? providersBuilder;
109+
104110
/// {@macro ui.auth.screens.sign_in_screen}
105111
const SignInScreen({
106112
super.key,
@@ -122,6 +128,7 @@ class SignInScreen extends MultiProviderScreen {
122128
this.styles,
123129
this.showPasswordVisibilityToggle = false,
124130
this.maxWidth,
131+
this.providersBuilder,
125132
});
126133

127134
@override
@@ -151,6 +158,7 @@ class SignInScreen extends MultiProviderScreen {
151158
breakpoint: breakpoint,
152159
showPasswordVisibilityToggle: showPasswordVisibilityToggle,
153160
maxWidth: maxWidth,
161+
providersBuilder: providersBuilder,
154162
),
155163
);
156164
}

packages/firebase_ui_auth/lib/src/views/login_view.dart

Lines changed: 91 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ typedef AuthViewContentBuilder = Widget Function(
1818
AuthAction action,
1919
);
2020

21+
typedef ProvidersBuilder = List<Widget> Function(
22+
BuildContext context,
23+
List<AuthProvider> providers,
24+
AuthAction action,
25+
);
26+
2127
/// {@template ui.auth.views.login_view}
2228
/// A view that could be used to build a custom [SignInScreen] or
2329
/// [RegisterScreen].
@@ -58,6 +64,12 @@ class LoginView extends StatefulWidget {
5864
/// {@macro ui.auth.widgets.email_from.showPasswordVisibilityToggle}
5965
final bool showPasswordVisibilityToggle;
6066

67+
/// A builder that allows to customize the order and appearance of the providers.
68+
///
69+
/// If not provided, the default explicit order is used:
70+
/// Email, Phone, Email Link, OAuth.
71+
final ProvidersBuilder? providersBuilder;
72+
6173
/// {@macro ui.auth.views.login_view}
6274
const LoginView({
6375
super.key,
@@ -72,6 +84,7 @@ class LoginView extends StatefulWidget {
7284
this.subtitleBuilder,
7385
this.actionButtonLabelOverride,
7486
this.showPasswordVisibilityToggle = false,
87+
this.providersBuilder,
7588
});
7689

7790
@override
@@ -204,53 +217,92 @@ class _LoginViewState extends State<LoginView> {
204217
super.didUpdateWidget(oldWidget);
205218
}
206219

220+
Widget? _buildProviderWidget(TargetPlatform platform, AuthProvider provider) {
221+
final l = FirebaseUILocalizations.labelsOf(context);
222+
223+
if (provider is EmailAuthProvider) {
224+
return EmailForm(
225+
key: ValueKey(_action),
226+
auth: widget.auth,
227+
action: _action,
228+
provider: provider,
229+
email: widget.email,
230+
actionButtonLabelOverride: widget.actionButtonLabelOverride,
231+
showPasswordVisibilityToggle: widget.showPasswordVisibilityToggle,
232+
);
233+
} else if (provider is PhoneAuthProvider) {
234+
return PhoneVerificationButton(
235+
label: l.signInWithPhoneButtonText,
236+
action: _action,
237+
auth: widget.auth,
238+
);
239+
} else if (provider is EmailLinkAuthProvider) {
240+
return EmailLinkSignInButton(
241+
auth: widget.auth,
242+
provider: provider,
243+
);
244+
} else if (provider is OAuthProvider && !_buttonsBuilt) {
245+
return _buildOAuthButtons(platform);
246+
}
247+
return null;
248+
}
249+
250+
List<Widget> _defaultProvidersBuilder(
251+
BuildContext context,
252+
List<AuthProvider> providers,
253+
AuthAction action,
254+
) {
255+
final platform = Theme.of(context).platform;
256+
final children = <Widget>[];
257+
258+
void addForType<T>() {
259+
for (var provider in providers) {
260+
if (provider is T && provider.supportsPlatform(platform)) {
261+
final w = _buildProviderWidget(platform, provider);
262+
263+
if (w != null) {
264+
if (provider is OAuthProvider) {
265+
children.add(w);
266+
} else {
267+
children.add(const SizedBox(height: 8));
268+
children.add(w);
269+
if (provider is PhoneAuthProvider) {
270+
children.add(const SizedBox(height: 8));
271+
}
272+
}
273+
}
274+
}
275+
}
276+
}
277+
278+
addForType<EmailAuthProvider>();
279+
addForType<PhoneAuthProvider>();
280+
addForType<EmailLinkAuthProvider>();
281+
addForType<OAuthProvider>();
282+
283+
return children;
284+
}
285+
207286
@override
208287
Widget build(BuildContext context) {
209-
final l = FirebaseUILocalizations.labelsOf(context);
210288
final platform = Theme.of(context).platform;
211289
_buttonsBuilt = false;
212290

291+
final children = <Widget>[
292+
if (_showTitle) ..._buildHeader(context),
293+
];
294+
295+
final builder = widget.providersBuilder ?? _defaultProvidersBuilder;
296+
children.addAll(builder(context, widget.providers, _action));
297+
298+
if (widget.footerBuilder != null) {
299+
children.add(widget.footerBuilder!(context, _action));
300+
}
301+
213302
return IntrinsicHeight(
214303
child: Column(
215304
crossAxisAlignment: CrossAxisAlignment.stretch,
216-
children: [
217-
if (_showTitle) ..._buildHeader(context),
218-
for (var provider in widget.providers)
219-
if (provider.supportsPlatform(platform))
220-
if (provider is EmailAuthProvider) ...[
221-
const SizedBox(height: 8),
222-
EmailForm(
223-
key: ValueKey(_action),
224-
auth: widget.auth,
225-
action: _action,
226-
provider: provider,
227-
email: widget.email,
228-
actionButtonLabelOverride: widget.actionButtonLabelOverride,
229-
showPasswordVisibilityToggle:
230-
widget.showPasswordVisibilityToggle,
231-
)
232-
] else if (provider is PhoneAuthProvider) ...[
233-
const SizedBox(height: 8),
234-
PhoneVerificationButton(
235-
label: l.signInWithPhoneButtonText,
236-
action: _action,
237-
auth: widget.auth,
238-
),
239-
const SizedBox(height: 8),
240-
] else if (provider is EmailLinkAuthProvider) ...[
241-
const SizedBox(height: 8),
242-
EmailLinkSignInButton(
243-
auth: widget.auth,
244-
provider: provider,
245-
),
246-
] else if (provider is OAuthProvider && !_buttonsBuilt)
247-
_buildOAuthButtons(platform),
248-
if (widget.footerBuilder != null)
249-
widget.footerBuilder!(
250-
context,
251-
_action,
252-
),
253-
],
305+
children: children,
254306
),
255307
);
256308
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2024, the Chromium project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
6+
import 'package:flutter/material.dart';
7+
import 'package:flutter_test/flutter_test.dart';
8+
import '../test_utils.dart';
9+
10+
void main() {
11+
group('RegisterScreen providersBuilder', () {
12+
setUpAll(() {
13+
setFirebaseUiIsTestMode(true);
14+
});
15+
16+
testWidgets('propagates providersBuilder to LoginView', (tester) async {
17+
await tester.pumpWidget(
18+
TestMaterialApp(
19+
child: RegisterScreen(
20+
auth: MockAuth(),
21+
providers: [EmailAuthProvider()],
22+
providersBuilder: (context, providers, action) {
23+
return [const Text('Custom Register Builder')];
24+
},
25+
),
26+
),
27+
);
28+
29+
expect(find.text('Custom Register Builder'), findsOneWidget);
30+
});
31+
});
32+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2024, the Chromium project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
6+
import 'package:flutter/material.dart';
7+
import 'package:flutter_test/flutter_test.dart';
8+
import '../test_utils.dart';
9+
10+
void main() {
11+
group('SignInScreen providersBuilder', () {
12+
setUpAll(() {
13+
setFirebaseUiIsTestMode(true);
14+
});
15+
16+
testWidgets('propagates providersBuilder to LoginView', (tester) async {
17+
await tester.pumpWidget(
18+
TestMaterialApp(
19+
child: SignInScreen(
20+
auth: MockAuth(),
21+
providers: [EmailAuthProvider()],
22+
providersBuilder: (context, providers, action) {
23+
return [const Text('Custom SignIn Builder')];
24+
},
25+
),
26+
),
27+
);
28+
29+
expect(find.text('Custom SignIn Builder'), findsOneWidget);
30+
});
31+
});
32+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright 2024, the Chromium project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
6+
import 'package:flutter/material.dart';
7+
import 'package:flutter_test/flutter_test.dart';
8+
import '../test_utils.dart';
9+
10+
void main() {
11+
group('LoginView providersBuilder', () {
12+
testWidgets('uses custom providersBuilder when provided', (tester) async {
13+
await tester.pumpWidget(
14+
TestMaterialApp(
15+
child: LoginView(
16+
auth: MockAuth(),
17+
action: AuthAction.signIn,
18+
providers: [
19+
EmailAuthProvider(),
20+
PhoneAuthProvider(),
21+
],
22+
providersBuilder: (context, providers, action) {
23+
return [
24+
const Text('Custom Header'),
25+
for (final provider in providers)
26+
if (provider is EmailAuthProvider)
27+
const Text('Custom Email Form'),
28+
];
29+
},
30+
),
31+
),
32+
);
33+
34+
expect(find.text('Custom Header'), findsOneWidget);
35+
expect(find.text('Custom Email Form'), findsOneWidget);
36+
// Phone provider should be ignored as per our custom builder logic
37+
expect(find.byType(PhoneVerificationButton), findsNothing);
38+
});
39+
40+
testWidgets('uses default explicit order when no builder provided', (tester) async {
41+
await tester.pumpWidget(
42+
TestMaterialApp(
43+
child: LoginView(
44+
auth: MockAuth(),
45+
action: AuthAction.signIn,
46+
providers: [
47+
PhoneAuthProvider(), // Phone first in list
48+
EmailAuthProvider(), // Email second
49+
],
50+
),
51+
),
52+
);
53+
54+
// But default explicit order should be Email then Phone (if we strictly follow the new spec)
55+
// The spec says: "If no providersBuilder is provided, the system should use a default explicit order: [Email, Phone, EmailLink, OAuth]."
56+
57+
final emailFinder = find.byType(EmailForm);
58+
final phoneFinder = find.byType(PhoneVerificationButton);
59+
60+
expect(emailFinder, findsOneWidget);
61+
expect(phoneFinder, findsOneWidget);
62+
63+
final emailTop = tester.getTopLeft(emailFinder).dy;
64+
final phoneTop = tester.getTopLeft(phoneFinder).dy;
65+
66+
// Verify explicit default order: Email should be above Phone
67+
expect(emailTop, lessThan(phoneTop));
68+
});
69+
});
70+
}

0 commit comments

Comments
 (0)