From 61d9f39ae0dc2bf8cf972d68ae025ecaf0019cc3 Mon Sep 17 00:00:00 2001 From: Pierre Monier Date: Fri, 9 Dec 2022 16:49:10 +0100 Subject: [PATCH] feat: web account deletion (#3416) * feat: add webview for account deletion * feat: add webview for account deletion * clean(preferences): remove useless reasonController * clean: fix variable naming * refactor: add deletionwebview state private and remove l10n from path segments --- packages/app/android/app/build.gradle | 2 +- packages/app/pubspec.lock | 28 +++++++++ .../data_models/user_management_provider.dart | 18 +++--- packages/smooth_app/lib/l10n/app_en.arb | 4 ++ .../preferences/account_deletion_webview.dart | 58 +++++++++++++++++++ .../preferences/user_preferences_account.dart | 52 +++-------------- packages/smooth_app/pubspec.lock | 28 +++++++++ packages/smooth_app/pubspec.yaml | 1 + .../pages/user_preferences_page_test.dart | 57 ++++++++++++++++++ .../test/tests_utils/local_database_mock.dart | 4 ++ .../smooth_app/test/tests_utils/mocks.dart | 9 ++- 11 files changed, 207 insertions(+), 54 deletions(-) create mode 100644 packages/smooth_app/lib/pages/preferences/account_deletion_webview.dart create mode 100644 packages/smooth_app/test/tests_utils/local_database_mock.dart diff --git a/packages/app/android/app/build.gradle b/packages/app/android/app/build.gradle index a08918dc501..a9b1054d711 100644 --- a/packages/app/android/app/build.gradle +++ b/packages/app/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + compileSdkVersion 33 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/packages/app/pubspec.lock b/packages/app/pubspec.lock index ae00923ccb2..57942405981 100644 --- a/packages/app/pubspec.lock +++ b/packages/app/pubspec.lock @@ -1351,6 +1351,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.0" + webview_flutter: + dependency: transitive + description: + name: webview_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.4" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.10.4" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.5" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + url: "https://pub.dartlang.org" + source: hosted + version: "2.9.5" win32: dependency: transitive description: diff --git a/packages/smooth_app/lib/data_models/user_management_provider.dart b/packages/smooth_app/lib/data_models/user_management_provider.dart index 5eea995cc9d..a1392329793 100644 --- a/packages/smooth_app/lib/data_models/user_management_provider.dart +++ b/packages/smooth_app/lib/data_models/user_management_provider.dart @@ -44,13 +44,16 @@ class UserManagementProvider with ChangeNotifier { } /// Mounts already stored credentials, called at app startup - static Future mountCredentials() async { - String? userId; - String? password; + /// + /// We can use optional parameters to mock in tests + static Future mountCredentials( + {String? userId, String? password}) async { + String? effectiveUserId; + String? effectivePassword; try { - userId = await DaoSecuredString.get(_USER_ID); - password = await DaoSecuredString.get(_PASSWORD); + effectiveUserId = userId ?? await DaoSecuredString.get(_USER_ID); + effectivePassword = password ?? await DaoSecuredString.get(_PASSWORD); } on PlatformException { /// Decrypting the values can go wrong if, for example, the app was /// manually overwritten from an external apk. @@ -59,11 +62,12 @@ class UserManagementProvider with ChangeNotifier { Logs.e('Credentials query failed, you have been logged out'); } - if (userId == null || password == null) { + if (effectiveUserId == null || effectivePassword == null) { return; } - final User user = User(userId: userId, password: password); + final User user = + User(userId: effectiveUserId, password: effectivePassword); OpenFoodAPIConfiguration.globalUser = user; } diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index e680fecd189..689a161ad4e 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -898,6 +898,10 @@ "@account_delete": { "description": "Delete account button (user profile)" }, + "account_deletion_subject": "Delete my account", + "@account_deletion_subject": { + "description": "Subject of the webview open when the user wants to delete his account" + }, "user_profile": "Account", "@user_profile": { "description": "User account (if connected)" diff --git a/packages/smooth_app/lib/pages/preferences/account_deletion_webview.dart b/packages/smooth_app/lib/pages/preferences/account_deletion_webview.dart new file mode 100644 index 00000000000..4a5f8a6176f --- /dev/null +++ b/packages/smooth_app/lib/pages/preferences/account_deletion_webview.dart @@ -0,0 +1,58 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:openfoodfacts/utils/OpenFoodAPIConfiguration.dart'; +import 'package:smooth_app/helpers/user_management_helper.dart'; +import 'package:smooth_app/widgets/smooth_scaffold.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class AccountDeletionWebview extends StatefulWidget { + @override + State createState() => _AccountDeletionWebviewState(); +} + +class _AccountDeletionWebviewState extends State { + @override + void initState() { + super.initState(); + // Enable virtual display. + if (Platform.isAndroid) { + WebView.platform = AndroidWebView(); + } + } + + String _getUrl() { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + final String subject = appLocalizations.account_deletion_subject; + + final String? userId = OpenFoodAPIConfiguration.globalUser?.userId; + + final Uri uri = Uri( + scheme: 'https', + host: 'blog.openfoodfacts.org', + pathSegments: [ + 'en', + 'account-deletion', + ], + queryParameters: { + 'your-subject': subject, + if (userId != null && userId.isEmail) + 'your-mail': userId + else if (userId != null) + 'your-name': userId + }); + + return uri.toString(); + } + + @override + Widget build(BuildContext context) { + return SmoothScaffold( + appBar: AppBar(), + body: WebView( + initialUrl: _getUrl(), + ), + ); + } +} diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart index 3ac8961c245..d8cb3f1f48c 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_email_sender/flutter_email_sender.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:openfoodfacts/utils/OpenFoodAPIConfiguration.dart'; @@ -11,11 +10,11 @@ import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/generic_lib/buttons/smooth_simple_button.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart'; -import 'package:smooth_app/generic_lib/widgets/smooth_text_form_field.dart'; import 'package:smooth_app/helpers/analytics_helper.dart'; import 'package:smooth_app/helpers/launch_url_helper.dart'; import 'package:smooth_app/helpers/user_management_helper.dart'; import 'package:smooth_app/pages/preferences/abstract_user_preferences.dart'; +import 'package:smooth_app/pages/preferences/account_deletion_webview.dart'; import 'package:smooth_app/pages/preferences/user_preferences_list_tile.dart'; import 'package:smooth_app/pages/preferences/user_preferences_page.dart'; import 'package:smooth_app/pages/preferences/user_preferences_widgets.dart'; @@ -231,7 +230,6 @@ class _UserPreferencesPageState extends State { final ThemeData theme = Theme.of(context); final AppLocalizations appLocalizations = AppLocalizations.of(context); final Size size = MediaQuery.of(context).size; - final TextEditingController reasonController = TextEditingController(); final List result; @@ -306,47 +304,13 @@ class _UserPreferencesPageState extends State { const UserPreferencesListItemDivider(), _getListTile( appLocalizations.account_delete, - () async { - final String? reason = await showDialog( - context: context, - builder: (BuildContext context) { - return SmoothAlertDialog( - title: appLocalizations.account_delete, - body: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(appLocalizations.account_delete_message), - const SizedBox( - height: 5, - ), - SmoothTextFormField( - type: TextFieldTypes.PLAIN_TEXT, - textInputType: TextInputType.text, - controller: reasonController, - hintText: appLocalizations.reason), - ], - ), - positiveAction: SmoothActionButton( - text: appLocalizations.account_delete, - onPressed: () => - Navigator.pop(context, reasonController.text), - ), - negativeAction: SmoothActionButton( - text: appLocalizations.cancel, - onPressed: () => Navigator.pop(context)), - ); - }); - if (reason != null) { - final Email email = Email( - body: - '${appLocalizations.email_body_account_deletion(userId)} $reason', - subject: appLocalizations.email_subject_account_deletion, - recipients: ['contact@openfoodfacts.org'], - ); - - await FlutterEmailSender.send(email); - } + () { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => AccountDeletionWebview(), + ), + ); }, Icons.delete, ), diff --git a/packages/smooth_app/pubspec.lock b/packages/smooth_app/pubspec.lock index 31691bc7d43..ec59fa72721 100644 --- a/packages/smooth_app/pubspec.lock +++ b/packages/smooth_app/pubspec.lock @@ -1358,6 +1358,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.0" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.4" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.10.4" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.5" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + url: "https://pub.dartlang.org" + source: hosted + version: "2.9.5" win32: dependency: transitive description: diff --git a/packages/smooth_app/pubspec.yaml b/packages/smooth_app/pubspec.yaml index d19f2764f1b..2cbf58e102e 100644 --- a/packages/smooth_app/pubspec.yaml +++ b/packages/smooth_app/pubspec.yaml @@ -80,6 +80,7 @@ dependencies: fimber: 0.6.6 shimmer: 2.0.0 lottie: 1.4.2 + webview_flutter: ^3.0.4 dev_dependencies: integration_test: diff --git a/packages/smooth_app/test/pages/user_preferences_page_test.dart b/packages/smooth_app/test/pages/user_preferences_page_test.dart index 085e3dec568..07435a86112 100644 --- a/packages/smooth_app/test/pages/user_preferences_page_test.dart +++ b/packages/smooth_app/test/pages/user_preferences_page_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:openfoodfacts/personalized_search/product_preferences_selection.dart'; @@ -7,10 +8,13 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:smooth_app/data_models/product_preferences.dart'; import 'package:smooth_app/data_models/user_management_provider.dart'; import 'package:smooth_app/data_models/user_preferences.dart'; +import 'package:smooth_app/pages/preferences/account_deletion_webview.dart'; import 'package:smooth_app/pages/preferences/user_preferences_page.dart'; import 'package:smooth_app/themes/theme_provider.dart'; +import 'package:webview_flutter/webview_flutter.dart'; import '../tests_utils/goldens.dart'; +import '../tests_utils/local_database_mock.dart'; import '../tests_utils/mocks.dart'; void main() { @@ -70,4 +74,57 @@ void main() { }); } }); + + testWidgets('it should open a webview for account deletion', + (WidgetTester tester) async { + // Override & mock out HTTP Requests + final HttpOverrides? priorOverrides = HttpOverrides.current; + HttpOverrides.global = MockHttpOverrides(); + + late UserPreferences userPreferences; + late ProductPreferences productPreferences; + late ThemeProvider themeProvider; + + SharedPreferences.setMockInitialValues( + mockSharedPreferences(), + ); + + userPreferences = await UserPreferences.getUserPreferences(); + + productPreferences = ProductPreferences(ProductPreferencesSelection( + setImportance: userPreferences.setImportance, + getImportance: userPreferences.getImportance, + notify: () => productPreferences.notifyListeners(), + )); + await productPreferences.init(PlatformAssetBundle()); + await userPreferences.init(productPreferences); + themeProvider = ThemeProvider(userPreferences); + + UserManagementProvider.mountCredentials( + userId: 'userId', + password: 'password', + ); + + await tester.pumpWidget( + MockSmoothApp( + userPreferences, + UserManagementProvider(), + productPreferences, + themeProvider, + const UserPreferencesPage(type: PreferencePageType.ACCOUNT), + localDatabase: MockLocalDatabase(), + ), + ); + await tester.pump(); + + await tester.tap(find.byIcon(Icons.delete)); + + await tester.pumpAndSettle(); + + expect(find.byType(AccountDeletionWebview), findsOneWidget); + expect(find.byType(WebView), findsOneWidget); + + // Restore prior overrides + HttpOverrides.global = priorOverrides; + }); } diff --git a/packages/smooth_app/test/tests_utils/local_database_mock.dart b/packages/smooth_app/test/tests_utils/local_database_mock.dart new file mode 100644 index 00000000000..76e009082c4 --- /dev/null +++ b/packages/smooth_app/test/tests_utils/local_database_mock.dart @@ -0,0 +1,4 @@ +import 'package:mockito/mockito.dart'; +import 'package:smooth_app/database/local_database.dart'; + +class MockLocalDatabase extends Mock implements LocalDatabase {} diff --git a/packages/smooth_app/test/tests_utils/mocks.dart b/packages/smooth_app/test/tests_utils/mocks.dart index a30158a3b83..57090000be0 100644 --- a/packages/smooth_app/test/tests_utils/mocks.dart +++ b/packages/smooth_app/test/tests_utils/mocks.dart @@ -10,6 +10,7 @@ import 'package:provider/provider.dart'; import 'package:smooth_app/data_models/product_preferences.dart'; import 'package:smooth_app/data_models/user_management_provider.dart'; import 'package:smooth_app/data_models/user_preferences.dart'; +import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/themes/smooth_theme.dart'; import 'package:smooth_app/themes/theme_provider.dart'; @@ -20,13 +21,15 @@ class MockSmoothApp extends StatelessWidget { this.userManagementProvider, this.productPreferences, this.themeProvider, - this.child, - ); + this.child, { + this.localDatabase, + }); final UserPreferences userPreferences; final UserManagementProvider userManagementProvider; final ProductPreferences productPreferences; final ThemeProvider themeProvider; + final LocalDatabase? localDatabase; final Widget child; @override @@ -39,6 +42,8 @@ class MockSmoothApp extends StatelessWidget { ChangeNotifierProvider.value(value: themeProvider), ChangeNotifierProvider.value( value: userManagementProvider), + if (localDatabase != null) + ChangeNotifierProvider.value(value: localDatabase!), ], child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates,