diff --git a/lib/widgets/app.dart b/lib/widgets/app.dart index b1aa763ac8..3c77984f1b 100644 --- a/lib/widgets/app.dart +++ b/lib/widgets/app.dart @@ -160,6 +160,10 @@ class _ZulipAppState extends State with WidgetsBindingObserver { void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); + + // On every startup is fine; the goal is to be assertive but stop short of a + // rug-pull where we just disable all the app's features. + showBetaCompleteDialog(); } @override diff --git a/lib/widgets/dialog.dart b/lib/widgets/dialog.dart index 4d269cddba..154025f1c4 100644 --- a/lib/widgets/dialog.dart +++ b/lib/widgets/dialog.dart @@ -1,7 +1,11 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../generated/l10n/zulip_localizations.dart'; import 'actions.dart'; +import 'app.dart'; Widget _dialogActionText(String text) { return Text( @@ -112,3 +116,75 @@ DialogStatus showSuggestedActionDialog({ ])); return DialogStatus(future); } + +bool debugDisableBetaCompleteDialog = false; +void resetDebugDisableBetaCompleteDialog() { + debugDisableBetaCompleteDialog = false; +} + +/// Show a brief dialog box saying that this beta channel has ended, +/// offering a way to get the app from prod. +void showBetaCompleteDialog() async { + if (debugDisableBetaCompleteDialog) return; + + final navigator = await ZulipApp.navigator; + final context = navigator.context; + assert(context.mounted); + 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 + + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.iOS: + break; + case TargetPlatform.macOS: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + // Do nothing on these unsupported platforms. + return; + } + + final zulipLocalizations = ZulipLocalizations.of(context); + + final message = 'Since Zulip’s new Flutter app has launched, this beta app is no longer maintained. We strongly recommend uninstalling it and switching to the main Zulip application to get the latest features and bug fixes. Thank you for being a beta tester!'; + + unawaited(showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text('Time to switch to the new app'), + content: SingleChildScrollView(child: Text(message)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: _dialogActionText('Got it')), + ...(switch (defaultTargetPlatform) { + TargetPlatform.android => [ + TextButton( + onPressed: () { + Navigator.pop(context); + PlatformActions.launchUrl(context, + Uri.parse('https://github.com/zulip/zulip-flutter/releases/latest')); + }, + child: _dialogActionText('Download official APKs (less common)')), + TextButton( + onPressed: () { + Navigator.pop(context); + PlatformActions.launchUrl(context, + Uri.parse('https://play.google.com/store/apps/details?id=com.zulipmobile')); + }, + child: _dialogActionText('Open Google Play Store')) + ], + TargetPlatform.iOS => [ + TextButton( + onPressed: () { + Navigator.pop(context); + PlatformActions.launchUrl(context, + Uri.parse('https://apps.apple.com/app/zulip/id1203036395')); + }, + child: _dialogActionText('Open App Store')), + ], + TargetPlatform.macOS || TargetPlatform.fuchsia + || TargetPlatform.linux || TargetPlatform.windows => throw UnimplementedError(), + }), + ]))); +} diff --git a/test/notifications/open_test.dart b/test/notifications/open_test.dart index a2c14ca20a..6067cf2071 100644 --- a/test/notifications/open_test.dart +++ b/test/notifications/open_test.dart @@ -13,6 +13,7 @@ import 'package:zulip/model/narrow.dart'; import 'package:zulip/notifications/open.dart'; import 'package:zulip/notifications/receive.dart'; import 'package:zulip/widgets/app.dart'; +import 'package:zulip/widgets/dialog.dart'; import 'package:zulip/widgets/home.dart'; import 'package:zulip/widgets/message_list.dart'; import 'package:zulip/widgets/page.dart'; @@ -76,6 +77,8 @@ void main() { final zulipLocalizations = GlobalLocalizations.zulipLocalizations; Future init({bool addSelfAccount = true}) async { + debugDisableBetaCompleteDialog = true; + addTearDown(resetDebugDisableBetaCompleteDialog); if (addSelfAccount) { await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); } diff --git a/test/widgets/app_test.dart b/test/widgets/app_test.dart index 7b1388dbec..4ebdeaa7ea 100644 --- a/test/widgets/app_test.dart +++ b/test/widgets/app_test.dart @@ -7,6 +7,7 @@ import 'package:zulip/log.dart'; import 'package:zulip/model/actions.dart'; import 'package:zulip/model/database.dart'; import 'package:zulip/widgets/app.dart'; +import 'package:zulip/widgets/dialog.dart'; import 'package:zulip/widgets/home.dart'; import 'package:zulip/widgets/page.dart'; @@ -27,6 +28,8 @@ void main() { late List> pushedRoutes = []; Future prepare(WidgetTester tester) async { + debugDisableBetaCompleteDialog = true; + addTearDown(resetDebugDisableBetaCompleteDialog); addTearDown(testBinding.reset); pushedRoutes = []; @@ -64,6 +67,8 @@ void main() { late List> poppedRoutes; Future prepare(WidgetTester tester) async { + debugDisableBetaCompleteDialog = true; + addTearDown(resetDebugDisableBetaCompleteDialog); addTearDown(testBinding.reset); pushedRoutes = []; @@ -279,6 +284,8 @@ void main() { }); testWidgets('choosing an account clears the navigator stack', (tester) async { + debugDisableBetaCompleteDialog = true; + addTearDown(resetDebugDisableBetaCompleteDialog); addTearDown(testBinding.reset); await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot()); @@ -391,6 +398,8 @@ void main() { }); testWidgets('reportErrorToUserBriefly with details', (tester) async { + debugDisableBetaCompleteDialog = true; + addTearDown(resetDebugDisableBetaCompleteDialog); addTearDown(testBinding.reset); await tester.pumpWidget(const ZulipApp()); const message = 'test error message'; @@ -418,6 +427,8 @@ void main() { }); Future prepareSnackBarWithDetails(WidgetTester tester, String message, String details) async { + debugDisableBetaCompleteDialog = true; + addTearDown(resetDebugDisableBetaCompleteDialog); addTearDown(testBinding.reset); await tester.pumpWidget(const ZulipApp()); await tester.pump(); @@ -484,6 +495,8 @@ void main() { }); testWidgets('reportErrorToUserModally', (tester) async { + debugDisableBetaCompleteDialog = true; + addTearDown(resetDebugDisableBetaCompleteDialog); addTearDown(testBinding.reset); await tester.pumpWidget(const ZulipApp()); const title = 'test title'; diff --git a/test/widgets/home_test.dart b/test/widgets/home_test.dart index 1b5c8ad8b5..48a68f7bb6 100644 --- a/test/widgets/home_test.dart +++ b/test/widgets/home_test.dart @@ -8,6 +8,7 @@ import 'package:zulip/model/store.dart'; import 'package:zulip/widgets/about_zulip.dart'; import 'package:zulip/widgets/app.dart'; import 'package:zulip/widgets/app_bar.dart'; +import 'package:zulip/widgets/dialog.dart'; import 'package:zulip/widgets/home.dart'; import 'package:zulip/widgets/icons.dart'; import 'package:zulip/widgets/inbox.dart'; @@ -48,6 +49,8 @@ void main () { ..onPopped = ((route, prevRoute) => lastPoppedRoute = route); Future prepare(WidgetTester tester) async { + debugDisableBetaCompleteDialog = true; + addTearDown(resetDebugDisableBetaCompleteDialog); addTearDown(testBinding.reset); topRoute = null; previousTopRoute = null; @@ -272,6 +275,8 @@ void main () { }); testWidgets('menu buttons dismiss the menu', (tester) async { + debugDisableBetaCompleteDialog = true; + addTearDown(resetDebugDisableBetaCompleteDialog); addTearDown(testBinding.reset); topRoute = null; previousTopRoute = null; @@ -328,6 +333,8 @@ void main () { } Future prepare(WidgetTester tester) async { + debugDisableBetaCompleteDialog = true; + addTearDown(resetDebugDisableBetaCompleteDialog); addTearDown(testBinding.reset); topRoute = null; previousTopRoute = null; @@ -521,6 +528,8 @@ void main () { }); testWidgets('logging out while still loading', (tester) async { + debugDisableBetaCompleteDialog = true; + addTearDown(resetDebugDisableBetaCompleteDialog); // Regression test for: https://github.com/zulip/zulip-flutter/issues/1219 addTearDown(testBinding.reset); await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); @@ -537,6 +546,8 @@ void main () { }); testWidgets('logging out after fully loaded', (tester) async { + debugDisableBetaCompleteDialog = true; + addTearDown(resetDebugDisableBetaCompleteDialog); // Regression test for: https://github.com/zulip/zulip-flutter/issues/1219 addTearDown(testBinding.reset); await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); diff --git a/test/widgets/login_test.dart b/test/widgets/login_test.dart index a5109ba5db..2dcf03701c 100644 --- a/test/widgets/login_test.dart +++ b/test/widgets/login_test.dart @@ -12,6 +12,7 @@ import 'package:zulip/model/binding.dart'; import 'package:zulip/model/database.dart'; import 'package:zulip/model/localizations.dart'; import 'package:zulip/widgets/app.dart'; +import 'package:zulip/widgets/dialog.dart'; import 'package:zulip/widgets/home.dart'; import 'package:zulip/widgets/login.dart'; import 'package:zulip/widgets/page.dart'; @@ -83,6 +84,8 @@ void main() { Future prepare(WidgetTester tester, GetServerSettingsResult serverSettings) async { + debugDisableBetaCompleteDialog = true; + addTearDown(resetDebugDisableBetaCompleteDialog); addTearDown(testBinding.reset); connection = testBinding.globalStore.apiConnection( diff --git a/test/widgets/message_list_test.dart b/test/widgets/message_list_test.dart index 69e8709e1c..38d5eafba7 100644 --- a/test/widgets/message_list_test.dart +++ b/test/widgets/message_list_test.dart @@ -26,6 +26,7 @@ import 'package:zulip/widgets/autocomplete.dart'; import 'package:zulip/widgets/color.dart'; import 'package:zulip/widgets/compose_box.dart'; import 'package:zulip/widgets/content.dart'; +import 'package:zulip/widgets/dialog.dart'; import 'package:zulip/widgets/icons.dart'; import 'package:zulip/widgets/message_list.dart'; import 'package:zulip/widgets/page.dart'; @@ -73,6 +74,8 @@ void main() { bool skipAssertAccountExists = false, bool skipPumpAndSettle = false, }) async { + debugDisableBetaCompleteDialog = true; + addTearDown(resetDebugDisableBetaCompleteDialog); TypingNotifier.debugEnable = false; addTearDown(TypingNotifier.debugReset); addTearDown(testBinding.reset);