Show accepted signup delivery without a second send step (#62)
This commit is contained in:
1 parent
e84d2d1ad3
commit
3af60f38b1
19 files changed
+421
-69
No files matched your search
@@ -99,10 +99,11 @@ class _AuthFormState extends State<AuthForm> {
|
||||
return;
|
||||
}
|
||||
final email = _email.text.trim();
|
||||
final delivery = signup.state.result!.delivery;
|
||||
TextInput.finishAutofillContext();
|
||||
_password.clear();
|
||||
_confirmation.clear();
|
||||
context.goNamed(AppRoutes.verificationSent, extra: email);
|
||||
context.goNamed(AppRoutes.verificationSent, extra: delivery ?? email);
|
||||
} else {
|
||||
final login = context.read<LoginProvider>();
|
||||
await login.login(
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:sxchnge_auth/sxchnge_auth.dart';
|
||||
|
||||
import 'package:sxchnge_wallet/sxchnge_wallet.dart';
|
||||
|
||||
import '../auth/page.dart';
|
||||
@@ -144,6 +146,9 @@ GoRouter createAppRouter({bool Function()? authenticated}) {
|
||||
name: AppRoutes.verificationSent,
|
||||
builder: (context, state) => VerificationPage(
|
||||
email: state.extra is String ? state.extra as String : null,
|
||||
delivery: state.extra is VerificationDelivery
|
||||
? state.extra as VerificationDelivery
|
||||
: null,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
|
||||
@@ -15,9 +15,10 @@ import '../routing/routes.dart';
|
||||
import '../generated/i18n/app_localizations.dart';
|
||||
|
||||
class VerificationPage extends StatefulWidget {
|
||||
const VerificationPage({super.key, this.email, this.token});
|
||||
const VerificationPage({super.key, this.email, this.token, this.delivery});
|
||||
final String? email;
|
||||
final String? token;
|
||||
final VerificationDelivery? delivery;
|
||||
|
||||
@override
|
||||
State<VerificationPage> createState() => _VerificationPageState();
|
||||
@@ -25,9 +26,12 @@ class VerificationPage extends StatefulWidget {
|
||||
|
||||
class _VerificationPageState extends State<VerificationPage> {
|
||||
final _form = GlobalKey<FormState>();
|
||||
late final _email = TextEditingController(text: widget.email);
|
||||
late final _email = TextEditingController(
|
||||
text: widget.delivery?.email ?? widget.email,
|
||||
);
|
||||
late final _provider = VerificationProvider(
|
||||
context.read<VerificationService>(),
|
||||
delivery: widget.delivery,
|
||||
);
|
||||
final _emailFocus = FocusNode();
|
||||
String? _recipient;
|
||||
@@ -44,7 +48,7 @@ class _VerificationPageState extends State<VerificationPage> {
|
||||
if (!mounted) return;
|
||||
if (widget.token != null) {
|
||||
_provider.confirm(widget.token!);
|
||||
} else if (_email.text.isNotEmpty) {
|
||||
} else if (widget.delivery == null && _email.text.isNotEmpty) {
|
||||
_send();
|
||||
}
|
||||
});
|
||||
@@ -58,14 +62,26 @@ class _VerificationPageState extends State<VerificationPage> {
|
||||
@override
|
||||
void didUpdateWidget(covariant VerificationPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.token == widget.token) return;
|
||||
context.read<JourneyRecorder>().abandon(JourneyExit.navigation);
|
||||
_provider.reset();
|
||||
if (oldWidget.token == widget.token &&
|
||||
oldWidget.email == widget.email &&
|
||||
identical(oldWidget.delivery, widget.delivery)) {
|
||||
return;
|
||||
}
|
||||
if (oldWidget.token != widget.token) {
|
||||
context.read<JourneyRecorder>().abandon(JourneyExit.navigation);
|
||||
}
|
||||
_provider.reset(delivery: widget.delivery);
|
||||
_lastWasSend = false;
|
||||
_validated = false;
|
||||
_email.clear();
|
||||
_recipient = null;
|
||||
_email.text = widget.delivery?.email ?? widget.email ?? '';
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && widget.token != null) _provider.confirm(widget.token!);
|
||||
if (!mounted) return;
|
||||
if (widget.token != null) {
|
||||
_provider.confirm(widget.token!);
|
||||
} else if (widget.delivery == null && _email.text.isNotEmpty) {
|
||||
_send();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ packages:
|
||||
source: hosted
|
||||
version: "18.0.1"
|
||||
http:
|
||||
dependency: transitive
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
|
||||
@@ -31,6 +31,7 @@ dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^6.0.0
|
||||
http: ^1.6.0
|
||||
flutter:
|
||||
generate: true
|
||||
uses-material-design: true
|
||||
@@ -42,6 +42,44 @@ Future<GoRouter> openRoute(
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('queued signup receipt is transient and updates its recipient', (
|
||||
tester,
|
||||
) async {
|
||||
final router = await openRoute(tester, '/');
|
||||
await tester.pumpAndSettle();
|
||||
router.goNamed(
|
||||
AppRoutes.verificationSent,
|
||||
extra: const VerificationDelivery(
|
||||
email: 'first@example.com',
|
||||
isSimulated: false,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(const ValueKey('verification-sent')), findsOneWidget);
|
||||
expect(find.text('first@example.com'), findsOneWidget);
|
||||
router.goNamed(
|
||||
AppRoutes.verificationSent,
|
||||
extra: const VerificationDelivery(
|
||||
email: 'second@example.com',
|
||||
isSimulated: false,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('first@example.com'), findsNothing);
|
||||
expect(find.text('second@example.com'), findsOneWidget);
|
||||
final restored = router.routeInformationParser.restoreRouteInformation(
|
||||
router.routerDelegate.currentConfiguration,
|
||||
)!;
|
||||
expect(restored.state.toString(), isNot(contains('second@example.com')));
|
||||
expect(restored.uri.toString(), '/verification-sent');
|
||||
router.goNamed(AppRoutes.login);
|
||||
await tester.pumpAndSettle();
|
||||
router.goNamed(AppRoutes.verificationSent);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(const ValueKey('verification-idle')), findsOneWidget);
|
||||
expect(find.text('second@example.com'), findsNothing);
|
||||
});
|
||||
|
||||
for (final language in ['en', 'ru']) {
|
||||
testWidgets(
|
||||
'verified link keeps success visible and prefills login ($language)',
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
|
||||
import 'package:sxchnge_analytics/sxchnge_analytics.dart';
|
||||
|
||||
import 'package:sxchnge_auth/sxchnge_auth.dart';
|
||||
|
||||
import 'package:sxchnge_app/main.dart';
|
||||
import 'package:sxchnge_app/providers/locale.dart';
|
||||
|
||||
class DeliveryBackend extends BackendAuthServices {
|
||||
DeliveryBackend(List<String> requests)
|
||||
: super(
|
||||
baseUri: Uri.parse('https://api.example/api/v1'),
|
||||
clientId: 'web',
|
||||
client: MockClient((request) async {
|
||||
requests.add(request.url.path);
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'data': request.url.path.endsWith('/signup')
|
||||
? {'userId': 'customer'}
|
||||
: {'accepted': true},
|
||||
}),
|
||||
202,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
int sends = 0;
|
||||
bool failResend = true;
|
||||
|
||||
@override
|
||||
Future<VerificationResult> send({
|
||||
required String email,
|
||||
required String locale,
|
||||
}) async {
|
||||
sends++;
|
||||
if (failResend) {
|
||||
throw const VerificationException(VerificationError.internal);
|
||||
}
|
||||
return super.send(email: email, locale: locale);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
for (final language in ['en', 'ru']) {
|
||||
testWidgets(
|
||||
'accepted signup survives an unavailable resend service ($language)',
|
||||
(tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
tester.platformDispatcher.textScaleFactorTestValue = 1.5;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue);
|
||||
final requests = <String>[];
|
||||
final backend = DeliveryBackend(requests);
|
||||
final analytics = DummyAnalyticsService();
|
||||
await tester.pumpWidget(
|
||||
SxchngeApp(
|
||||
backend: backend,
|
||||
analyticsService: analytics,
|
||||
localeProvider: LocaleProvider(initialLocale: Locale(language)),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final signup = find.text(language == 'ru' ? 'Регистрация' : 'Sign up');
|
||||
await tester.ensureVisible(signup);
|
||||
await tester.tap(signup);
|
||||
await tester.pumpAndSettle();
|
||||
for (final entry in {
|
||||
'name': 'Ada Lovelace',
|
||||
'email': 'ada@example.com',
|
||||
'password': 'password preserved',
|
||||
'confirmation': 'password preserved',
|
||||
}.entries) {
|
||||
await tester.enterText(find.byKey(ValueKey(entry.key)), entry.value);
|
||||
}
|
||||
final submit = find.byKey(const ValueKey('submit'));
|
||||
await tester.ensureVisible(submit);
|
||||
await tester.tap(submit);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const ValueKey('verification-sent')), findsOneWidget);
|
||||
expect(find.text('ada@example.com'), findsOneWidget);
|
||||
expect(backend.sends, 0);
|
||||
expect(requests, ['/api/v1/accounts/signup']);
|
||||
final router = GoRouter.of(tester.element(find.byType(Scaffold)));
|
||||
final restored = router.routeInformationParser.restoreRouteInformation(
|
||||
router.routerDelegate.currentConfiguration,
|
||||
)!;
|
||||
expect(restored.uri.toString(), '/verification-sent');
|
||||
expect(restored.state.toString(), isNot(contains('ada@example.com')));
|
||||
expect(
|
||||
analytics.events.where(
|
||||
(event) => event.name == 'auth_signup_succeeded',
|
||||
),
|
||||
hasLength(1),
|
||||
);
|
||||
expect(
|
||||
analytics.events.where(
|
||||
(event) => event.name == 'auth_email_delivery_succeeded',
|
||||
),
|
||||
hasLength(1),
|
||||
);
|
||||
|
||||
final resend = find.byKey(const ValueKey('verification-resend'));
|
||||
await tester.ensureVisible(resend);
|
||||
await tester.tap(resend);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.byKey(const ValueKey('verification-error')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(backend.sends, 1);
|
||||
expect(
|
||||
tester
|
||||
.widget<TextFormField>(
|
||||
find.byKey(const ValueKey('verification-email')),
|
||||
)
|
||||
.controller!
|
||||
.text,
|
||||
'ada@example.com',
|
||||
);
|
||||
backend.failResend = false;
|
||||
await tester.ensureVisible(resend);
|
||||
await tester.tap(resend);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(const ValueKey('verification-sent')), findsOneWidget);
|
||||
expect(backend.sends, 2);
|
||||
expect(requests.last, '/api/v1/accounts/verification/send');
|
||||
expect(requests, hasLength(2));
|
||||
expect(
|
||||
analytics.events.map((event) => event.journeyId).toSet(),
|
||||
hasLength(1),
|
||||
);
|
||||
expect(
|
||||
analytics.events.map((event) => event.properties).toString(),
|
||||
isNot(contains('ada@example.com')),
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user