Skip to content

Commit fd5138b

Browse files
committed
startup: Add beta-complete dialog
Fixes zulip#1603.
1 parent cd53b81 commit fd5138b

File tree

6 files changed

+122
-0
lines changed

6 files changed

+122
-0
lines changed

lib/widgets/app.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ class _ZulipAppState extends State<ZulipApp> with WidgetsBindingObserver {
160160
void initState() {
161161
super.initState();
162162
WidgetsBinding.instance.addObserver(this);
163+
164+
// On every startup is fine; the goal is to be assertive but stop short of a
165+
// rug-pull where we just disable all the app's features.
166+
BetaCompleteDialog.show();
163167
}
164168

165169
@override

lib/widgets/dialog.dart

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import 'dart:async';
2+
3+
import 'package:flutter/foundation.dart';
14
import 'package:flutter/material.dart';
25

36
import '../generated/l10n/zulip_localizations.dart';
47
import 'actions.dart';
8+
import 'app.dart';
59

610
Widget _dialogActionText(String text) {
711
return Text(
@@ -112,3 +116,87 @@ DialogStatus<bool> showSuggestedActionDialog({
112116
]));
113117
return DialogStatus(future);
114118
}
119+
120+
bool debugDisableBetaCompleteDialog = false;
121+
122+
/// A brief dialog box saying that this beta channel has ended,
123+
/// offering a way to get the app from prod.
124+
///
125+
/// Shown on every startup.
126+
class BetaCompleteDialog extends StatelessWidget {
127+
const BetaCompleteDialog._();
128+
129+
static void show() async {
130+
if (debugDisableBetaCompleteDialog) return;
131+
132+
final navigator = await ZulipApp.navigator;
133+
final context = navigator.context;
134+
assert(context.mounted);
135+
if (!context.mounted) return; // TODO(linter): this is impossible as there's no actual async gap, but the use_build_context_synchronously lint doesn't see that
136+
137+
switch (defaultTargetPlatform) {
138+
case TargetPlatform.android:
139+
case TargetPlatform.iOS:
140+
break;
141+
case TargetPlatform.macOS:
142+
case TargetPlatform.fuchsia:
143+
case TargetPlatform.linux:
144+
case TargetPlatform.windows:
145+
// Do nothing on these unsupported platforms.
146+
return;
147+
}
148+
149+
unawaited(showDialog(
150+
context: context,
151+
builder: (BuildContext context) => BetaCompleteDialog._()));
152+
}
153+
154+
Widget _linkButton(BuildContext context, {
155+
required String url,
156+
required String label,
157+
}) {
158+
return TextButton(
159+
onPressed: () {
160+
Navigator.pop(context);
161+
PlatformActions.launchUrl(context,
162+
Uri.parse(url));
163+
},
164+
child: _dialogActionText(label));
165+
}
166+
167+
@override
168+
Widget build(BuildContext context) {
169+
final message = 'Thanks for being a beta tester of the new Zulip app!'
170+
' This app became the main Zulip mobile app in June 2025,'
171+
' and this beta version is no longer maintained.'
172+
' We recommend uninstalling this beta after switching'
173+
' to the main Zulip app, in order to get the latest features'
174+
' and bug fixes.';
175+
176+
return AlertDialog(
177+
title: Text('Time to switch to the new app'),
178+
content: SingleChildScrollView(child: Text(message)),
179+
actions: [
180+
TextButton(
181+
onPressed: () => Navigator.pop(context),
182+
child: _dialogActionText('Got it')),
183+
...(switch (defaultTargetPlatform) {
184+
TargetPlatform.android => [
185+
_linkButton(context,
186+
url: 'https://github.com/zulip/zulip-flutter/releases/latest',
187+
label: 'Download official APKs (less common)'),
188+
_linkButton(context,
189+
url: 'https://play.google.com/store/apps/details?id=com.zulipmobile',
190+
label: 'Open Google Play Store'),
191+
],
192+
TargetPlatform.iOS => [
193+
_linkButton(context,
194+
url: 'https://apps.apple.com/app/zulip/id1203036395',
195+
label: 'Open App Store'),
196+
],
197+
TargetPlatform.macOS || TargetPlatform.fuchsia
198+
|| TargetPlatform.linux || TargetPlatform.windows => throw UnimplementedError(),
199+
}),
200+
]);
201+
}
202+
}

test/notifications/open_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'package:zulip/model/narrow.dart';
1313
import 'package:zulip/notifications/open.dart';
1414
import 'package:zulip/notifications/receive.dart';
1515
import 'package:zulip/widgets/app.dart';
16+
import 'package:zulip/widgets/dialog.dart';
1617
import 'package:zulip/widgets/home.dart';
1718
import 'package:zulip/widgets/message_list.dart';
1819
import 'package:zulip/widgets/page.dart';
@@ -76,6 +77,8 @@ void main() {
7677
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
7778

7879
Future<void> init({bool addSelfAccount = true}) async {
80+
debugDisableBetaCompleteDialog = true;
81+
addTearDown(() => debugDisableBetaCompleteDialog = false);
7982
if (addSelfAccount) {
8083
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
8184
}

test/widgets/app_test.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:zulip/log.dart';
77
import 'package:zulip/model/actions.dart';
88
import 'package:zulip/model/database.dart';
99
import 'package:zulip/widgets/app.dart';
10+
import 'package:zulip/widgets/dialog.dart';
1011
import 'package:zulip/widgets/home.dart';
1112
import 'package:zulip/widgets/page.dart';
1213

@@ -27,6 +28,8 @@ void main() {
2728
late List<Route<dynamic>> pushedRoutes = [];
2829

2930
Future<void> prepare(WidgetTester tester) async {
31+
debugDisableBetaCompleteDialog = true;
32+
addTearDown(() => debugDisableBetaCompleteDialog = false);
3033
addTearDown(testBinding.reset);
3134

3235
pushedRoutes = [];
@@ -64,6 +67,8 @@ void main() {
6467
late List<Route<void>> poppedRoutes;
6568

6669
Future<void> prepare(WidgetTester tester) async {
70+
debugDisableBetaCompleteDialog = true;
71+
addTearDown(() => debugDisableBetaCompleteDialog = false);
6772
addTearDown(testBinding.reset);
6873

6974
pushedRoutes = [];
@@ -279,6 +284,8 @@ void main() {
279284
});
280285

281286
testWidgets('choosing an account clears the navigator stack', (tester) async {
287+
debugDisableBetaCompleteDialog = true;
288+
addTearDown(() => debugDisableBetaCompleteDialog = false);
282289
addTearDown(testBinding.reset);
283290
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
284291
await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot());
@@ -391,6 +398,8 @@ void main() {
391398
});
392399

393400
testWidgets('reportErrorToUserBriefly with details', (tester) async {
401+
debugDisableBetaCompleteDialog = true;
402+
addTearDown(() => debugDisableBetaCompleteDialog = false);
394403
addTearDown(testBinding.reset);
395404
await tester.pumpWidget(const ZulipApp());
396405
const message = 'test error message';
@@ -418,6 +427,8 @@ void main() {
418427
});
419428

420429
Future<void> prepareSnackBarWithDetails(WidgetTester tester, String message, String details) async {
430+
debugDisableBetaCompleteDialog = true;
431+
addTearDown(() => debugDisableBetaCompleteDialog = false);
421432
addTearDown(testBinding.reset);
422433
await tester.pumpWidget(const ZulipApp());
423434
await tester.pump();
@@ -484,6 +495,8 @@ void main() {
484495
});
485496

486497
testWidgets('reportErrorToUserModally', (tester) async {
498+
debugDisableBetaCompleteDialog = true;
499+
addTearDown(() => debugDisableBetaCompleteDialog = false);
487500
addTearDown(testBinding.reset);
488501
await tester.pumpWidget(const ZulipApp());
489502
const title = 'test title';

test/widgets/home_test.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:zulip/model/store.dart';
88
import 'package:zulip/widgets/about_zulip.dart';
99
import 'package:zulip/widgets/app.dart';
1010
import 'package:zulip/widgets/app_bar.dart';
11+
import 'package:zulip/widgets/dialog.dart';
1112
import 'package:zulip/widgets/home.dart';
1213
import 'package:zulip/widgets/icons.dart';
1314
import 'package:zulip/widgets/inbox.dart';
@@ -48,6 +49,8 @@ void main () {
4849
..onPopped = ((route, prevRoute) => lastPoppedRoute = route);
4950

5051
Future<void> prepare(WidgetTester tester) async {
52+
debugDisableBetaCompleteDialog = true;
53+
addTearDown(() => debugDisableBetaCompleteDialog = false);
5154
addTearDown(testBinding.reset);
5255
topRoute = null;
5356
previousTopRoute = null;
@@ -272,6 +275,8 @@ void main () {
272275
});
273276

274277
testWidgets('menu buttons dismiss the menu', (tester) async {
278+
debugDisableBetaCompleteDialog = true;
279+
addTearDown(() => debugDisableBetaCompleteDialog = false);
275280
addTearDown(testBinding.reset);
276281
topRoute = null;
277282
previousTopRoute = null;
@@ -328,6 +333,8 @@ void main () {
328333
}
329334

330335
Future<void> prepare(WidgetTester tester) async {
336+
debugDisableBetaCompleteDialog = true;
337+
addTearDown(() => debugDisableBetaCompleteDialog = false);
331338
addTearDown(testBinding.reset);
332339
topRoute = null;
333340
previousTopRoute = null;
@@ -521,6 +528,8 @@ void main () {
521528
});
522529

523530
testWidgets('logging out while still loading', (tester) async {
531+
debugDisableBetaCompleteDialog = true;
532+
addTearDown(() => debugDisableBetaCompleteDialog = false);
524533
// Regression test for: https://github.com/zulip/zulip-flutter/issues/1219
525534
addTearDown(testBinding.reset);
526535
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
@@ -537,6 +546,8 @@ void main () {
537546
});
538547

539548
testWidgets('logging out after fully loaded', (tester) async {
549+
debugDisableBetaCompleteDialog = true;
550+
addTearDown(() => debugDisableBetaCompleteDialog = false);
540551
// Regression test for: https://github.com/zulip/zulip-flutter/issues/1219
541552
addTearDown(testBinding.reset);
542553
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());

test/widgets/login_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:zulip/model/binding.dart';
1212
import 'package:zulip/model/database.dart';
1313
import 'package:zulip/model/localizations.dart';
1414
import 'package:zulip/widgets/app.dart';
15+
import 'package:zulip/widgets/dialog.dart';
1516
import 'package:zulip/widgets/home.dart';
1617
import 'package:zulip/widgets/login.dart';
1718
import 'package:zulip/widgets/page.dart';
@@ -83,6 +84,8 @@ void main() {
8384

8485
Future<void> prepare(WidgetTester tester,
8586
GetServerSettingsResult serverSettings) async {
87+
debugDisableBetaCompleteDialog = true;
88+
addTearDown(() => debugDisableBetaCompleteDialog = false);
8689
addTearDown(testBinding.reset);
8790

8891
connection = testBinding.globalStore.apiConnection(

0 commit comments

Comments
 (0)