Fixed design issues, added updated read me file

This commit is contained in:
Abdullah Alassaf
2024-08-01 12:57:13 +03:00
parent 296726ac82
commit 8d6da30d09
6 changed files with 297 additions and 214 deletions

View File

@ -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 For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials, [online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference. 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.

View File

@ -23,13 +23,12 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
String timeZoneSelected = ''; String timeZoneSelected = '';
String regionSelected = ''; String regionSelected = '';
final TextEditingController searchController = TextEditingController(); 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<RegionModel> allRegions = []; List<RegionModel> allRegions = [];
List<TimeZone> allTimeZone = []; List<TimeZone> allTimeZone = [];
ProfileBloc() : super(InitialState()) { ProfileBloc() : super(InitialState()) {
on<InitialProfileEvent>(_fetchUserInfo); on<InitialProfileEvent>(_fetchUserInfo);
on<TimeZoneInitialEvent>(_fetchTimeZone); on<TimeZoneInitialEvent>(_fetchTimeZone);
@ -53,7 +52,7 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
final nameParts = fullName.split(' '); final nameParts = fullName.split(' ');
final firstName = nameParts[0]; final firstName = nameParts[0];
final lastName = nameParts.length > 1 ? nameParts[1] : ''; 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()); add(InitialProfileEvent());
await HomeCubit.getInstance().fetchUserInfo(); await HomeCubit.getInstance().fetchUserInfo();
CustomSnackBar.displaySnackBar('Save Successfully'); CustomSnackBar.displaySnackBar('Save Successfully');
@ -125,7 +124,6 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
} }
} }
Future<void> searchRegion(SearchRegionEvent event, Emitter<ProfileState> emit) async { Future<void> searchRegion(SearchRegionEvent event, Emitter<ProfileState> emit) async {
emit(LoadingInitialState()); emit(LoadingInitialState());
final query = event.query.toLowerCase(); final query = event.query.toLowerCase();
@ -158,7 +156,6 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
} }
} }
void _fetchRegion(RegionInitialEvent event, Emitter<ProfileState> emit) async { void _fetchRegion(RegionInitialEvent event, Emitter<ProfileState> emit) async {
try { try {
emit(LoadingInitialState()); emit(LoadingInitialState());
@ -170,6 +167,7 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
} }
Future<void> _selectImage(SelectImageEvent event, Emitter<ProfileState> emit) async { Future<void> _selectImage(SelectImageEvent event, Emitter<ProfileState> emit) async {
try {
if (await _requestPermission()) { if (await _requestPermission()) {
emit(ChangeImageState()); emit(ChangeImageState());
final pickedFile = await _picker.pickImage(source: ImageSource.gallery); final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
@ -182,24 +180,24 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
image = null; image = null;
CustomSnackBar.displaySnackBar('Image size must be 1 MB or less'); CustomSnackBar.displaySnackBar('Image size must be 1 MB or less');
} else { } else {
emit(LoadingInitialState());
await _saveImage(); await _saveImage();
emit(ImageSelectedState());
} }
} else {
print('No image selected.');
} }
emit(ImageSelectedState()); emit(ImageSelectedState());
} else { } else {
_showPermissionDeniedDialog(event.context); _showPermissionDeniedDialog(event.context);
} }
} catch (_) {
emit(const FailedState(errorMessage: 'Something went wrong'));
}
} }
Future<void> _saveImage() async { Future<void> _saveImage() async {
emit(LoadingInitialState());
List<int> imageBytes = image!.readAsBytesSync(); List<int> imageBytes = image!.readAsBytesSync();
String base64Image = base64Encode(imageBytes); String base64Image = base64Encode(imageBytes);
print(base64Image); await ProfileApi.saveImage(base64Image);
var response = await ProfileApi.saveImage(base64Image);
emit(ImageSelectedState());
} }
void _showPermissionDeniedDialog(BuildContext context) { void _showPermissionDeniedDialog(BuildContext context) {
@ -229,9 +227,6 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
); );
} }
bool _validateInputs() { bool _validateInputs() {
final nameError = fullNameValidator(nameController.text); final nameError = fullNameValidator(nameController.text);
if (nameError != null) { if (nameError != null) {
@ -241,7 +236,6 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
return false; return false;
} }
String? fullNameValidator(String? value) { String? fullNameValidator(String? value) {
if (value == null) return 'Full name is required'; if (value == null) return 'Full name is required';
final withoutExtraSpaces = value.replaceAll(RegExp(r"\s+"), ' ').trim(); final withoutExtraSpaces = value.replaceAll(RegExp(r"\s+"), ' ').trim();
@ -261,7 +255,6 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
return null; return null;
} }
Future<bool> _requestPermission() async { Future<bool> _requestPermission() async {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid) { if (Platform.isAndroid) {
@ -301,5 +294,4 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
} }
} }
} }
} }

View File

@ -21,12 +21,11 @@ class MenuView extends StatelessWidget {
builder: (context, state) { builder: (context, state) {
return BlocBuilder<AuthCubit, AuthState>( return BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) { builder: (context, state) {
final profileBloc = BlocProvider.of<MenuCubit>(context);
return SingleChildScrollView( return SingleChildScrollView(
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
child: Column( child: Column(
children: [ children: [
ProfileTab(), const ProfileTab(),
for (var section in menuSections) for (var section in menuSections)
MenuList( MenuList(
section: section, section: section,

View File

@ -7,7 +7,9 @@ import 'package:syncrow_app/features/shared_widgets/text_widgets/body_medium.dar
import 'package:syncrow_app/features/shared_widgets/text_widgets/body_small.dart'; import 'package:syncrow_app/features/shared_widgets/text_widgets/body_small.dart';
class ProfileTab extends StatelessWidget { class ProfileTab extends StatelessWidget {
const ProfileTab({super.key,}); const ProfileTab({
super.key,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<HomeCubit, HomeState>( return BlocBuilder<HomeCubit, HomeState>(
@ -18,9 +20,10 @@ class ProfileTab extends StatelessWidget {
} }
Widget _buildProfileContent(BuildContext context) { Widget _buildProfileContent(BuildContext context) {
final homeCubit = context.read<HomeCubit>();
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 10,), padding: const EdgeInsets.symmetric(
vertical: 10,
),
child: InkWell( child: InkWell(
onTap: () { onTap: () {
Navigator.of(context) Navigator.of(context)
@ -28,7 +31,8 @@ class ProfileTab extends StatelessWidget {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const ProfileView(), builder: (context) => const ProfileView(),
), ),
).then((result) { )
.then((result) {
context.read<HomeCubit>().fetchUserInfo(); context.read<HomeCubit>().fetchUserInfo();
}); });
}, },
@ -47,7 +51,7 @@ class ProfileTab extends StatelessWidget {
Row( Row(
children: [ children: [
BodyMedium( BodyMedium(
text: '${HomeCubit.user!.firstName ?? ''} ', text: '${HomeCubit.user?.firstName ?? ''} ',
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
BodyMedium( BodyMedium(
@ -56,7 +60,9 @@ class ProfileTab extends StatelessWidget {
), ),
], ],
), ),
SizedBox(height: 5,), const SizedBox(
height: 5,
),
const BodySmall(text: "Syncrow Account"), const BodySmall(text: "Syncrow Account"),
], ],
), ),
@ -81,7 +87,7 @@ class ProfileTab extends StatelessWidget {
width: 110, width: 110,
height: 110, height: 110,
) )
: Icon(Icons.person, size: 70), // Fallback if no image : const Icon(Icons.person, size: 70), // Fallback if no image
), ),
), ),
), ),

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart';
import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_bloc.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_bloc.dart';
@ -25,10 +24,11 @@ class ProfileView extends StatelessWidget {
final profileBloc = BlocProvider.of<ProfileBloc>(context); final profileBloc = BlocProvider.of<ProfileBloc>(context);
return DefaultScaffold( return DefaultScaffold(
title: 'Syncrow Account', title: 'Syncrow Account',
child: child: state is LoadingInitialState
state is LoadingInitialState ? const Center(child: CircularProgressIndicator())
? const Center(child: CircularProgressIndicator()): : SizedBox(
Column( height: MediaQuery.sizeOf(context).height,
child: ListView(
children: [ children: [
SizedBox( SizedBox(
height: MediaQuery.of(context).size.height * 0.05, height: MediaQuery.of(context).size.height * 0.05,
@ -128,7 +128,9 @@ class ProfileView extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const BodyMedium(text: 'email '), const BodyMedium(text: 'email '),
Flexible(child: BodyMedium(text: HomeCubit.user!.email ?? 'No Email')), Flexible(
child: BodyMedium(
text: HomeCubit.user!.email ?? 'No Email')),
], ],
), ),
), ),
@ -154,7 +156,9 @@ class ProfileView extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const BodyMedium(text: 'Region '), const BodyMedium(text: 'Region '),
Flexible(child: BodyMedium(text: HomeCubit.user!.regionName ?? 'No Region')), Flexible(
child: BodyMedium(
text: HomeCubit.user!.regionName ?? 'No Region')),
], ],
), ),
), ),
@ -182,18 +186,18 @@ class ProfileView extends StatelessWidget {
children: [ children: [
const BodyMedium(text: 'Time Zone '), const BodyMedium(text: 'Time Zone '),
Flexible( Flexible(
child: BodyMedium(text: HomeCubit.user!.timeZone ?? "No Time Zone"), child: BodyMedium(
text: HomeCubit.user!.timeZone ?? "No Time Zone"),
), ),
], ],
), ),
), ),
), ),
], ],
) )),
),
], ],
), ),
),
); );
}, },
), ),

View File

@ -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. # 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 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: environment:
sdk: ">=3.0.6 <4.0.0" sdk: ">=3.0.6 <4.0.0"