From b383df914200a9ca9e7c306e56ee148a8e0cd667 Mon Sep 17 00:00:00 2001 From: Dante291 Date: Sat, 9 Dec 2023 22:07:47 +0530 Subject: [PATCH] Enhancing UI for inviting --- .../profile_page_view_model.dart | 241 ++++++++++-------- .../profile_page_view_model_test.dart | 123 +++++++++ 2 files changed, 263 insertions(+), 101 deletions(-) create mode 100644 test/view_model_tests/after_auth_view_model_tests/profile_view_model_tests/profile_page_view_model_test.dart diff --git a/lib/view_model/after_auth_view_models/profile_view_models/profile_page_view_model.dart b/lib/view_model/after_auth_view_models/profile_view_models/profile_page_view_model.dart index 07ef94972..e540a514d 100644 --- a/lib/view_model/after_auth_view_models/profile_view_models/profile_page_view_model.dart +++ b/lib/view_model/after_auth_view_models/profile_view_models/profile_page_view_model.dart @@ -1,17 +1,11 @@ -// ignore_for_file: talawa_api_doc, avoid_dynamic_calls -// ignore_for_file: talawa_good_doc_comments - import 'package:currency_picker/currency_picker.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:hive/hive.dart'; import 'package:qr_flutter/qr_flutter.dart'; -import 'package:social_share/social_share.dart'; import 'package:talawa/constants/constants.dart'; -import 'package:talawa/custom_painters/telegram_logo.dart'; -import 'package:talawa/custom_painters/whatsapp_logo.dart'; +import 'package:talawa/custom_painters/talawa_logo.dart'; import 'package:talawa/enums/enums.dart'; import 'package:talawa/locator.dart'; import 'package:talawa/models/organization/org_info.dart'; @@ -20,13 +14,13 @@ import 'package:talawa/services/graphql_config.dart'; import 'package:talawa/services/navigation_service.dart'; import 'package:talawa/services/size_config.dart'; import 'package:talawa/services/user_config.dart'; +import 'package:talawa/utils/app_localization.dart'; import 'package:talawa/view_model/base_view_model.dart'; import 'package:talawa/view_model/lang_view_model.dart'; import 'package:talawa/widgets/custom_alert_dialog.dart'; import 'package:talawa/widgets/custom_progress_dialog.dart'; -/// ProfilePageViewModel class helps to interact with model to serve data -/// and react to user's input in Profile Page view. +/// ProfilePageViewModel class helps to interact with model to serve data and react to user's input in Profile Page view. /// /// Methods include: /// * `logout` @@ -35,20 +29,50 @@ class ProfilePageViewModel extends BaseModel { final _userConfig = locator(); final _navigationService = locator(); final _appLanguageService = locator(); + + /// GlobalKey for scaffoldKey. final GlobalKey scaffoldKey = GlobalKey(); + + /// FocusNode for donationField. final FocusNode donationField = FocusNode(); + + /// Text Controller for donation Amount. TextEditingController donationAmount = TextEditingController(); + + /// Hive Box of user. late final Box user; + + /// Hive Box of url. late final Box url; + + /// Hive Box of organisation. late final Box organisation; + + /// Holds Current Organization. late OrgInfo currentOrg; + + /// Holds Current user. late User currentUser; + + /// Size of Bottom Sheet Height. double bottomSheetHeight = SizeConfig.screenHeight! * 0.68; + + /// donationCurrency. String donationCurrency = "USD"; + + /// Currency Symbol. String donationCurrencySymbol = "\$"; + + /// denomination. final List denomination = ['1', '5', '10']; - // initializer + /// First function to initialize the viewmodel. + /// + /// **params**: + /// None + /// + /// **returns**: + /// None void initialize() { setState(ViewState.busy); currentOrg = _userConfig.currentOrg; @@ -56,8 +80,13 @@ class ProfilePageViewModel extends BaseModel { setState(ViewState.idle); } - /// This method destroys the user's session or sign out the user from app. - /// The function asks for the confimation in Custom Alert Dialog. + /// This method destroys the user's session or sign out the user from app, The function asks for the confimation in Custom Alert Dialog. + /// + /// **params**: + /// * `context`: BuildContext of the widget + /// + /// **returns**: + /// * `Future`: Resolves when user logout Future logout(BuildContext context) async { // push custom alert dialog with the confirmation message. navigationService.pushDialog( @@ -111,7 +140,17 @@ class ProfilePageViewModel extends BaseModel { } /// This method changes the currency of the user for donation purpose. - void changeCurrency(BuildContext context, Function setter) { + /// + /// **params**: + /// * `context`: BuildContext of the widget + /// * `setter`: Setter Function + /// + /// **returns**: + /// None + void changeCurrency( + BuildContext context, + void Function(void Function()) setter, + ) { showCurrencyPicker( context: context, currencyFilter: supportedCurrencies, @@ -124,100 +163,69 @@ class ProfilePageViewModel extends BaseModel { ); } - /// This function generates the organization invitation link in a Dialog Box. - /// Dialog box contains the QR-code of organization invite link and social media sharing options. + /// This Function creates a QR Code for latest release . + /// + /// **params**: + /// * `context`: Build Context + /// + /// **returns**: + /// None void invite(BuildContext context) { _appLanguageService.initialize(); - // organization url - final String url = - 'https://cyberwake.github.io/applink/invite?selectLang=${_appLanguageService.appLocal.languageCode}&setUrl=${GraphqlConfig.orgURI}&selectOrg=${userConfig.currentOrg.id!}'; - // QR final String qrData = '${GraphqlConfig.orgURI}?orgid=${userConfig.currentOrg.id!}'; - print(url); - print(qrData); - showModalBottomSheet( + + showDialog( context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(30), - topRight: Radius.circular(30), - ), - ), builder: (BuildContext context) { - return ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(30), - topRight: Radius.circular(30), + return Dialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20.0), ), child: Container( - height: MediaQuery.of(context).size.height * 0.75, - decoration: const BoxDecoration( - color: Colors.white, + padding: const EdgeInsets.all(20), + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.80, ), child: Column( - mainAxisSize: MainAxisSize.max, + mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - QrImage( + iconButton( + CustomPaint( + size: const Size(48, 48 * 1), + painter: AppLogo(), + ), + () {}, + ), + const SizedBox(height: 20), + Text( + '${userConfig.currentOrg.name}', + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 20), + QrImageView( data: qrData, version: QrVersions.auto, size: 200.0, - foregroundColor: Colors.black, - ), - SizedBox( - height: SizeConfig.screenHeight! * 0.08, ), + const SizedBox(height: 20), Text( - 'Scan the QR to join ${userConfig.currentOrg.name}', - style: const TextStyle(color: Colors.black), - ), - SizedBox( - height: SizeConfig.screenHeight! * 0.02, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - mainAxisSize: MainAxisSize.min, - children: [ - iconButton( - const FaIcon( - FontAwesomeIcons.twitter, - size: 35, - color: Color(0xFF1DA1F2), - ), - () async => SocialShare.shareTwitter('Join us', url: url), - ), - iconButton( - CustomPaint( - size: const Size( - 50, - 50 * 1.004, - ), //You can Replace [WIDTH] with your desired width for Custom Paint and height will be calculated automatically - painter: WhatsappLogo(), - ), - () async => SocialShare.shareWhatsapp(url), - ), - iconButton( - CustomPaint( - size: Size( - 45, - (45 * 1).toDouble(), - ), //You can Replace [WIDTH] with your desired width for Custom Paint and height will be calculated automatically - painter: TelegramLogo(), - ), - () async => SocialShare.shareTelegram(url), - ), - iconButton( - const FaIcon( - FontAwesomeIcons.shareNodes, - size: 30, - color: Color(0xff40c351), - ), - () async => SocialShare.shareOptions(url), - ), - ], + AppLocalizations.of(context)!.strictTranslate('JOIN'), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), ), + const SizedBox(height: 30), ], ), ), @@ -228,10 +236,13 @@ class ProfilePageViewModel extends BaseModel { /// This widget returns the button for social media sharing option. /// - /// params: - /// * [icon] : This is `Widget` type with icon details. - /// * [onTap] : This is `Function`, which invoke on tap. - Widget iconButton(Widget icon, Function onTap) { + /// **params**: + /// * `icon`: This is Widget type with icon details. + /// * `onTap`: This is Function which invoke on tap. + /// + /// **returns**: + /// * `Widget`: Icon Button + Widget iconButton(Widget icon, void Function() onTap) { return Stack( children: [ IconButton( @@ -247,13 +258,17 @@ class ProfilePageViewModel extends BaseModel { /// This widget returns button for domination. /// - /// params: - /// * [amount] : donation Amount. - /// * [setter] : `Function` type, which on tap set the amount to `donationAmount`. + /// **params**: + /// * `amount`: donation Amount. + /// * `context`: BuildContext. + /// * `setter`: `Function` type, which on tap set the amount to `donationAmount`. + /// + /// **returns**: + /// * `Widget`: Icon Button Widget dominationButton( String amount, BuildContext context, - Function setter, + void Function(void Function()) setter, ) { return InkWell( onTap: () { @@ -280,8 +295,14 @@ class ProfilePageViewModel extends BaseModel { ); } - // Listener on `donationField` widget focus. - void attachListener(Function setter) { + /// This widget returns button for domination. + /// + /// **params**: + /// * `setter`: SetState holder. + /// + /// **returns**: + /// None + void attachListener(void Function(void Function()) setter) { donationField.addListener(() { if (donationField.hasFocus) { setter(() { @@ -299,18 +320,36 @@ class ProfilePageViewModel extends BaseModel { }); } - // pop the route from `navigationService`. + /// pop the route from `navigationService`. + /// + /// **params**: + /// None + /// + /// **returns**: + /// None void popBottomSheet() { _navigationService.pop(); } - // to update the bottom sheet height. + /// to update the bottom sheet height. + /// + /// **params**: + /// None + /// + /// **returns**: + /// None void updateSheetHeight() { bottomSheetHeight = SizeConfig.screenHeight! * 0.65; notifyListeners(); } - // show message on Snack Bar. + /// show message on Snack Bar. + /// + /// **params**: + /// * `message`: String Message to show on snackbar + /// + /// **returns**: + /// None void showSnackBar(String message) { _navigationService.showTalawaErrorDialog(message, MessageType.error); } diff --git a/test/view_model_tests/after_auth_view_model_tests/profile_view_model_tests/profile_page_view_model_test.dart b/test/view_model_tests/after_auth_view_model_tests/profile_view_model_tests/profile_page_view_model_test.dart new file mode 100644 index 000000000..ba2ac49dd --- /dev/null +++ b/test/view_model_tests/after_auth_view_model_tests/profile_view_model_tests/profile_page_view_model_test.dart @@ -0,0 +1,123 @@ +// ignore_for_file: talawa_api_doc +// ignore_for_file: talawa_good_doc_comments + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:talawa/services/graphql_config.dart'; +import 'package:talawa/services/size_config.dart'; +import 'package:talawa/view_model/after_auth_view_models/profile_view_models/profile_page_view_model.dart'; + +import '../../../helpers/test_helpers.dart'; +import '../../../helpers/test_locator.dart'; + +class MockCallbackFunction extends Mock { + void call(); +} + +class MockNavigatorObserver extends Mock implements NavigatorObserver {} + +class MockBuildContext extends Mock implements BuildContext {} + +void verifyInteraction(dynamic x, {required String mockName}) { + // Ensures that navigation service was called + try { + verifyZeroInteractions(x); + //If 0 interactions passes that means mock was not called hence test fails + throw Exception("Expected interaction but found 0 with $mockName"); + } on TestFailure { + //If test fails then 1 or more interactions with navigation service hence test passes + expect(true, true); + } +} + +void main() { + testSetupLocator(); + locator().test(); + locator().test(); + + setUp(() { + registerServices(); + locator().test(); + }); + + tearDown(() { + unregisterServices(); + }); + + group('ProfilePageViewModel Tests -', () { + test("Test initialization", () { + final model = ProfilePageViewModel(); + model.initialize(); + expect(model.currentOrg, userConfig.currentOrg); + expect(model.currentUser, userConfig.currentUser); + }); + + test("Test showSnackBar and popBottomSheet function", () { + final model = ProfilePageViewModel(); + model.initialize(); + + model.showSnackBar("fake_message"); + verify(navigationService.showSnackBar("fake_message")); + + model.popBottomSheet(); + verify(navigationService.pop()); + }); + + test("Test updateSheetHeight function", () { + final model = ProfilePageViewModel(); + model.initialize(); + model.updateSheetHeight(); + expect(model.bottomSheetHeight, SizeConfig.screenHeight! * 0.65); + }); + + testWidgets("Test iconButton function", (tester) async { + final model = ProfilePageViewModel(); + model.initialize(); + const Icon testIcon = Icon(Icons.cancel); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: model.iconButton(testIcon, () {}), + ), + ), + ); + final iconButtonFinder = find.byType(IconButton); + final iconButton = tester.firstWidget(iconButtonFinder); + expect((iconButton as IconButton).icon, testIcon); + }); + + testWidgets("Test dominationButton function", (tester) async { + final mockContext = MockBuildContext(); + final model = ProfilePageViewModel(); + model.initialize(); + const String amt = "test_amt"; + await tester.pumpWidget( + MaterialApp( + home: Scaffold(body: model.dominationButton(amt, mockContext, () {})), + ), + ); + final containerFinder = find.byType(Container); + final Container container = tester.firstWidget(containerFinder); + expect( + container.padding, + EdgeInsets.symmetric( + vertical: SizeConfig.screenHeight! * 0.02, + horizontal: SizeConfig.screenWidth! * 0.075, + ), + ); + }); + + testWidgets("Test logout function", (tester) async { + final mockContext = MockBuildContext(); + final model = ProfilePageViewModel(); + final mocknav = getAndRegisterNavigationService(); + model.initialize(); + await model.logout(mockContext); + await tester.pumpAndSettle(); + + //Ensures that naviagation service was called + verifyInteraction(mocknav, mockName: "NavigationService"); + }); + }); +}