diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index c33acae5c86..9ff0d109534 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -289,6 +289,10 @@ "@loginServerUrlInputLabel": { "description": "Input label in login page for Zulip server URL entry." }, + "serverUrlDocLinkLabel": "What's this?", + "@serverUrlDocLinkLabel": { + "description": "Link to doc to help users understand what a server URL is and how to find theirs." + }, "errorUnableToOpenLinkTitle": "Unable to open link", "@errorUnableToOpenLinkTitle": { "description": "Error title when a link fails to open." diff --git a/lib/widgets/login.dart b/lib/widgets/login.dart index 2c316d08e16..aaa600456b1 100644 --- a/lib/widgets/login.dart +++ b/lib/widgets/login.dart @@ -9,6 +9,7 @@ import '../model/store.dart'; import 'app.dart'; import 'dialog.dart'; import 'input.dart'; +import 'launch_url.dart'; import 'page.dart'; import 'store.dart'; @@ -101,6 +102,8 @@ class ServerUrlTextEditingController extends TextEditingController { class AddAccountPage extends StatefulWidget { const AddAccountPage({super.key}); + static const String serverUrlHelpUrl = 'https://zulip.com/help/logging-in#find-the-zulip-log-in-url'; + static Route buildRoute() { return _LoginSequenceRoute(page: const AddAccountPage()); } @@ -207,7 +210,6 @@ class _AddAccountPageState extends State { child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 400), child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - // TODO(#109) Link to doc about what a "server URL" is and how to find it // TODO(#111) Perhaps give tappable realm URL suggestions based on text typed so far TextField( controller: _controller, @@ -223,7 +225,14 @@ class _AddAccountPageState extends State { decoration: InputDecoration( labelText: zulipLocalizations.loginServerUrlInputLabel, errorText: errorText, - helperText: kLayoutPinningHelperText, + helper: GestureDetector( + onTap: () { + launchUrlWithoutRealm(context, Uri.parse(AddAccountPage.serverUrlHelpUrl)); + }, + child: Text( + zulipLocalizations.serverUrlDocLinkLabel, + style: Theme.of(context).textTheme.bodySmall! + .apply(color: const HSLColor.fromAHSL(1, 200, 1, 0.4).toColor()))), hintText: 'your-org.zulipchat.com')), const SizedBox(height: 8), ElevatedButton( diff --git a/test/widgets/login_test.dart b/test/widgets/login_test.dart index f53af3544b6..13a102454f0 100644 --- a/test/widgets/login_test.dart +++ b/test/widgets/login_test.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/zulip_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; +import 'package:url_launcher/url_launcher.dart'; import 'package:zulip/api/route/account.dart'; import 'package:zulip/api/route/realm.dart'; import 'package:zulip/model/localizations.dart'; @@ -13,6 +14,7 @@ import '../api/fake_api.dart'; import '../example_data.dart' as eg; import '../model/binding.dart'; import '../stdlib_checks.dart'; +import 'dialog_checks.dart'; void main() { TestZulipBinding.ensureInitialized(); @@ -142,4 +144,41 @@ void main() { // TODO test handling failure in fetchApiKey request // TODO test _inProgress logic }); + + group('Server URL Helper Text', () { + Future prepareAddAccountPage(WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp( + localizationsDelegates: ZulipLocalizations.localizationsDelegates, + supportedLocales: ZulipLocalizations.supportedLocales, + home: AddAccountPage(), + )); + } + + final zulipLocalizations = GlobalLocalizations.zulipLocalizations; + + Future findHelperText(WidgetTester tester) async { + return find.text(zulipLocalizations.serverUrlDocLinkLabel); + } + + testWidgets('launches URL when helper text is tapped', (tester) async { + await prepareAddAccountPage(tester); + final helper = await findHelperText(tester); + await tester.tap(helper); + + check(testBinding.takeLaunchUrlCalls()) + .single.equals((url: Uri.parse(AddAccountPage.serverUrlHelpUrl), mode: LaunchMode.platformDefault)); + }); + + testWidgets('shows error dialog when URL fails to open', (tester) async { + await prepareAddAccountPage(tester); + testBinding.launchUrlResult = false; + final helper = await findHelperText(tester); + await tester.tap(helper); + await tester.pump(); + + checkErrorDialog(tester, + expectedTitle: zulipLocalizations.errorUnableToOpenLinkTitle, + expectedMessage: zulipLocalizations.errorLinkCouldNotBeOpened(AddAccountPage.serverUrlHelpUrl)); + }); + }); }