Skip to content

Commit 6ead9e8

Browse files
chrisbobbegnprice
authored andcommitted
startup: Add beta-complete dialog
Fixes zulip#1603.
1 parent 0190b1f commit 6ead9e8

File tree

6 files changed

+118
-0
lines changed

6 files changed

+118
-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: 84 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,83 @@ 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, {required String url, required String label}) =>
155+
TextButton(
156+
onPressed: () {
157+
Navigator.pop(context);
158+
PlatformActions.launchUrl(context,
159+
Uri.parse(url));
160+
},
161+
child: _dialogActionText(label));
162+
163+
@override
164+
Widget build(BuildContext context) {
165+
final message = 'Thanks for being a beta tester of the new Zulip app!'
166+
' This app became the main Zulip mobile app in June 2025,'
167+
' and this beta version is no longer maintained.'
168+
' We recommend uninstalling this beta after switching'
169+
' to the main Zulip app, in order to get the latest features'
170+
' and bug fixes.';
171+
172+
return AlertDialog(
173+
title: Text('Time to switch to the new app'),
174+
content: SingleChildScrollView(child: Text(message)),
175+
actions: [
176+
TextButton(
177+
onPressed: () => Navigator.pop(context),
178+
child: _dialogActionText('Got it')),
179+
...(switch (defaultTargetPlatform) {
180+
TargetPlatform.android => [
181+
_linkButton(context,
182+
url: 'https://github.com/zulip/zulip-flutter/releases/latest',
183+
label: 'Download official APKs (less common)'),
184+
_linkButton(context,
185+
url: 'https://play.google.com/store/apps/details?id=com.zulipmobile',
186+
label: 'Open Google Play Store'),
187+
],
188+
TargetPlatform.iOS => [
189+
_linkButton(context,
190+
url: 'https://apps.apple.com/app/zulip/id1203036395',
191+
label: 'Open App Store'),
192+
],
193+
TargetPlatform.macOS || TargetPlatform.fuchsia
194+
|| TargetPlatform.linux || TargetPlatform.windows => throw UnimplementedError(),
195+
}),
196+
]);
197+
}
198+
}

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)