Skip to content

Commit f0c82eb

Browse files
login: Link to doc for what "server URL" is and how to find it
- Add informative helper text below the "server URL" field in the login screen. - When tapped, the helper text opens Zulip documentation explaining server URLs and how to find them. - Improves user experience during login by providing clear guidance. - Fixes: #109
1 parent 711bc69 commit f0c82eb

File tree

3 files changed

+90
-2
lines changed

3 files changed

+90
-2
lines changed

assets/l10n/app_en.arb

+15
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,21 @@
289289
"@loginServerUrlInputLabel": {
290290
"description": "Input label in login page for Zulip server URL entry."
291291
},
292+
"serverUrlDocLinkLabel": "What's this?",
293+
"@serverUrlDocLinkLabel": {
294+
"description": "Link to doc to help users understand what a server URL is and how to find theirs."
295+
},
296+
"errorUnableToOpenLinkTitle": "Unable to open link",
297+
"@errorUnableToOpenLinkTitle": {
298+
"description": "Error title when a link fails to open."
299+
},
300+
"errorLinkCouldNotBeOpened": "Link could not be opened: {url}",
301+
"@errorLinkCouldNotBeOpened": {
302+
"description": "Error message when a specific link could not be opened.",
303+
"placeholders": {
304+
"url": {"type": "String", "example": "http://example.com/"}
305+
}
306+
},
292307
"loginHidePassword": "Hide password",
293308
"@loginHidePassword": {
294309
"description": "Icon label for button to hide password in input form."

lib/widgets/login.dart

+36-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter/services.dart';
23
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
34

45
import '../api/exception.dart';
56
import '../api/route/account.dart';
67
import '../api/route/realm.dart';
78
import '../api/route/users.dart';
9+
import '../model/binding.dart';
810
import '../model/store.dart';
911
import 'app.dart';
1012
import 'dialog.dart';
@@ -101,6 +103,8 @@ class ServerUrlTextEditingController extends TextEditingController {
101103
class AddAccountPage extends StatefulWidget {
102104
const AddAccountPage({super.key});
103105

106+
static const String serverUrlHelpUrl = 'https://zulip.com/help/logging-in#find-the-zulip-log-in-url';
107+
104108
static Route<void> buildRoute() {
105109
return _LoginSequenceRoute(page: const AddAccountPage());
106110
}
@@ -207,7 +211,6 @@ class _AddAccountPageState extends State<AddAccountPage> {
207211
child: ConstrainedBox(
208212
constraints: const BoxConstraints(maxWidth: 400),
209213
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
210-
// TODO(#109) Link to doc about what a "server URL" is and how to find it
211214
// TODO(#111) Perhaps give tappable realm URL suggestions based on text typed so far
212215
TextField(
213216
controller: _controller,
@@ -223,7 +226,14 @@ class _AddAccountPageState extends State<AddAccountPage> {
223226
decoration: InputDecoration(
224227
labelText: zulipLocalizations.loginServerUrlInputLabel,
225228
errorText: errorText,
226-
helperText: kLayoutPinningHelperText,
229+
helper: GestureDetector(
230+
onTap: () {
231+
_launchUrl(context);
232+
},
233+
child: Text(
234+
zulipLocalizations.serverUrlDocLinkLabel,
235+
style: Theme.of(context).textTheme.bodySmall!
236+
.apply(color: const HSLColor.fromAHSL(1, 200, 1, 0.4).toColor()))),
227237
hintText: 'your-org.zulipchat.com')),
228238
const SizedBox(height: 8),
229239
ElevatedButton(
@@ -235,6 +245,30 @@ class _AddAccountPageState extends State<AddAccountPage> {
235245
}
236246
}
237247

248+
void _launchUrl(BuildContext context) async {
249+
Future<void> showError(BuildContext context, String? message) {
250+
return showErrorDialog(
251+
context: context,
252+
title: ZulipLocalizations.of(context).errorUnableToOpenLinkTitle,
253+
message: [
254+
ZulipLocalizations.of(context).errorLinkCouldNotBeOpened(AddAccountPage.serverUrlHelpUrl),
255+
if (message != null) message,
256+
].join("\n\n"));
257+
}
258+
259+
bool launched = false;
260+
String? errorMessage;
261+
try {
262+
launched = await ZulipBinding.instance.launchUrl(Uri.parse(AddAccountPage.serverUrlHelpUrl));
263+
} on PlatformException catch (e) {
264+
errorMessage = e.message;
265+
}
266+
if (!launched) {
267+
if (!context.mounted) return;
268+
await showError(context, errorMessage);
269+
}
270+
}
271+
238272
class PasswordLoginPage extends StatefulWidget {
239273
const PasswordLoginPage({super.key, required this.serverSettings});
240274

test/widgets/login_test.dart

+39
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
33
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
44
import 'package:flutter_test/flutter_test.dart';
55
import 'package:http/http.dart' as http;
6+
import 'package:url_launcher/url_launcher.dart';
67
import 'package:zulip/api/route/account.dart';
78
import 'package:zulip/api/route/realm.dart';
89
import 'package:zulip/model/localizations.dart';
@@ -13,6 +14,7 @@ import '../api/fake_api.dart';
1314
import '../example_data.dart' as eg;
1415
import '../model/binding.dart';
1516
import '../stdlib_checks.dart';
17+
import 'dialog_checks.dart';
1618

1719
void main() {
1820
TestZulipBinding.ensureInitialized();
@@ -142,4 +144,41 @@ void main() {
142144
// TODO test handling failure in fetchApiKey request
143145
// TODO test _inProgress logic
144146
});
147+
148+
group('Server URL Helper Text', () {
149+
Future<void> prepareAddAccountPage(WidgetTester tester) async {
150+
await tester.pumpWidget(const MaterialApp(
151+
localizationsDelegates: ZulipLocalizations.localizationsDelegates,
152+
supportedLocales: ZulipLocalizations.supportedLocales,
153+
home: AddAccountPage(),
154+
));
155+
}
156+
157+
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
158+
159+
Future<Finder> findHelperText(WidgetTester tester) async {
160+
return find.text(zulipLocalizations.serverUrlDocLinkLabel);
161+
}
162+
163+
testWidgets('launches URL when helper text is tapped', (tester) async {
164+
await prepareAddAccountPage(tester);
165+
final helper = await findHelperText(tester);
166+
await tester.tap(helper);
167+
168+
check(testBinding.takeLaunchUrlCalls())
169+
.single.equals((url: Uri.parse(AddAccountPage.serverUrlHelpUrl), mode: LaunchMode.platformDefault));
170+
});
171+
172+
testWidgets('shows error dialog when URL fails to open', (tester) async {
173+
await prepareAddAccountPage(tester);
174+
testBinding.launchUrlResult = false;
175+
final helper = await findHelperText(tester);
176+
await tester.tap(helper);
177+
await tester.pump();
178+
179+
checkErrorDialog(tester,
180+
expectedTitle: zulipLocalizations.errorUnableToOpenLinkTitle,
181+
expectedMessage: zulipLocalizations.errorLinkCouldNotBeOpened(AddAccountPage.serverUrlHelpUrl));
182+
});
183+
});
145184
}

0 commit comments

Comments
 (0)