Complete verification links and preserve verified login email (#60)
This commit is contained in:
1 parent
30f06c2f8f
commit
5f3b443e7b
25 files changed
+270
-32
No files matched your search
@@ -19,7 +19,8 @@ import 'validation.dart';
|
||||
import '../generated/i18n/app_localizations.dart';
|
||||
|
||||
class AuthForm extends StatefulWidget {
|
||||
const AuthForm({super.key});
|
||||
const AuthForm({super.key, this.email});
|
||||
final String? email;
|
||||
|
||||
@override
|
||||
State<AuthForm> createState() => _AuthFormState();
|
||||
@@ -33,7 +34,7 @@ class _AuthFormState extends State<AuthForm> {
|
||||
bool _showPassword = false;
|
||||
bool _showConfirmation = false;
|
||||
bool _submitted = false;
|
||||
final _email = TextEditingController();
|
||||
late final _email = TextEditingController(text: widget.email);
|
||||
final _name = TextEditingController();
|
||||
|
||||
AuthProvider get _provider =>
|
||||
@@ -264,6 +265,7 @@ class _AuthFormState extends State<AuthForm> {
|
||||
key: const ValueKey('password'),
|
||||
enabled: !state.isLoading,
|
||||
controller: _password,
|
||||
autofocus: widget.email?.isNotEmpty ?? false,
|
||||
obscureText: !_showPassword,
|
||||
autofillHints: [
|
||||
_signUp
|
||||
|
||||
@@ -10,7 +10,8 @@ import 'form.dart';
|
||||
import '../generated/i18n/app_localizations.dart';
|
||||
|
||||
class AuthPage extends StatefulWidget {
|
||||
const AuthPage({super.key});
|
||||
const AuthPage({super.key, this.email});
|
||||
final String? email;
|
||||
|
||||
@override
|
||||
State<AuthPage> createState() => _AuthPageState();
|
||||
@@ -56,7 +57,7 @@ class _AuthPageState extends State<AuthPage> {
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 440),
|
||||
child: AuthForm(key: _formKey),
|
||||
child: AuthForm(key: _formKey, email: widget.email),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"verificationRequest": "Send a new verification email",
|
||||
"verificationRetry": "Retry verification",
|
||||
"verificationBack": "Back to login",
|
||||
"verificationContinue": "Continue to login",
|
||||
"verificationHint": "No email? Check your spam folder or request another below.",
|
||||
"verificationDemoLink": "Continue to confirmation",
|
||||
"verificationStep": "CONFIRM YOUR EMAIL",
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"verificationRequest": "Отправить новое письмо",
|
||||
"verificationRetry": "Повторить подтверждение",
|
||||
"verificationBack": "Вернуться ко входу",
|
||||
"verificationContinue": "Перейти ко входу",
|
||||
"verificationHint": "Нет письма? Проверьте папку «Спам» или запросите новое ниже.",
|
||||
"verificationDemoLink": "Перейти к подтверждению",
|
||||
"verificationStep": "ПОДТВЕРДИТЕ ПОЧТУ",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_web_plugins/url_strategy.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:sxchnge_analytics/sxchnge_analytics.dart';
|
||||
@@ -18,6 +20,7 @@ import 'routing/router.dart';
|
||||
import 'generated/i18n/app_localizations.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
usePathUrlStrategy();
|
||||
final binding = WidgetsFlutterBinding.ensureInitialized();
|
||||
final locale = await LocaleProvider.load(binding.platformDispatcher.locales);
|
||||
const apiUrl = String.fromEnvironment('SXCHNGE_API_URL');
|
||||
|
||||
@@ -135,7 +135,9 @@ GoRouter createAppRouter({bool Function()? authenticated}) {
|
||||
GoRoute(
|
||||
path: '/',
|
||||
name: AppRoutes.login,
|
||||
builder: (context, state) => const AuthPage(),
|
||||
builder: (context, state) => AuthPage(
|
||||
email: state.extra is String ? state.extra as String : null,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/verification-sent',
|
||||
|
||||
@@ -318,15 +318,21 @@ class _VerificationPageState extends State<VerificationPage> {
|
||||
if (verified) ...[
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(
|
||||
onPressed: () => context.go('/accounts'),
|
||||
child: Text(l.wEnter),
|
||||
key: const ValueKey('verification-continue'),
|
||||
onPressed: () => context.goNamed(
|
||||
AppRoutes.login,
|
||||
extra: state.result?.verifiedEmail,
|
||||
),
|
||||
child: Text(l.verificationContinue),
|
||||
),
|
||||
],
|
||||
if (!verified) ...[
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
onPressed: () => context.goNamed(AppRoutes.login),
|
||||
child: Text(l.verificationBack),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
onPressed: () => context.goNamed(AppRoutes.login),
|
||||
child: Text(l.verificationBack),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -185,7 +185,7 @@ packages:
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
|
||||
@@ -14,6 +14,8 @@ dependencies:
|
||||
path: ../packages/sxchnge_analytics
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_web_plugins:
|
||||
sdk: flutter
|
||||
sxchnge_auth:
|
||||
path: ../packages/sxchnge_auth
|
||||
sxchnge_theme:
|
||||
|
||||
@@ -145,8 +145,8 @@ void main() {
|
||||
find.byKey(const ValueKey('verification-verified')),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.ensureVisible(find.text('Back to login'));
|
||||
await tester.tap(find.text('Back to login'));
|
||||
await tester.ensureVisible(find.text('Continue to login'));
|
||||
await tester.tap(find.text('Continue to login'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(const ValueKey('confirmation')), findsNothing);
|
||||
expect(find.text('example-password'), findsNothing);
|
||||
|
||||
@@ -42,6 +42,67 @@ Future<GoRouter> openRoute(
|
||||
}
|
||||
|
||||
void main() {
|
||||
for (final language in ['en', 'ru']) {
|
||||
testWidgets(
|
||||
'verified link keeps success visible and prefills login ($language)',
|
||||
(tester) async {
|
||||
tester.platformDispatcher.defaultRouteNameTestValue =
|
||||
'/verify-email?token=demo-valid';
|
||||
addTearDown(tester.platformDispatcher.clearDefaultRouteNameTestValue);
|
||||
await tester.pumpWidget(
|
||||
SxchngeApp(
|
||||
localeProvider: LocaleProvider(initialLocale: Locale(language)),
|
||||
verificationService: const DummyVerificationService(
|
||||
delay: Duration.zero,
|
||||
verifiedEmail: 'private-alias@example.com',
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.byKey(const ValueKey('verification-verified')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byKey(const ValueKey('email')), findsNothing);
|
||||
final router = GoRouter.of(
|
||||
tester.element(find.byType(VerificationPage)),
|
||||
);
|
||||
expect(router.routeInformationProvider.value.uri.path, '/verify-email');
|
||||
await tester.ensureVisible(
|
||||
find.byKey(const ValueKey('verification-continue')),
|
||||
);
|
||||
await tester.tap(find.byKey(const ValueKey('verification-continue')));
|
||||
await tester.pumpAndSettle();
|
||||
final email = tester.widget<TextFormField>(
|
||||
find.byKey(const ValueKey('email')),
|
||||
);
|
||||
expect(email.controller!.text, 'private-alias@example.com');
|
||||
final password = tester.state<EditableTextState>(
|
||||
find.descendant(
|
||||
of: find.byKey(const ValueKey('password')),
|
||||
matching: find.byType(EditableText),
|
||||
),
|
||||
);
|
||||
expect(password.currentTextEditingValue.text, isEmpty);
|
||||
expect(password.widget.focusNode.hasFocus, isTrue);
|
||||
expect(router.routeInformationProvider.value.uri.toString(), '/');
|
||||
final restored = router.routeInformationParser.restoreRouteInformation(
|
||||
router.routerDelegate.currentConfiguration,
|
||||
)!;
|
||||
expect(
|
||||
restored.state.toString(),
|
||||
isNot(contains('private-alias@example.com')),
|
||||
);
|
||||
expect(restored.state.toString(), isNot(contains('demo-valid')));
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('email')),
|
||||
'changed@example.com',
|
||||
);
|
||||
expect(email.controller!.text, 'changed@example.com');
|
||||
expect(find.byKey(const ValueKey('confirmation')), findsNothing);
|
||||
},
|
||||
);
|
||||
}
|
||||
testWidgets('unknown deep link redirects to canonical login URL', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
Reference in new issue
Block a user