From 8d6da30d09112a0fb6b57d7f42437ffa7b79287e Mon Sep 17 00:00:00 2001 From: Abdullah Alassaf Date: Thu, 1 Aug 2024 12:57:13 +0300 Subject: [PATCH] Fixed design issues, added updated read me file --- README.md | 82 +++++ .../menu/bloc/profile_bloc/profile_bloc.dart | 68 ++-- lib/features/menu/view/menu_view.dart | 3 +- .../view/widgets/profile/profile_tab.dart | 32 +- .../view/widgets/profile/profile_view.dart | 324 +++++++++--------- pubspec.yaml | 2 +- 6 files changed, 297 insertions(+), 214 deletions(-) diff --git a/README.md b/README.md index 6db7d9e..418c018 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,85 @@ A few resources to get you started if this is your first Flutter project: For help getting started with Flutter development, view the [online documentation](https://docs.flutter.dev/), which offers tutorials, samples, guidance on mobile development, and a full API reference. + +## Development Process + +1- You'll receive a task assignment in Jira that's been assigned to you. + +2- In Jira, change the status of the task to "in progress". + +3- Create a new branch for the task using the command "git checkout -b task_number". + +4- Make your changes and commit them using the command "git commit -m 'Add my changes'". + +5- Push your changes to the task branch using the command "git push origin task-branch". +Open a pull request on the DEV branch and add a reviewer to it. + +6- Once the reviewer approves your pull request, merge your changes into the DEV branch. + +7- Use the command "git checkout DEV" to switch to the DEV branch. + +8- Upload apk file and ipa file to Firebase distribution (you can find the steps to upload your app to Firebase Distribution for both Android and iOS platforms in the Deployment section). + +9- Update the task status in Jira to "QA". +## Deployment + +### Android + +**- Firebase Distribution:** + +To test the app, we use Firebase Distribution to send testing builds of the app to the testers. + +- Create an Android build for testing with the command: `flutter build apk --dart-define FLAVOR=staging --build-name={build_version}-qa --build-number={build_number}`. + +- Upload the apk file to Firebase distribution + +**- Google Play Store:** + +1- To create an APK file from your Flutter project, you can use the command: `flutter build apk --release` + +2- Upload your APK file to the Google Play Console and provide all necessary information about the release, such as release notes and version number. + +3- Submit your app for review, which can take several days to complete. Once your app is approved, it will be available for download on the Google Play Store. + +### iOS + +**- Firebase Distribution:** + +To test the app, we use Firebase Distribution to send testing builds of the app to the testers. + +1- Create an iOS for testing with the command: `flutter build ios --dart-define FLAVOR=staging --build-name=1.0.0-qa --build-number=1`. + +2- Create an archive of your app: Open Xcode and go to the "Product" menu, then select "Archive" to create an archive of your app. Make sure to select the "Generic iOS Device" as the build destination. + +3- Once the archive is complete, go to the "Organizer" window and select the archive you just created and click on the "Distribute App" button. + +4- Choose "Ad Hoc" as the distribution method and click "Next". + +5- Choose the appropriate signing identity and click "Export". + +6- Choose a location to save the exported app and click "Save". + +7- Your HOC build is now ready to be signed and uploaded to Firebase Distribution. + +8- Upload the ipa file to Firebase distribution + +You can also create an archive through these commands lines: + +1- Create an iOS for testing with the command: `flutter build ios --dart-define FLAVOR=staging --build-name={build_version}-qa --build-number={build_number}`. + +2- Create an archive with this command: `xcodebuild -workspace Runner.xcworkspace -scheme Runner -archivePath "build/Runner.xcarchive" archive`. + +3- Export the ipa file with this command: `-exportArchive -archivePath "build/Runner.xcarchive" -exportPath "build/exported_ipas" -exportOptionsPlist "build/ExportOptions.plist"` + +**- Apple Store** + +1- Build your app: Use Flutter to build your app for release. To build your app for iOS, use the command: `flutter build ios --release --no-codesign` + +Note: The --no-codesign flag will tell Flutter not to sign your app, since you'll be doing that later with Xcode. + +2- Create an archive of your app: Open Xcode and go to the "Product" menu, then select "Archive" to create an archive of your app. Make sure to select the "Generic iOS Device" as the build destination. + +3- Validate and upload your app: Once the archive is created, Xcode will automatically open the Organizer window. Select the archive you just created and click the "Validate App" button to validate your app. Once the validation process is complete, click the "Distribute App" button to upload your app to the App Store Connect. + +4- Submit your app for review: After uploading your app, you'll need to submit it for review by Apple. This process can take several days, and you'll be notified by email once your app is approved or rejected. diff --git a/lib/features/menu/bloc/profile_bloc/profile_bloc.dart b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart index 640527b..c01a5c5 100644 --- a/lib/features/menu/bloc/profile_bloc/profile_bloc.dart +++ b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart @@ -23,13 +23,12 @@ class ProfileBloc extends Bloc { String timeZoneSelected = ''; String regionSelected = ''; final TextEditingController searchController = TextEditingController(); - final TextEditingController nameController = TextEditingController(text: '${HomeCubit.user!.firstName} ${HomeCubit.user!.lastName}'); - + final TextEditingController nameController = + TextEditingController(text: '${HomeCubit.user!.firstName} ${HomeCubit.user!.lastName}'); List allRegions = []; List allTimeZone = []; - ProfileBloc() : super(InitialState()) { on(_fetchUserInfo); on(_fetchTimeZone); @@ -53,7 +52,7 @@ class ProfileBloc extends Bloc { final nameParts = fullName.split(' '); final firstName = nameParts[0]; final lastName = nameParts.length > 1 ? nameParts[1] : ''; - var response = await ProfileApi.saveName(firstName: firstName, lastName: lastName); + await ProfileApi.saveName(firstName: firstName, lastName: lastName); add(InitialProfileEvent()); await HomeCubit.getInstance().fetchUserInfo(); CustomSnackBar.displaySnackBar('Save Successfully'); @@ -72,7 +71,7 @@ class ProfileBloc extends Bloc { Future.delayed(const Duration(milliseconds: 500), () { focusNode.requestFocus(); }); - }else { + } else { focusNode.unfocus(); } emit(NameEditingState(editName: editName)); @@ -116,7 +115,7 @@ class ProfileBloc extends Bloc { Future selectRegion(SelectRegionEvent event, Emitter emit) async { try { emit(LoadingInitialState()); - await ProfileApi.saveRegion(regionUuid:event.val ); + await ProfileApi.saveRegion(regionUuid: event.val); CustomSnackBar.displaySnackBar('Save Successfully'); emit(SaveState()); } catch (e) { @@ -125,7 +124,6 @@ class ProfileBloc extends Bloc { } } - Future searchRegion(SearchRegionEvent event, Emitter emit) async { emit(LoadingInitialState()); final query = event.query.toLowerCase(); @@ -158,7 +156,6 @@ class ProfileBloc extends Bloc { } } - void _fetchRegion(RegionInitialEvent event, Emitter emit) async { try { emit(LoadingInitialState()); @@ -170,36 +167,37 @@ class ProfileBloc extends Bloc { } Future _selectImage(SelectImageEvent event, Emitter emit) async { - if (await _requestPermission()) { - emit(ChangeImageState()); - final pickedFile = await _picker.pickImage(source: ImageSource.gallery); - if (pickedFile != null) { - image = File(pickedFile.path); - final bytes = image!.readAsBytesSync().lengthInBytes; - final kb = bytes / 1024; - final mb = kb / 1024; - if(mb>1){ - image=null; - CustomSnackBar.displaySnackBar('Image size must be 1 MB or less'); - }else{ - await _saveImage(); + try { + if (await _requestPermission()) { + emit(ChangeImageState()); + final pickedFile = await _picker.pickImage(source: ImageSource.gallery); + if (pickedFile != null) { + image = File(pickedFile.path); + final bytes = image!.readAsBytesSync().lengthInBytes; + final kb = bytes / 1024; + final mb = kb / 1024; + if (mb > 1) { + image = null; + CustomSnackBar.displaySnackBar('Image size must be 1 MB or less'); + } else { + emit(LoadingInitialState()); + await _saveImage(); + emit(ImageSelectedState()); + } } + emit(ImageSelectedState()); } else { - print('No image selected.'); + _showPermissionDeniedDialog(event.context); } - emit(ImageSelectedState()); - } else { - _showPermissionDeniedDialog(event.context); + } catch (_) { + emit(const FailedState(errorMessage: 'Something went wrong')); } } Future _saveImage() async { - emit(LoadingInitialState()); List imageBytes = image!.readAsBytesSync(); String base64Image = base64Encode(imageBytes); - print(base64Image); - var response = await ProfileApi.saveImage(base64Image); - emit(ImageSelectedState()); + await ProfileApi.saveImage(base64Image); } void _showPermissionDeniedDialog(BuildContext context) { @@ -229,9 +227,6 @@ class ProfileBloc extends Bloc { ); } - - - bool _validateInputs() { final nameError = fullNameValidator(nameController.text); if (nameError != null) { @@ -241,7 +236,6 @@ class ProfileBloc extends Bloc { return false; } - String? fullNameValidator(String? value) { if (value == null) return 'Full name is required'; final withoutExtraSpaces = value.replaceAll(RegExp(r"\s+"), ' ').trim(); @@ -261,13 +255,12 @@ class ProfileBloc extends Bloc { return null; } - Future _requestPermission() async { DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); - if (Platform.isAndroid ) { + if (Platform.isAndroid) { AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; PermissionStatus status = await Permission.photos.status; - if(androidInfo.version.sdkInt<33){ + if (androidInfo.version.sdkInt < 33) { if (status.isDenied) { PermissionStatus status = await Permission.storage.request(); if (status.isGranted) { @@ -276,7 +269,7 @@ class ProfileBloc extends Bloc { return false; } } - }else{ + } else { if (status.isGranted) { return true; } else if (status.isDenied) { @@ -301,5 +294,4 @@ class ProfileBloc extends Bloc { } } } - } diff --git a/lib/features/menu/view/menu_view.dart b/lib/features/menu/view/menu_view.dart index 9b2cf56..298d1f7 100644 --- a/lib/features/menu/view/menu_view.dart +++ b/lib/features/menu/view/menu_view.dart @@ -21,12 +21,11 @@ class MenuView extends StatelessWidget { builder: (context, state) { return BlocBuilder( builder: (context, state) { - final profileBloc = BlocProvider.of(context); return SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Column( children: [ - ProfileTab(), + const ProfileTab(), for (var section in menuSections) MenuList( section: section, diff --git a/lib/features/menu/view/widgets/profile/profile_tab.dart b/lib/features/menu/view/widgets/profile/profile_tab.dart index d3e2dd3..eaa11b3 100644 --- a/lib/features/menu/view/widgets/profile/profile_tab.dart +++ b/lib/features/menu/view/widgets/profile/profile_tab.dart @@ -7,20 +7,23 @@ import 'package:syncrow_app/features/shared_widgets/text_widgets/body_medium.dar import 'package:syncrow_app/features/shared_widgets/text_widgets/body_small.dart'; class ProfileTab extends StatelessWidget { - const ProfileTab({super.key,}); + const ProfileTab({ + super.key, + }); @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - return _buildProfileContent(context ); + return _buildProfileContent(context); }, ); } Widget _buildProfileContent(BuildContext context) { - final homeCubit = context.read(); return Padding( - padding: const EdgeInsets.symmetric(vertical: 10,), + padding: const EdgeInsets.symmetric( + vertical: 10, + ), child: InkWell( onTap: () { Navigator.of(context) @@ -28,7 +31,8 @@ class ProfileTab extends StatelessWidget { MaterialPageRoute( builder: (context) => const ProfileView(), ), - ).then((result) { + ) + .then((result) { context.read().fetchUserInfo(); }); }, @@ -47,7 +51,7 @@ class ProfileTab extends StatelessWidget { Row( children: [ BodyMedium( - text: '${HomeCubit.user!.firstName ?? ''} ', + text: '${HomeCubit.user?.firstName ?? ''} ', fontWeight: FontWeight.bold, ), BodyMedium( @@ -56,7 +60,9 @@ class ProfileTab extends StatelessWidget { ), ], ), - SizedBox(height: 5,), + const SizedBox( + height: 5, + ), const BodySmall(text: "Syncrow Account"), ], ), @@ -76,12 +82,12 @@ class ProfileTab extends StatelessWidget { child: ClipOval( child: HomeCubit.user?.profilePicture != null ? Image.memory( - HomeCubit.user!.profilePicture!, - fit: BoxFit.cover, - width: 110, - height: 110, - ) - : Icon(Icons.person, size: 70), // Fallback if no image + HomeCubit.user!.profilePicture!, + fit: BoxFit.cover, + width: 110, + height: 110, + ) + : const Icon(Icons.person, size: 70), // Fallback if no image ), ), ), diff --git a/lib/features/menu/view/widgets/profile/profile_view.dart b/lib/features/menu/view/widgets/profile/profile_view.dart index 7139ec1..7fab399 100644 --- a/lib/features/menu/view/widgets/profile/profile_view.dart +++ b/lib/features/menu/view/widgets/profile/profile_view.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_bloc.dart'; @@ -25,175 +24,180 @@ class ProfileView extends StatelessWidget { final profileBloc = BlocProvider.of(context); return DefaultScaffold( title: 'Syncrow Account', - child: - state is LoadingInitialState - ? const Center(child: CircularProgressIndicator()): - Column( - children: [ - SizedBox( - height: MediaQuery.of(context).size.height * 0.05, - ), - InkWell( - onTap: () { - profileBloc.add(SelectImageEvent(context: context, isSelected: false)); - }, - child: SizedBox.square( - dimension: 125, - child: CircleAvatar( - backgroundColor: Colors.white, - child: SizedBox.square( - dimension: 120, - child: CircleAvatar( - backgroundColor: Colors.white, - backgroundImage: profileBloc.image == null - ? null - : FileImage(profileBloc.image!), - child: profileBloc.image != null - ? null - :HomeCubit.user!.profilePicture != null - ? ClipOval( - child: Image.memory( - HomeCubit.user!.profilePicture!, - fit: BoxFit.cover, - width: 120, - height: 120, - ), - ) - : null, + child: state is LoadingInitialState + ? const Center(child: CircularProgressIndicator()) + : SizedBox( + height: MediaQuery.sizeOf(context).height, + child: ListView( + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.05, ), - ), - ), - ), - ), - const SizedBox(height: 20), - SizedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IntrinsicWidth( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 200), - child: TextFormField( - maxLength: 30, - style: const TextStyle( - color: Colors.black, - ), - textAlign: TextAlign.center, - focusNode: profileBloc.focusNode, - controller: profileBloc.nameController, - enabled: profileBloc.editName, - onEditingComplete: () { - profileBloc.add(SaveNameEvent(context: context)); - }, - decoration: const InputDecoration( - hintText: "Your Name", - border: InputBorder.none, - fillColor: Colors.white10, - counterText: '', + InkWell( + onTap: () { + profileBloc.add(SelectImageEvent(context: context, isSelected: false)); + }, + child: SizedBox.square( + dimension: 125, + child: CircleAvatar( + backgroundColor: Colors.white, + child: SizedBox.square( + dimension: 120, + child: CircleAvatar( + backgroundColor: Colors.white, + backgroundImage: profileBloc.image == null + ? null + : FileImage(profileBloc.image!), + child: profileBloc.image != null + ? null + : HomeCubit.user!.profilePicture != null + ? ClipOval( + child: Image.memory( + HomeCubit.user!.profilePicture!, + fit: BoxFit.cover, + width: 120, + height: 120, + ), + ) + : null, + ), + ), ), ), ), - ), - const SizedBox(width: 5), - InkWell( - onTap: () { - profileBloc.add(const ChangeNameEvent(value: true)); - }, - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 10), - child: Icon( - Icons.edit_outlined, - size: 20, - color: ColorsManager.textPrimaryColor, - ), - ), - ), - ], - ), - ), - const SizedBox(height: 10), - DefaultContainer( - padding: const EdgeInsets.symmetric( - horizontal: 25, - vertical: 5, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 20, bottom: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const BodyMedium(text: 'email '), - Flexible(child: BodyMedium(text: HomeCubit.user!.email ?? 'No Email')), - ], - ), - ), - Container( - height: 1, - color: ColorsManager.greyColor, - ), - InkWell( - onTap: () { - profileBloc.add(const ChangeNameEvent(value: false)); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const RegionPage(), - ), - ).then((result) { - profileBloc.add(InitialProfileEvent()); - }); - }, - child: Padding( - padding: const EdgeInsets.only(top: 20, bottom: 20), + const SizedBox(height: 20), + SizedBox( child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - const BodyMedium(text: 'Region '), - Flexible(child: BodyMedium(text: HomeCubit.user!.regionName ?? 'No Region')), - ], - ), - ), - ), - Container( - height: 1, - color: ColorsManager.greyColor, - ), - InkWell( - onTap: () { - profileBloc.add(const ChangeNameEvent(value: false)); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const TimeZoneScreenPage(), - ), - ).then((result) { - profileBloc.add(InitialProfileEvent()); - }); - }, - child: Padding( - padding: const EdgeInsets.only(top: 15, bottom: 15), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const BodyMedium(text: 'Time Zone '), - Flexible( - child: BodyMedium(text: HomeCubit.user!.timeZone ?? "No Time Zone"), + IntrinsicWidth( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 200), + child: TextFormField( + maxLength: 30, + style: const TextStyle( + color: Colors.black, + ), + textAlign: TextAlign.center, + focusNode: profileBloc.focusNode, + controller: profileBloc.nameController, + enabled: profileBloc.editName, + onEditingComplete: () { + profileBloc.add(SaveNameEvent(context: context)); + }, + decoration: const InputDecoration( + hintText: "Your Name", + border: InputBorder.none, + fillColor: Colors.white10, + counterText: '', + ), + ), + ), + ), + const SizedBox(width: 5), + InkWell( + onTap: () { + profileBloc.add(const ChangeNameEvent(value: true)); + }, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: Icon( + Icons.edit_outlined, + size: 20, + color: ColorsManager.textPrimaryColor, + ), + ), ), ], ), ), - ), - ], - ) - - ), - ], - ), + const SizedBox(height: 10), + DefaultContainer( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 5, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 20, bottom: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const BodyMedium(text: 'email '), + Flexible( + child: BodyMedium( + text: HomeCubit.user!.email ?? 'No Email')), + ], + ), + ), + Container( + height: 1, + color: ColorsManager.greyColor, + ), + InkWell( + onTap: () { + profileBloc.add(const ChangeNameEvent(value: false)); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const RegionPage(), + ), + ).then((result) { + profileBloc.add(InitialProfileEvent()); + }); + }, + child: Padding( + padding: const EdgeInsets.only(top: 20, bottom: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const BodyMedium(text: 'Region '), + Flexible( + child: BodyMedium( + text: HomeCubit.user!.regionName ?? 'No Region')), + ], + ), + ), + ), + Container( + height: 1, + color: ColorsManager.greyColor, + ), + InkWell( + onTap: () { + profileBloc.add(const ChangeNameEvent(value: false)); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const TimeZoneScreenPage(), + ), + ).then((result) { + profileBloc.add(InitialProfileEvent()); + }); + }, + child: Padding( + padding: const EdgeInsets.only(top: 15, bottom: 15), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const BodyMedium(text: 'Time Zone '), + Flexible( + child: BodyMedium( + text: HomeCubit.user!.timeZone ?? "No Time Zone"), + ), + ], + ), + ), + ), + ], + )), + ], + ), + ), ); }, ), diff --git a/pubspec.yaml b/pubspec.yaml index 6439621..57cc582 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ description: This is the mobile application project, developed with Flutter for # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: 1.0.2+15 +version: 1.0.2+16 environment: sdk: ">=3.0.6 <4.0.0"