Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ Future<bool> showReauthenticateDialog({
/// A callback that is being called after user has successfully signed in.
VoidCallback? onSignedIn,

/// {@macro ui.auth.views.reauthenticate_view.on_phone_verified}
VoidCallback? onPhoneVerfifed,

/// A label that would be used for the "Sign in" button.
String? actionButtonLabelOverride,
}) async {
Expand All @@ -34,8 +37,9 @@ Future<bool> showReauthenticateDialog({
child: ReauthenticateDialog(
providers: providers,
auth: auth,
onSignedIn: onSignedIn,
onSignedIn: onSignedIn ?? () => Navigator.of(context).pop(true),
actionButtonLabelOverride: actionButtonLabelOverride,
onPhoneVerfifed: onPhoneVerfifed,
),
),
);
Expand Down
38 changes: 30 additions & 8 deletions packages/firebase_ui_auth/lib/src/views/reauthenticate_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ class ReauthenticateView extends StatelessWidget {
/// A callback that is being called when the user has successfuly signed in.
final VoidCallback? onSignedIn;

/// {@template ui.auth.views.reauthenticate_view.on_phone_verified}
/// A callback that is only called if a phone number is used to reauthenticate.
/// Called before [onSignedIn].
/// If not provided, [PhoneInputScreen] and [SMSCodeInputScreen] will be popped.
/// Otherwise, it's up to the user to handle navigation logic.
/// {@endtemplate}
final VoidCallback? onPhoneVerfifed;

/// A label that would be used for the "Sign in" button.
final String? actionButtonLabelOverride;

Expand All @@ -33,6 +41,7 @@ class ReauthenticateView extends StatelessWidget {
this.onSignedIn,
this.actionButtonLabelOverride,
this.showPasswordVisibilityToggle = false,
this.onPhoneVerfifed,
});

@override
Expand Down Expand Up @@ -60,7 +69,27 @@ class ReauthenticateView extends StatelessWidget {
}
}

return AuthStateListener(
final m = ModalRoute.of(context);

final onSignedInAction = AuthStateChangeAction<SignedIn>((context, state) {
if (getControllerForState(state) is PhoneAuthController) {
if (onPhoneVerfifed != null) {
onPhoneVerfifed?.call();
} else {
// Phone verification flow pushes new routes, so we need to pop them.
Navigator.of(context).popUntil((route) {
return route == m;
});
}
}

onSignedIn?.call();
});

return FirebaseUIActions(
actions: [
onSignedInAction,
],
child: LoginView(
action: AuthAction.signIn,
providers: providers,
Expand All @@ -69,13 +98,6 @@ class ReauthenticateView extends StatelessWidget {
actionButtonLabelOverride: actionButtonLabelOverride,
showPasswordVisibilityToggle: showPasswordVisibilityToggle,
),
listener: (oldState, newState, ctrl) {
if (newState is SignedIn) {
onSignedIn?.call();
}

return false;
},
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class ReauthenticateDialog extends StatelessWidget {
/// A callback that is being called when the user has successfully signed in.
final VoidCallback? onSignedIn;

/// {@macro ui.auth.views.reauthenticate_view.on_phone_verified}
final VoidCallback? onPhoneVerfifed;

/// A label that would be used for the "Sign in" button.
final String? actionButtonLabelOverride;

Expand All @@ -34,6 +37,7 @@ class ReauthenticateDialog extends StatelessWidget {
this.auth,
this.onSignedIn,
this.actionButtonLabelOverride,
this.onPhoneVerfifed,
});

@override
Expand All @@ -47,6 +51,7 @@ class ReauthenticateDialog extends StatelessWidget {
auth: auth,
providers: providers,
onSignedIn: onSignedIn,
onPhoneVerfifed: onPhoneVerfifed,
actionButtonLabelOverride: actionButtonLabelOverride,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Future<void> sendSMS(WidgetTester tester, String phoneNumber) async {

void main() {
const labels = DefaultLocalizations();
setUpTests();

group('PhoneInputScreen', () {
testWidgets('allows to pick country code', (tester) async {
Expand Down Expand Up @@ -98,8 +99,8 @@ void main() {

await completer.future;

final codes = await getVerificationCodes();
expect(codes['+1555555555'], isNotEmpty);
final code = await getVerificationCode('+1555555555');
expect(code, isNotEmpty);
},
);

Expand Down Expand Up @@ -149,14 +150,14 @@ void main() {
final smsCodeInput = find.byType(SMSCodeInput);
expect(smsCodeInput, findsOneWidget);

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

await tester.tap(smsCodeInput);

await tester.enterText(smsCodeInput, invalidCode);
await tester.pumpAndSettle();
await tester.testTextInput.receiveAction(TextInputAction.done);
await completer.future;

Expand Down Expand Up @@ -192,8 +193,7 @@ void main() {
await sendSMS(tester, '555555557');

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

await tester.tap(smsCodeInput);

Expand All @@ -209,4 +209,79 @@ void main() {
},
);
});

group('showReauthenticateDialog', () {
testWidgets(
'can reauthenticate using phone number',
(tester) async {
final credCompleter = Completer<fba.PhoneAuthCredential>();

await auth.verifyPhoneNumber(
phoneNumber: '+1555555558',
verificationCompleted: credCompleter.complete,
verificationFailed: credCompleter.completeError,
codeSent: (verificationId, _) async {
final code = await getVerificationCode('+1555555558');

final credential = fba.PhoneAuthProvider.credential(
verificationId: verificationId,
smsCode: code,
);

credCompleter.complete(credential);
},
codeAutoRetrievalTimeout: (_) {},
);

final cred = await credCompleter.future;
await auth.signInWithCredential(cred);

final reauthenticationCompleter = Completer<void>();
// emulator doesn't return a new sms code if the same phone number is
// used within 30(?) seconds.
await Future.delayed(const Duration(seconds: 30));
bool onPhoneVerifiedCalled = false;

await render(
tester,
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
showReauthenticateDialog(
context: context,
providers: [PhoneAuthProvider()],
onSignedIn: () => reauthenticationCompleter.complete(),
onPhoneVerfifed: () => onPhoneVerifiedCalled = true,
);
},
child: const Text('Reauthenticate'),
);
}),
);

await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();

await tester.tap(find.text('Sign in with phone'));
await tester.pumpAndSettle();

await sendSMS(tester, '555555558');

final smsCodeInput = find.byType(SMSCodeInput);
final code = await getVerificationCode('+1555555558');

await tester.tap(smsCodeInput);
await tester.enterText(smsCodeInput, code);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();

final future = reauthenticationCompleter.future.timeout(
const Duration(seconds: 5),
);

expect(future, completes);
expect(onPhoneVerifiedCalled, isTrue);
},
);
});
}
46 changes: 26 additions & 20 deletions tests/integration_test/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import 'dart:async';
import 'dart:convert';
import 'dart:io';

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

Future<http.Response> retry(Future<http.Response> Function() fn) async {
Future<T> retry<T>(Future<T> Function() fn, [int maxAttempts = 5]) async {
var delay = const Duration(milliseconds: 100);
int attempts = 0;
int maxAttempts = 5;

final completer = Completer<http.Response>();
final completer = Completer<T>();

await Future.doWhile(() async {
try {
Expand All @@ -101,8 +99,8 @@ Future<http.Response> retry(Future<http.Response> Function() fn) async {
return false;
}

stdout.writeln('Request failed: $e');
stdout.writeln('retrying in $delay');
debugPrint('Request failed: $e');
debugPrint('retrying in $delay');
await Future.delayed(delay);
delay *= 2;
attempts++;
Expand All @@ -122,24 +120,32 @@ Future<void> deleteAllAccounts() async {
if (res.statusCode != 200) throw Exception('Delete failed');
}

Future<Map<String, String>> getVerificationCodes() async {
Future<String> getVerificationCode(String phoneNumber) async {
final id = DefaultFirebaseOptions.currentPlatform.projectId;
final uriString =
'http://$testEmulatorHost:9099/emulator/v1/projects/$id/verificationCodes';
final res = await retry(() => http.get(Uri.parse(uriString)));

final body = json.decode(res.body);
final codes = (body['verificationCodes'] as List).fold<Map<String, String>>(
{},
(acc, value) {
return {
...acc,
value['phoneNumber']: value['code'],
};
},
);
final code = await retry(() async {
final res = await http.get(Uri.parse(uriString));
final body = json.decode(res.body);

final codes = (body['verificationCodes'] as List).fold<Map<String, String>>(
{},
(acc, value) {
return {
...acc,
value['phoneNumber']: value['code'],
};
},
);

if (codes[phoneNumber] == null) {
throw Exception('Code not found');
}

return codes[phoneNumber]!;
}, 6);

return codes;
return code;
}

Future<CollectionReference<T>> clearCollection<T>(
Expand Down