Skip to content

Commit 3d0f90a

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 3d0f90a

File tree

3 files changed

+94
-1
lines changed

3 files changed

+94
-1
lines changed

assets/l10n/app_en.arb

Lines changed: 15 additions & 0 deletions
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

Lines changed: 35 additions & 1 deletion
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';
@@ -223,7 +225,14 @@ class _AddAccountPageState extends State<AddAccountPage> {
223225
decoration: InputDecoration(
224226
labelText: zulipLocalizations.loginServerUrlInputLabel,
225227
errorText: errorText,
226-
helperText: kLayoutPinningHelperText,
228+
helper: GestureDetector(
229+
onTap: () {
230+
_launchUrl(context, zulipLocalizations);
231+
},
232+
child: Text(
233+
zulipLocalizations.serverURLDocLinkLabel,
234+
style: Theme.of(context).textTheme.bodySmall!
235+
.apply(color: const HSLColor.fromAHSL(1, 200, 1, 0.4).toColor()))),
227236
hintText: 'your-org.zulipchat.com')),
228237
const SizedBox(height: 8),
229238
ElevatedButton(
@@ -235,6 +244,31 @@ class _AddAccountPageState extends State<AddAccountPage> {
235244
}
236245
}
237246

247+
void _launchUrl(BuildContext context, ZulipLocalizations zulipLocalizations) async {
248+
String urlString = 'https://zulip.com/help/logging-in#find-the-zulip-log-in-url';
249+
Future<void> showError(BuildContext context, String? message) {
250+
return showErrorDialog(
251+
context: context,
252+
title: zulipLocalizations.errorUnableToOpenLinkTitle,
253+
message: [
254+
zulipLocalizations.errorLinkCouldNotBeOpened(urlString),
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(urlString));
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

Lines changed: 44 additions & 0 deletions
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,46 @@ 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+
const url = 'https://zulip.com/help/logging-in#find-the-zulip-log-in-url';
159+
160+
Future<Finder> findHelperText(WidgetTester tester) async {
161+
return find.byWidgetPredicate((widget) =>
162+
widget is GestureDetector &&
163+
widget.child is Text &&
164+
(widget.child as Text).data == zulipLocalizations.serverURLDocLinkLabel);
165+
}
166+
167+
testWidgets('launches URL when helper text is tapped', (WidgetTester tester) async {
168+
await prepareAddAccountPage(tester);
169+
final helper = await findHelperText(tester);
170+
await tester.tap(helper);
171+
172+
check(testBinding.takeLaunchUrlCalls())
173+
.single
174+
.equals((url: Uri.parse(url), mode: LaunchMode.platformDefault));
175+
});
176+
177+
testWidgets('shows error dialog when URL fails to open', (WidgetTester tester) async {
178+
await prepareAddAccountPage(tester);
179+
testBinding.launchUrlResult = false;
180+
final helper = await findHelperText(tester);
181+
await tester.tap(helper);
182+
await tester.pump();
183+
184+
checkErrorDialog(tester,
185+
expectedTitle: zulipLocalizations.errorUnableToOpenLinkTitle,
186+
expectedMessage: zulipLocalizations.errorLinkCouldNotBeOpened(url));
187+
});
188+
});
145189
}

0 commit comments

Comments
 (0)