Skip to content

Commit c0612cf

Browse files
authored
fix(ui_auth): correctly handle phone auth in showReauthenticateDialog (#209)
1 parent a0526e5 commit c0612cf

File tree

5 files changed

+147
-35
lines changed

5 files changed

+147
-35
lines changed

Diff for: packages/firebase_ui_auth/lib/src/navigation/authentication.dart

+5-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ Future<bool> showReauthenticateDialog({
2020
/// A callback that is being called after user has successfully signed in.
2121
VoidCallback? onSignedIn,
2222

23+
/// {@macro ui.auth.views.reauthenticate_view.on_phone_verified}
24+
VoidCallback? onPhoneVerfifed,
25+
2326
/// A label that would be used for the "Sign in" button.
2427
String? actionButtonLabelOverride,
2528
}) async {
@@ -34,8 +37,9 @@ Future<bool> showReauthenticateDialog({
3437
child: ReauthenticateDialog(
3538
providers: providers,
3639
auth: auth,
37-
onSignedIn: onSignedIn,
40+
onSignedIn: onSignedIn ?? () => Navigator.of(context).pop(true),
3841
actionButtonLabelOverride: actionButtonLabelOverride,
42+
onPhoneVerfifed: onPhoneVerfifed,
3943
),
4044
),
4145
);

Diff for: packages/firebase_ui_auth/lib/src/views/reauthenticate_view.dart

+30-8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ class ReauthenticateView extends StatelessWidget {
1919
/// A callback that is being called when the user has successfuly signed in.
2020
final VoidCallback? onSignedIn;
2121

22+
/// {@template ui.auth.views.reauthenticate_view.on_phone_verified}
23+
/// A callback that is only called if a phone number is used to reauthenticate.
24+
/// Called before [onSignedIn].
25+
/// If not provided, [PhoneInputScreen] and [SMSCodeInputScreen] will be popped.
26+
/// Otherwise, it's up to the user to handle navigation logic.
27+
/// {@endtemplate}
28+
final VoidCallback? onPhoneVerfifed;
29+
2230
/// A label that would be used for the "Sign in" button.
2331
final String? actionButtonLabelOverride;
2432

@@ -33,6 +41,7 @@ class ReauthenticateView extends StatelessWidget {
3341
this.onSignedIn,
3442
this.actionButtonLabelOverride,
3543
this.showPasswordVisibilityToggle = false,
44+
this.onPhoneVerfifed,
3645
});
3746

3847
@override
@@ -60,7 +69,27 @@ class ReauthenticateView extends StatelessWidget {
6069
}
6170
}
6271

63-
return AuthStateListener(
72+
final m = ModalRoute.of(context);
73+
74+
final onSignedInAction = AuthStateChangeAction<SignedIn>((context, state) {
75+
if (getControllerForState(state) is PhoneAuthController) {
76+
if (onPhoneVerfifed != null) {
77+
onPhoneVerfifed?.call();
78+
} else {
79+
// Phone verification flow pushes new routes, so we need to pop them.
80+
Navigator.of(context).popUntil((route) {
81+
return route == m;
82+
});
83+
}
84+
}
85+
86+
onSignedIn?.call();
87+
});
88+
89+
return FirebaseUIActions(
90+
actions: [
91+
onSignedInAction,
92+
],
6493
child: LoginView(
6594
action: AuthAction.signIn,
6695
providers: providers,
@@ -69,13 +98,6 @@ class ReauthenticateView extends StatelessWidget {
6998
actionButtonLabelOverride: actionButtonLabelOverride,
7099
showPasswordVisibilityToggle: showPasswordVisibilityToggle,
71100
),
72-
listener: (oldState, newState, ctrl) {
73-
if (newState is SignedIn) {
74-
onSignedIn?.call();
75-
}
76-
77-
return false;
78-
},
79101
);
80102
}
81103
}

Diff for: packages/firebase_ui_auth/lib/src/widgets/reauthenticate_dialog.dart

+5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ class ReauthenticateDialog extends StatelessWidget {
2424
/// A callback that is being called when the user has successfully signed in.
2525
final VoidCallback? onSignedIn;
2626

27+
/// {@macro ui.auth.views.reauthenticate_view.on_phone_verified}
28+
final VoidCallback? onPhoneVerfifed;
29+
2730
/// A label that would be used for the "Sign in" button.
2831
final String? actionButtonLabelOverride;
2932

@@ -34,6 +37,7 @@ class ReauthenticateDialog extends StatelessWidget {
3437
this.auth,
3538
this.onSignedIn,
3639
this.actionButtonLabelOverride,
40+
this.onPhoneVerfifed,
3741
});
3842

3943
@override
@@ -47,6 +51,7 @@ class ReauthenticateDialog extends StatelessWidget {
4751
auth: auth,
4852
providers: providers,
4953
onSignedIn: onSignedIn,
54+
onPhoneVerfifed: onPhoneVerfifed,
5055
actionButtonLabelOverride: actionButtonLabelOverride,
5156
);
5257

Diff for: tests/integration_test/firebase_ui_auth/phone_verification_test.dart

+81-6
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Future<void> sendSMS(WidgetTester tester, String phoneNumber) async {
2424

2525
void main() {
2626
const labels = DefaultLocalizations();
27+
setUpTests();
2728

2829
group('PhoneInputScreen', () {
2930
testWidgets('allows to pick country code', (tester) async {
@@ -98,8 +99,8 @@ void main() {
9899

99100
await completer.future;
100101

101-
final codes = await getVerificationCodes();
102-
expect(codes['+1555555555'], isNotEmpty);
102+
final code = await getVerificationCode('+1555555555');
103+
expect(code, isNotEmpty);
103104
},
104105
);
105106

@@ -149,14 +150,14 @@ void main() {
149150
final smsCodeInput = find.byType(SMSCodeInput);
150151
expect(smsCodeInput, findsOneWidget);
151152

152-
final codes = await getVerificationCodes();
153-
final code = codes['+1555555556']!;
153+
final code = await getVerificationCode('+1555555556');
154154
final invalidCode =
155155
code.split('').map(int.parse).map((v) => (v + 1) % 10).join();
156156

157157
await tester.tap(smsCodeInput);
158158

159159
await tester.enterText(smsCodeInput, invalidCode);
160+
await tester.pumpAndSettle();
160161
await tester.testTextInput.receiveAction(TextInputAction.done);
161162
await completer.future;
162163

@@ -192,8 +193,7 @@ void main() {
192193
await sendSMS(tester, '555555557');
193194

194195
final smsCodeInput = find.byType(SMSCodeInput);
195-
final codes = await getVerificationCodes();
196-
final code = codes['+1555555557']!;
196+
final code = await getVerificationCode('+1555555557');
197197

198198
await tester.tap(smsCodeInput);
199199

@@ -209,4 +209,79 @@ void main() {
209209
},
210210
);
211211
});
212+
213+
group('showReauthenticateDialog', () {
214+
testWidgets(
215+
'can reauthenticate using phone number',
216+
(tester) async {
217+
final credCompleter = Completer<fba.PhoneAuthCredential>();
218+
219+
await auth.verifyPhoneNumber(
220+
phoneNumber: '+1555555558',
221+
verificationCompleted: credCompleter.complete,
222+
verificationFailed: credCompleter.completeError,
223+
codeSent: (verificationId, _) async {
224+
final code = await getVerificationCode('+1555555558');
225+
226+
final credential = fba.PhoneAuthProvider.credential(
227+
verificationId: verificationId,
228+
smsCode: code,
229+
);
230+
231+
credCompleter.complete(credential);
232+
},
233+
codeAutoRetrievalTimeout: (_) {},
234+
);
235+
236+
final cred = await credCompleter.future;
237+
await auth.signInWithCredential(cred);
238+
239+
final reauthenticationCompleter = Completer<void>();
240+
// emulator doesn't return a new sms code if the same phone number is
241+
// used within 30(?) seconds.
242+
await Future.delayed(const Duration(seconds: 30));
243+
bool onPhoneVerifiedCalled = false;
244+
245+
await render(
246+
tester,
247+
Builder(builder: (context) {
248+
return ElevatedButton(
249+
onPressed: () {
250+
showReauthenticateDialog(
251+
context: context,
252+
providers: [PhoneAuthProvider()],
253+
onSignedIn: () => reauthenticationCompleter.complete(),
254+
onPhoneVerfifed: () => onPhoneVerifiedCalled = true,
255+
);
256+
},
257+
child: const Text('Reauthenticate'),
258+
);
259+
}),
260+
);
261+
262+
await tester.tap(find.byType(ElevatedButton));
263+
await tester.pumpAndSettle();
264+
265+
await tester.tap(find.text('Sign in with phone'));
266+
await tester.pumpAndSettle();
267+
268+
await sendSMS(tester, '555555558');
269+
270+
final smsCodeInput = find.byType(SMSCodeInput);
271+
final code = await getVerificationCode('+1555555558');
272+
273+
await tester.tap(smsCodeInput);
274+
await tester.enterText(smsCodeInput, code);
275+
await tester.testTextInput.receiveAction(TextInputAction.done);
276+
await tester.pumpAndSettle();
277+
278+
final future = reauthenticationCompleter.future.timeout(
279+
const Duration(seconds: 5),
280+
);
281+
282+
expect(future, completes);
283+
expect(onPhoneVerifiedCalled, isTrue);
284+
},
285+
);
286+
});
212287
}

Diff for: tests/integration_test/utils.dart

+26-20
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import 'dart:async';
66
import 'dart:convert';
7-
import 'dart:io';
87

98
import 'package:cloud_firestore/cloud_firestore.dart';
109
import 'package:firebase_auth/firebase_auth.dart' as fba;
@@ -83,12 +82,11 @@ Future<void> render(WidgetTester tester, Widget widget) async {
8382
);
8483
}
8584

86-
Future<http.Response> retry(Future<http.Response> Function() fn) async {
85+
Future<T> retry<T>(Future<T> Function() fn, [int maxAttempts = 5]) async {
8786
var delay = const Duration(milliseconds: 100);
8887
int attempts = 0;
89-
int maxAttempts = 5;
9088

91-
final completer = Completer<http.Response>();
89+
final completer = Completer<T>();
9290

9391
await Future.doWhile(() async {
9492
try {
@@ -101,8 +99,8 @@ Future<http.Response> retry(Future<http.Response> Function() fn) async {
10199
return false;
102100
}
103101

104-
stdout.writeln('Request failed: $e');
105-
stdout.writeln('retrying in $delay');
102+
debugPrint('Request failed: $e');
103+
debugPrint('retrying in $delay');
106104
await Future.delayed(delay);
107105
delay *= 2;
108106
attempts++;
@@ -122,24 +120,32 @@ Future<void> deleteAllAccounts() async {
122120
if (res.statusCode != 200) throw Exception('Delete failed');
123121
}
124122

125-
Future<Map<String, String>> getVerificationCodes() async {
123+
Future<String> getVerificationCode(String phoneNumber) async {
126124
final id = DefaultFirebaseOptions.currentPlatform.projectId;
127125
final uriString =
128126
'http://$testEmulatorHost:9099/emulator/v1/projects/$id/verificationCodes';
129-
final res = await retry(() => http.get(Uri.parse(uriString)));
130-
131-
final body = json.decode(res.body);
132-
final codes = (body['verificationCodes'] as List).fold<Map<String, String>>(
133-
{},
134-
(acc, value) {
135-
return {
136-
...acc,
137-
value['phoneNumber']: value['code'],
138-
};
139-
},
140-
);
127+
final code = await retry(() async {
128+
final res = await http.get(Uri.parse(uriString));
129+
final body = json.decode(res.body);
130+
131+
final codes = (body['verificationCodes'] as List).fold<Map<String, String>>(
132+
{},
133+
(acc, value) {
134+
return {
135+
...acc,
136+
value['phoneNumber']: value['code'],
137+
};
138+
},
139+
);
140+
141+
if (codes[phoneNumber] == null) {
142+
throw Exception('Code not found');
143+
}
144+
145+
return codes[phoneNumber]!;
146+
}, 6);
141147

142-
return codes;
148+
return code;
143149
}
144150

145151
Future<CollectionReference<T>> clearCollection<T>(

0 commit comments

Comments
 (0)