From 32f0c7c8ede2cff6f1caa2c1f8603a50aeda949e Mon Sep 17 00:00:00 2001 From: Abdur Rafay Saleem <62943972+arafaysaleem@users.noreply.github.com> Date: Sun, 1 Aug 2021 20:28:05 +0500 Subject: [PATCH] Release 01-08-2021 (#63) --- .github/labeler.yml | 5 +- .github/workflows/PR-merge-build-release.yaml | 4 +- .github/workflows/PR-open-test-build.yaml | 2 +- .gitignore | 2 +- analysis_options.yaml | 18 +- android/app/src/main/AndroidManifest.xml | 4 +- codecov.yml | 10 + coverage/lcov.info | 2055 +++++++++++++++++ coverage_helper_script.bash | 9 + lib/enums/booking_status_enum.dart | 8 +- lib/enums/movie_type_enum.dart | 6 +- lib/enums/payment_method_enum.dart | 8 +- lib/enums/role_type_enum.dart | 6 +- lib/enums/show_status_enum.dart | 6 +- lib/enums/show_type_enum.dart | 6 +- lib/enums/theater_type_enum.dart | 6 +- lib/enums/user_role_enum.dart | 6 +- lib/helper/extensions/string_extension.dart | 2 +- lib/helper/utils/assets_helper.dart | 4 +- lib/helper/utils/constants.dart | 59 +- lib/helper/utils/form_validator.dart | 107 + lib/main.dart | 13 +- lib/models/booking_model.dart | 2 +- lib/models/movie_model.dart | 10 +- lib/models/movie_role_model.dart | 6 +- lib/models/payment_model.dart | 2 +- lib/models/show_model.dart | 3 +- lib/models/show_time_model.dart | 3 +- lib/models/theater_model.dart | 2 +- lib/models/user_payment_model.dart | 11 +- lib/providers/all_providers.dart | 65 +- lib/providers/auth_provider.dart | 65 +- lib/providers/bookings_provider.dart | 15 +- lib/providers/movies_provider.dart | 6 +- lib/providers/payments_provider.dart | 9 +- lib/providers/shows_provider.dart | 34 +- lib/providers/theaters_provider.dart | 8 +- lib/routes/app_router.dart | 27 +- .../local_storage/key_value_storage_base.dart | 93 + .../key_value_storage_service.dart | 90 + lib/services/local_storage/prefs_base.dart | 51 - lib/services/local_storage/prefs_service.dart | 79 - lib/services/networking/api_endpoint.dart | 96 +- lib/services/networking/api_service.dart | 27 +- lib/services/networking/dio_service.dart | 51 +- .../interceptors/api_interceptor.dart | 21 +- .../interceptors/logging_interceptor.dart | 58 +- .../refresh_token_interceptor.dart | 71 +- .../networking/network_exception.dart | 38 +- .../repositories/auth_repository.dart | 16 +- .../repositories/bookings_repository.dart | 10 +- .../repositories/movies_repository.dart | 6 +- .../repositories/payments_repository.dart | 6 +- .../repositories/shows_repository.dart | 6 +- .../repositories/theaters_repository.dart | 6 +- lib/views/screens/app_startup_screen.dart | 1 + lib/views/screens/change_password_screen.dart | 10 +- lib/views/screens/confirmation_screen.dart | 10 +- lib/views/screens/home_screen.dart | 10 +- lib/views/screens/login_screen.dart | 35 +- lib/views/screens/movie_details_screen.dart | 2 +- lib/views/screens/register_screen.dart | 188 +- lib/views/screens/shows_screen.dart | 8 +- lib/views/screens/trailer_screen.dart | 4 +- lib/views/screens/user_bookings_screen.dart | 2 +- lib/views/screens/welcome_screen.dart | 2 +- .../skeletons/shows_skeleton_loader.dart | 6 +- .../change_password_fields.dart | 48 +- .../change_password/save_password_button.dart | 2 +- .../widgets/common/custom_error_widget.dart | 4 +- .../widgets/common/custom_network_image.dart | 2 +- .../widgets/common/custom_textfield.dart | 2 +- lib/views/widgets/common/ratings.dart | 6 +- .../confirmation/more_bookings_button.dart | 2 +- .../confirmation/retry_payment_button.dart | 2 +- .../movie_details/movie_actors_list.dart | 2 +- .../movie_details/movie_details_column.dart | 2 +- .../movie_details/movie_details_sheet.dart | 14 +- .../movie_details/movie_summary_box.dart | 2 +- .../movie_details/play_button_widget.dart | 2 +- .../widgets/movies/movie_backdrop_view.dart | 2 +- .../widgets/movies/movie_overview_column.dart | 2 +- .../widgets/movies/white_movie_container.dart | 2 +- .../widgets/payment/billing_details.dart | 16 +- .../widgets/payment/mode_details_input.dart | 104 +- lib/views/widgets/payment/pay_button.dart | 2 +- .../widgets/payment/payment_options.dart | 8 +- .../theater/purchase_seats_button.dart | 2 +- .../theater/seat_color_indicators.dart | 6 +- .../confirm_bookings_button.dart | 4 +- .../ticket_summary/show_details_section.dart | 12 +- .../ticket_summary/ticket_details_list.dart | 6 +- .../user_bookings/booking_details_dialog.dart | 10 +- .../user_bookings/booking_summary_row.dart | 8 +- .../widgets/welcome/browse_movies_button.dart | 2 +- .../widgets/welcome/user_profile_details.dart | 16 +- .../widgets/welcome/view_bookings_button.dart | 2 +- pubspec.lock | 112 +- pubspec.yaml | 27 +- test/enums/booking_status_enum_test.dart | 46 + test/enums/movie_type_enum_test.dart | 46 + test/enums/payment_method_enum_test.dart | 68 + test/enums/role_type_enum_test.dart | 68 + test/enums/show_status_enum_test.dart | 69 + test/enums/show_type_enum_test.dart | 46 + test/enums/theater_type_enum_test.dart | 46 + test/enums/user_role_enum_test.dart | 46 + test/helper/utils/form_validator_test.dart | 242 ++ test/models/booking_model_test.dart | 423 ++++ test/models/genre_model_test.dart | 101 + test/models/movie_model_test.dart | 451 ++++ test/models/movie_role_model_test.dart | 133 ++ test/models/payment_model_test.dart | 362 +++ test/models/role_model_test.dart | 117 + test/models/seat_model_test.dart | 101 + test/models/show_model_test.dart | 210 ++ test/models/show_seating_model_test.dart | 137 ++ test/models/show_time_model_test.dart | 212 ++ test/models/theater_model_test.dart | 370 +++ test/models/user_booking_model_test.dart | 263 +++ test/models/user_booking_show_model_test.dart | 110 + test/models/user_model_test.dart | 165 ++ test/models/user_payment_model_test.dart | 244 ++ 123 files changed, 7254 insertions(+), 872 deletions(-) create mode 100644 codecov.yml create mode 100644 coverage/lcov.info create mode 100644 coverage_helper_script.bash create mode 100644 lib/helper/utils/form_validator.dart create mode 100644 lib/services/local_storage/key_value_storage_base.dart create mode 100644 lib/services/local_storage/key_value_storage_service.dart delete mode 100644 lib/services/local_storage/prefs_base.dart delete mode 100644 lib/services/local_storage/prefs_service.dart create mode 100644 test/enums/booking_status_enum_test.dart create mode 100644 test/enums/movie_type_enum_test.dart create mode 100644 test/enums/payment_method_enum_test.dart create mode 100644 test/enums/role_type_enum_test.dart create mode 100644 test/enums/show_status_enum_test.dart create mode 100644 test/enums/show_type_enum_test.dart create mode 100644 test/enums/theater_type_enum_test.dart create mode 100644 test/enums/user_role_enum_test.dart create mode 100644 test/helper/utils/form_validator_test.dart create mode 100644 test/models/booking_model_test.dart create mode 100644 test/models/genre_model_test.dart create mode 100644 test/models/movie_model_test.dart create mode 100644 test/models/movie_role_model_test.dart create mode 100644 test/models/payment_model_test.dart create mode 100644 test/models/role_model_test.dart create mode 100644 test/models/seat_model_test.dart create mode 100644 test/models/show_model_test.dart create mode 100644 test/models/show_seating_model_test.dart create mode 100644 test/models/show_time_model_test.dart create mode 100644 test/models/theater_model_test.dart create mode 100644 test/models/user_booking_model_test.dart create mode 100644 test/models/user_booking_show_model_test.dart create mode 100644 test/models/user_model_test.dart create mode 100644 test/models/user_payment_model_test.dart diff --git a/.github/labeler.yml b/.github/labeler.yml index dc35649..080914b 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -98,4 +98,7 @@ native/android: - any: ['android/**/*'] gradle: - - any: ['android/**/*'] \ No newline at end of file + - any: ['android/**/*'] + +tests: + - any: [ 'test/**/*' ] \ No newline at end of file diff --git a/.github/workflows/PR-merge-build-release.yaml b/.github/workflows/PR-merge-build-release.yaml index ed12af2..d76e195 100644 --- a/.github/workflows/PR-merge-build-release.yaml +++ b/.github/workflows/PR-merge-build-release.yaml @@ -28,6 +28,8 @@ jobs: run: flutter packages pub run build_runner build --delete-conflicting-outputs - name: Run Dart Analyzer run: flutter analyze . + - name: Run tests + run: flutter test assemble-release: name: Setup signing keys @@ -92,7 +94,7 @@ jobs: - name: Run build runner for codegen files run: flutter packages pub run build_runner build --delete-conflicting-outputs - name: Generate Splitted Release APKs - run: flutter build apk --target-platform android-arm,android-arm64 --split-per-abi --obfuscate --split-debug-info=./ez_tickets_app/debug_trace + run: flutter build apk --target-platform android-arm,android-arm64 --split-per-abi --obfuscate --split-debug-info=./ez_tickets_app/debug_trace --dart-define=BASE_URL=${{ secrets.BASE_URL }} - name: Remove bundled APK run: rm build/app/outputs/flutter-apk/app.apk - name: Upload Built APKs Artifact diff --git a/.github/workflows/PR-open-test-build.yaml b/.github/workflows/PR-open-test-build.yaml index 734bafe..fe37ffa 100644 --- a/.github/workflows/PR-open-test-build.yaml +++ b/.github/workflows/PR-open-test-build.yaml @@ -38,4 +38,4 @@ jobs: files: ./coverage/lcov.info verbose: true - name: Attempt Debug APK Build - run: flutter build apk --debug --dart-define=BASE_URL=${{ secrets.BASE_URL }} \ No newline at end of file + run: flutter build apk --debug --dart-define=BASE_URL=${{ secrets.BASE_URL }} diff --git a/.gitignore b/.gitignore index cc1361c..929d44e 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,4 @@ app.*.map.json *.gr.dart #keystore -*.jks \ No newline at end of file +*.jks diff --git a/analysis_options.yaml b/analysis_options.yaml index 44d6229..37c53c1 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1,4 @@ -include: package:effective_dart/analysis_options.yaml +include: package:flutter_lints/flutter.yaml analyzer: exclude: @@ -8,13 +8,23 @@ analyzer: errors: missing_required_param: error missing_return: error + prefer_const_constructors: error + prefer_const_constructors_in_immutables: error + todo: ignore + strong-mode: + implicit-casts: false + implicit-dynamic: false linter: rules: + prefer_single_quotes: true directives_ordering: false - public_member_api_docs: false #Enable at the end + prefer_double_quotes: false + use_key_in_widget_constructors: false + always_specify_types: false + unnecessary_final: false + public_member_api_docs: false + prefer_expression_function_bodies: false avoid_classes_with_only_static_members: false - prefer_const_constructors: true - prefer_const_constructors_in_immutables: true prefer_const_literals_to_create_immutables: true lines_longer_than_80_chars: false \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 50d8166..9406ff7 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,7 +3,9 @@ + android:icon="@mipmap/ic_launcher" + android:allowBackup="false" + android:fullBackupContent="false"> $file +echo "// Helper file to make coverage work for all dart files\n" > $file +echo "// ignore_for_file: unused_import" >> $file +packageName="$(cat pubspec.yaml| grep '^name: ' | awk '{print $2}')" +find lib '!' -path '*generated*/*' '!' -name '*.g.dart' '!' -name '*.freezed.dart' '!' -name '*.gr.dart' '!' -name '*.part.dart' -name '*.dart' | cut -c4- | awk -v package="$packageName" '{printf "import '\''package:%s%s'\'';\n", package, $1}' >> $file +echo "\nvoid main(){}" >> $file \ No newline at end of file diff --git a/lib/enums/booking_status_enum.dart b/lib/enums/booking_status_enum.dart index 6961e0f..00cf2cb 100644 --- a/lib/enums/booking_status_enum.dart +++ b/lib/enums/booking_status_enum.dart @@ -4,13 +4,13 @@ import 'package:freezed_annotation/freezed_annotation.dart'; /// A collection of statuses that bookings can have. enum BookingStatus { -@JsonValue("confirmed") CONFIRMED, -@JsonValue("cancelled") CANCELLED, -@JsonValue("reserved") RESERVED, +@JsonValue('confirmed') CONFIRMED, +@JsonValue('cancelled') CANCELLED, +@JsonValue('reserved') RESERVED, } /// A utility with extensions for enum name and serialized value. -extension ExtMovieType on BookingStatus{ +extension ExtBookingStatus on BookingStatus{ String get name => describeEnum(this); String get toJson => name.toLowerCase(); diff --git a/lib/enums/movie_type_enum.dart b/lib/enums/movie_type_enum.dart index 656761d..967996e 100644 --- a/lib/enums/movie_type_enum.dart +++ b/lib/enums/movie_type_enum.dart @@ -4,9 +4,9 @@ import 'package:freezed_annotation/freezed_annotation.dart'; /// A collection of types that movies can be. enum MovieType { - @JsonValue("now_showing") NOW_SHOWING, - @JsonValue("coming_soon") COMING_SOON, - @JsonValue("removed") REMOVED, + @JsonValue('now_showing') NOW_SHOWING, + @JsonValue('coming_soon') COMING_SOON, + @JsonValue('removed') REMOVED, ALL_MOVIES, } diff --git a/lib/enums/payment_method_enum.dart b/lib/enums/payment_method_enum.dart index c54ea80..ab2a910 100644 --- a/lib/enums/payment_method_enum.dart +++ b/lib/enums/payment_method_enum.dart @@ -6,13 +6,13 @@ import '../helper/extensions/string_extension.dart'; /// A collection of payment methods that a user can choose. enum PaymentMethod { -@JsonValue("cash") CASH, -@JsonValue("cod") COD, -@JsonValue("card") CARD, +@JsonValue('cash') CASH, +@JsonValue('cod') COD, +@JsonValue('card') CARD, } /// A utility with extensions for enum name and serialized value. -extension ExtRoleType on PaymentMethod { +extension ExtPaymentMethod on PaymentMethod { String get name => describeEnum(this); String get toJson => name.toLowerCase(); String get inString => name.capitalize; diff --git a/lib/enums/role_type_enum.dart b/lib/enums/role_type_enum.dart index 37a12f2..f9a9a54 100644 --- a/lib/enums/role_type_enum.dart +++ b/lib/enums/role_type_enum.dart @@ -6,9 +6,9 @@ import '../helper/extensions/string_extension.dart'; /// A collection of roles that movie actors can have. enum RoleType { - @JsonValue("director") DIRECTOR, - @JsonValue("producer") PRODUCER, - @JsonValue("cast") CAST, + @JsonValue('director') DIRECTOR, + @JsonValue('producer') PRODUCER, + @JsonValue('cast') CAST, } /// A utility with extensions for enum name and serialized value. diff --git a/lib/enums/show_status_enum.dart b/lib/enums/show_status_enum.dart index ee44be2..6735fe7 100644 --- a/lib/enums/show_status_enum.dart +++ b/lib/enums/show_status_enum.dart @@ -6,9 +6,9 @@ import '../helper/extensions/string_extension.dart'; /// A collection of statuses that a show can have. enum ShowStatus { - @JsonValue("free") FREE, - @JsonValue("almost_full") ALMOST_FULL, - @JsonValue("full") FULL, + @JsonValue('free') FREE, + @JsonValue('almost_full') ALMOST_FULL, + @JsonValue('full') FULL, } /// A utility with extensions for enum name and serialized value. diff --git a/lib/enums/show_type_enum.dart b/lib/enums/show_type_enum.dart index 6db9a55..dffe5d2 100644 --- a/lib/enums/show_type_enum.dart +++ b/lib/enums/show_type_enum.dart @@ -4,15 +4,15 @@ import 'package:freezed_annotation/freezed_annotation.dart'; /// A collection of types that a show can be. enum ShowType { - @JsonValue("2D") i2D, - @JsonValue("3D") i3D, + @JsonValue('2D') i2D, + @JsonValue('3D') i3D, } /// A utility with extensions for enum name and serialized value. extension ExtShowType on ShowType{ String get name => describeEnum(this); - String get toJson => name.substring(1).toLowerCase(); String get inString => name.substring(1); //removes i prefix + String get toJson => inString; } diff --git a/lib/enums/theater_type_enum.dart b/lib/enums/theater_type_enum.dart index 9094254..8f69672 100644 --- a/lib/enums/theater_type_enum.dart +++ b/lib/enums/theater_type_enum.dart @@ -4,12 +4,12 @@ import 'package:freezed_annotation/freezed_annotation.dart'; /// A collection of types that a theater can be. enum TheaterType { -@JsonValue("normal") NORMAL, -@JsonValue("royal") ROYAL, +@JsonValue('normal') NORMAL, +@JsonValue('royal') ROYAL, } /// A utility with extensions for enum name and serialized value. -extension ExtMovieType on TheaterType{ +extension ExtTheaterType on TheaterType{ String get name => describeEnum(this); String get toJson => name.toLowerCase(); diff --git a/lib/enums/user_role_enum.dart b/lib/enums/user_role_enum.dart index 8d53bac..9ae2543 100644 --- a/lib/enums/user_role_enum.dart +++ b/lib/enums/user_role_enum.dart @@ -4,9 +4,9 @@ import 'package:freezed_annotation/freezed_annotation.dart'; /// A collection of roles that a user can be. enum UserRole { - @JsonValue("admin") ADMIN, - @JsonValue("api_user") API_USER, - @JsonValue("super_user") SUPER_USER, + @JsonValue('admin') ADMIN, + @JsonValue('api_user') API_USER, + @JsonValue('super_user') SUPER_USER, } /// A utility with extensions for enum name and serialized value. diff --git a/lib/helper/extensions/string_extension.dart b/lib/helper/extensions/string_extension.dart index 56ab290..f963307 100644 --- a/lib/helper/extensions/string_extension.dart +++ b/lib/helper/extensions/string_extension.dart @@ -28,5 +28,5 @@ extension StringExt on String { String get capitalize => this[0].toUpperCase() + this.substring(1).toLowerCase(); /// An extension for replacing underscores in a String with spaces. - String get removeUnderScore => this.replaceAll("_", " "); + String get removeUnderScore => this.replaceAll('_', ' '); } diff --git a/lib/helper/utils/assets_helper.dart b/lib/helper/utils/assets_helper.dart index c96bc6a..86eb1f5 100644 --- a/lib/helper/utils/assets_helper.dart +++ b/lib/helper/utils/assets_helper.dart @@ -7,8 +7,8 @@ class AssetsHelper { const AssetsHelper._(); /// The path for face id image asset - static const String faceId = "assets/face_id.png"; + static const String faceId = 'assets/face_id.png'; /// The path for Pakistani flag image asset - static const String pkFlag = "assets/pk_flag.png"; + static const String pkFlag = 'assets/pk_flag.png'; } diff --git a/lib/helper/utils/constants.dart b/lib/helper/utils/constants.dart index ed7ec33..6bc5510 100644 --- a/lib/helper/utils/constants.dart +++ b/lib/helper/utils/constants.dart @@ -125,22 +125,67 @@ class Constants { ); /// The regular expression for validating contacts in the app. - static RegExp contactRegex = RegExp(r"^(03|3)\d{9}$"); + static RegExp contactRegex = RegExp(r'^(03|3)\d{9}$'); /// The regular expression for validating full names in the app. - static RegExp fullNameRegex = RegExp(r"^[a-zA-Z ]+$"); + static RegExp fullNameRegex = RegExp(r'^[a-zA-Z ]+$'); /// The regular expression for validating zip codes in the app. - static RegExp zipCodeRegex = RegExp(r"^\\d{5}$"); + static RegExp zipCodeRegex = RegExp(r'^\d{5}$'); /// The regular expression for validating credit card numbers in the app. - static RegExp creditCardNumberRegex = RegExp(r"^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14})$"); + static RegExp creditCardNumberRegex = RegExp(r'^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14})$'); /// The regular expression for validating credit card CVV in the app. - static RegExp creditCardCVVRegex = RegExp(r"^[0-9]{3}"); + static RegExp creditCardCVVRegex = RegExp(r'^[0-9]{3}$'); /// The regular expression for validating credit card expiry in the app. - static RegExp creditCardExpiryRegex = RegExp(r"(0[1-9]|10|11|12)/20[0-9]{2}$"); + static RegExp creditCardExpiryRegex = RegExp(r'(0[1-9]|10|11|12)/20[0-9]{2}$'); - static T? toNull(_) => null; + /// The error message for invalid email input. + static const invalidEmailError = 'Please enter a valid email address'; + + /// The error message for empty email input. + static const emptyEmailInputError = 'Please enter an email'; + + /// The error message for empty password input. + static const emptyPasswordInputError = 'Please enter a password'; + + /// The error message for invalid confirm password input. + static const invalidConfirmPwError = "Passwords don't match"; + + /// The error message for invalid current password input. + static const invalidCurrentPwError = 'Invalid current password!'; + + /// The error message for invalid new password input. + static const invalidNewPwError = "Current and new password can't be same"; + + /// The error message for invalid full name input. + static const invalidFullNameError = 'Please enter a valid full name'; + + /// The error message for empty address input. + static const emptyAddressInputError = 'Please enter a address'; + + /// The error message for empty cinema branch input. + static const emptyBranchInputError = 'Please enter the branch name'; + + /// The error message for invalid contact input. + static const invalidContactError = 'Please enter a valid contact'; + + /// The error message for invalid zip code input. + static const invalidZipCodeError = 'Please enter a valid zip code'; + + /// The error message for invalid promo code input. + static const invalidPromoCodeError = 'Please enter a valid promo code'; + + /// The error message for invalid credit card number input. + static const invalidCreditCardNumberError = 'Invalid credit card number'; + + /// The error message for invalid credit card CVV input. + static const invalidCreditCardCVVError = 'Please enter a valid CVV'; + + /// The error message for invalid credit card expiry input. + static const invalidCreditCardExpiryError = 'Please enter a valid expiry date'; + + static T? toNull(dynamic _) => null; } diff --git a/lib/helper/utils/form_validator.dart b/lib/helper/utils/form_validator.dart new file mode 100644 index 0000000..4c0e560 --- /dev/null +++ b/lib/helper/utils/form_validator.dart @@ -0,0 +1,107 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +//Helpers +import '../../helper/extensions/string_extension.dart'; +import 'constants.dart'; + +/// A utility class that holds methods for validating different textFields. +/// This class has no constructor and all methods are `static`. +@immutable +class FormValidator{ + const FormValidator._(); + + /// A method containing validation logic for email input. + static String? emailValidator(String? email){ + if(email == null || email.isEmpty) { + return Constants.emptyEmailInputError; + } else if (!email.isValidEmail) { + return Constants.invalidEmailError; + } + return null; + } + + /// A method containing validation logic for password input. + static String? passwordValidator(String? password) { + if (password!.isEmpty) return Constants.emptyPasswordInputError; + return null; + } + + /// A method containing validation logic for confirm password input. + static String? confirmPasswordValidator(String? confirmPw, String inputPw) { + if (confirmPw == inputPw.trim()) return null; + return Constants.invalidConfirmPwError; + } + + /// A method containing validation logic for current password input. + static String? currentPasswordValidator(String? inputPw, String currentPw) { + if (inputPw == currentPw) return null; + return Constants.invalidCurrentPwError; + } + + /// A method containing validation logic for new password input. + static String? newPasswordValidator(String? newPw, String currentPw) { + if (newPw!.isEmpty) { + return Constants.emptyPasswordInputError; + } + else if(newPw == currentPw) { + return Constants.invalidNewPwError; + } + return null; + } + + /// A method containing validation logic for full name input. + static String? fullNameValidator(String? fullName) { + if (fullName != null && fullName.isValidFullName) return null; + return Constants.invalidFullNameError; + } + + /// A method containing validation logic for address input. + static String? addressValidator(String? address) { + if (address!.isEmpty) return Constants.emptyAddressInputError; + return null; + } + + /// A method containing validation logic for contact number input. + static String? contactValidator(String? contact) { + if (contact != null && contact.isValidContact) return null; + return Constants.invalidContactError; + } + + /// A method containing validation logic for zipcode input. + static String? zipCodeValidator(String? zipCode) { + if (zipCode != null && zipCode.isValidZipCode) return null; + return Constants.invalidZipCodeError; + } + + /// A method containing validation logic for promo code input. + static String? promoCodeValidator(String? promoCode) { + if (promoCode != null && promoCode.length == 6) return null; + return Constants.invalidPromoCodeError; + } + + /// A method containing validation logic for cinema branch name input. + static String? branchNameValidator(String? branchName) { + if (branchName!.isEmpty) return Constants.emptyBranchInputError; + return null; + } + + /// A method containing validation logic for credit card number input. + static String? creditCardNumberValidator(String? ccNumber) { + if (ccNumber != null && ccNumber.isValidCreditCardNumber) return null; + return Constants.invalidCreditCardNumberError; + } + + /// A method containing validation logic for credit card CVV input. + static String? creditCardCVVValidator(String? cvv) { + if (cvv != null && cvv.isValidCreditCardCVV) return null; + return Constants.invalidCreditCardCVVError; + } + + /// A method containing validation logic for credit card expiry input. + static String? creditCardExpiryValidator(String? expiry) { + if (expiry != null && expiry.isValidCreditCardExpiry) return null; + return Constants.invalidCreditCardExpiryError; + } + +} diff --git a/lib/main.dart b/lib/main.dart index 3299f74..40e1e64 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -10,12 +11,12 @@ import 'helper/utils/custom_theme.dart'; import 'routes/app_router.gr.dart'; //Services -import 'services/local_storage/prefs_base.dart'; +import 'services/local_storage/key_value_storage_base.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); debugPrint = setDebugPrint; - await PrefsBase.init(); + await KeyValueStorageBase.init(); runApp(MyApp()); SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, @@ -24,10 +25,10 @@ void main() async { } void setDebugPrint(String? message, {int? wrapWidth}) { - final date = DateTime.now(); - var msg = "${date.year}/${date.month}/${date.day}"; - msg += " ${date.hour}:${date.minute}:${date.second}"; - msg += " $message"; + final date = clock.now(); + var msg = '${date.year}/${date.month}/${date.day}'; + msg += ' ${date.hour}:${date.minute}:${date.second}'; + msg += ' $message'; debugPrintSynchronously( msg, wrapWidth: wrapWidth, diff --git a/lib/models/booking_model.dart b/lib/models/booking_model.dart index 40a4c86..f414794 100644 --- a/lib/models/booking_model.dart +++ b/lib/models/booking_model.dart @@ -39,7 +39,7 @@ class BookingModel with _$BookingModel { seatNumber == null && price == null && bookingStatus == null && - bookingDatetime == null) return const {}; + bookingDatetime == null) return const {}; return copyWith( userId: userId, showId: showId ?? this.showId, diff --git a/lib/models/movie_model.dart b/lib/models/movie_model.dart index 04be22f..1f8a13c 100644 --- a/lib/models/movie_model.dart +++ b/lib/models/movie_model.dart @@ -33,10 +33,10 @@ class MovieModel with _$MovieModel { return MovieModel( movieId: null, year: 0, - title: "", - summary: "", - trailerUrl: "", - posterUrl: "", + title: '', + summary: '', + trailerUrl: '', + posterUrl: '', genres: [], movieType: MovieType.COMING_SOON, ); @@ -58,7 +58,7 @@ class MovieModel with _$MovieModel { posterUrl == null && rating == null && movieType == null - ) return const {}; + ) return const {}; return copyWith( movieId: movieId, year: year ?? this.year, diff --git a/lib/models/movie_role_model.dart b/lib/models/movie_role_model.dart index 13805bd..5f8befc 100644 --- a/lib/models/movie_role_model.dart +++ b/lib/models/movie_role_model.dart @@ -24,9 +24,9 @@ class MovieRoleModel with _$MovieRoleModel { _$MovieRoleModelFromJson(json); Map toCustomJson() { - return { - "role_id": role.roleId, - "role_type": roleType.toJson, + return { + 'role_id': role.roleId, + 'role_type': roleType.toJson, }; } } diff --git a/lib/models/payment_model.dart b/lib/models/payment_model.dart index d74f6d1..cfb686e 100644 --- a/lib/models/payment_model.dart +++ b/lib/models/payment_model.dart @@ -34,7 +34,7 @@ class PaymentModel with _$PaymentModel { showId == null && amount == null && paymentMethod == null && - paymentDatetime == null) return const {}; + paymentDatetime == null) return const {}; return copyWith( paymentId: paymentId, showId: showId ?? this.showId, diff --git a/lib/models/show_model.dart b/lib/models/show_model.dart index 8454a31..9339887 100644 --- a/lib/models/show_model.dart +++ b/lib/models/show_model.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'show_time_model.dart'; @@ -18,7 +19,7 @@ class ShowModel with _$ShowModel { factory ShowModel.initial(){ return ShowModel( - date: DateTime.now(), + date: clock.now(), movieId: 0, showTimes: const [] ); diff --git a/lib/models/show_time_model.dart b/lib/models/show_time_model.dart index d8152e5..3072cee 100644 --- a/lib/models/show_time_model.dart +++ b/lib/models/show_time_model.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import '../helper/utils/constants.dart'; @@ -23,7 +24,7 @@ class ShowTimeModel with _$ShowTimeModel { }) = _ShowTimeModel; factory ShowTimeModel.initial(){ - final dummyTime = DateTime.now(); + final dummyTime = clock.now(); return ShowTimeModel( startTime: dummyTime, endTime: dummyTime, diff --git a/lib/models/theater_model.dart b/lib/models/theater_model.dart index c687482..b393822 100644 --- a/lib/models/theater_model.dart +++ b/lib/models/theater_model.dart @@ -37,7 +37,7 @@ class TheaterModel with _$TheaterModel { theaterType == null && missing == null && blocked == null - ) return const {}; + ) return const {}; return copyWith( theaterId: theaterId, numOfRows: numOfRows ?? this.numOfRows, diff --git a/lib/models/user_payment_model.dart b/lib/models/user_payment_model.dart index 9840d53..55b7d2b 100644 --- a/lib/models/user_payment_model.dart +++ b/lib/models/user_payment_model.dart @@ -13,21 +13,22 @@ class UserPaymentModel with _$UserPaymentModel { required double amount, required DateTime paymentDatetime, required PaymentMethod paymentMethod, - required _UserPaymentMovieModel movie, + required UserPaymentMovieModel movie, }) = _UserPaymentModel; factory UserPaymentModel.fromJson(Map json) => _$UserPaymentModelFromJson(json); } @freezed -class _UserPaymentMovieModel with _$_UserPaymentMovieModel { +@visibleForTesting +class UserPaymentMovieModel with _$UserPaymentMovieModel { @JsonSerializable() - const factory _UserPaymentMovieModel({ + const factory UserPaymentMovieModel({ required String title, required String posterUrl, - }) = __UserPaymentMovieModel; + }) = _UserPaymentMovieModel; - factory _UserPaymentMovieModel.fromJson(Map json) => _$_UserPaymentMovieModelFromJson(json); + factory UserPaymentMovieModel.fromJson(Map json) => _$UserPaymentMovieModelFromJson(json); } diff --git a/lib/providers/all_providers.dart b/lib/providers/all_providers.dart index 6da8582..13d3979 100644 --- a/lib/providers/all_providers.dart +++ b/lib/providers/all_providers.dart @@ -1,10 +1,19 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -//services imports -import '../services/local_storage/prefs_service.dart'; +//Service imports +import '../services/networking/dio_service.dart'; +import '../services/local_storage/key_value_storage_service.dart'; import '../services/networking/api_service.dart'; +import '../services/networking/api_endpoint.dart'; -//repository imports +//Interceptor imports +import '../services/networking/interceptors/api_interceptor.dart'; +import '../services/networking/interceptors/logging_interceptor.dart'; +import '../services/networking/interceptors/refresh_token_interceptor.dart'; + +//Repository imports import '../services/repositories/auth_repository.dart'; import '../services/repositories/bookings_repository.dart'; import '../services/repositories/movies_repository.dart'; @@ -12,20 +21,46 @@ import '../services/repositories/payments_repository.dart'; import '../services/repositories/shows_repository.dart'; import '../services/repositories/theaters_repository.dart'; -//provider imports +//Provider imports import 'auth_provider.dart'; import 'bookings_provider.dart'; import 'movies_provider.dart'; import 'payments_provider.dart'; import 'shows_provider.dart'; -//states +//State imports import 'states/auth_state.dart'; import 'theaters_provider.dart'; -//service providers -final _apiServiceProvider = Provider((ref) => ApiService()); -final _prefsServiceProvider = Provider((ref) => PrefsService()); +//Services +final keyValueStorageServiceProvider = Provider( + (ref) => KeyValueStorageService(), +); + +final _dioProvider = Provider((ref) { + final baseOptions = BaseOptions( + baseUrl: ApiEndpoint.baseUrl, + ); + return Dio(baseOptions); +}); + +final _dioServiceProvider = Provider((ref) { + final _dio = ref.watch(_dioProvider); + // Order of interceptors very important + return DioService( + dioClient: _dio, + interceptors: [ + ApiInterceptor(ref), + if (kDebugMode) LoggingInterceptor(), + RefreshTokenInterceptor(dioClient: _dio, ref: ref) + ], + ); +}); + +final _apiServiceProvider = Provider((ref) { + final _dioService = ref.watch(_dioServiceProvider); + return ApiService(_dioService); +}); //repositories providers final _authRepositoryProvider = Provider((ref) { @@ -61,11 +96,11 @@ final _paymentsRepositoryProvider = Provider((ref) { //notifier providers final authProvider = StateNotifierProvider((ref) { final _authRepository = ref.watch(_authRepositoryProvider); - final _prefsService = ref.watch(_prefsServiceProvider); + final _keyValueStorageService = ref.watch(keyValueStorageServiceProvider); return AuthProvider( reader: ref.read, authRepository: _authRepository, - prefsService: _prefsService, + keyValueStorageService: _keyValueStorageService, ); }); @@ -88,11 +123,15 @@ final theatersProvider = ChangeNotifierProvider((ref) { final bookingsProvider = Provider((ref) { final _bookingsRepository = ref.watch(_bookingsRepositoryProvider); return BookingsProvider( - read: ref.read, bookingsRepository: _bookingsRepository); + read: ref.read, + bookingsRepository: _bookingsRepository, + ); }); final paymentsProvider = Provider((ref) { final _paymentsRepository = ref.watch(_paymentsRepositoryProvider); return PaymentsProvider( - read: ref.read, paymentsRepository: _paymentsRepository); + read: ref.read, + paymentsRepository: _paymentsRepository, + ); }); diff --git a/lib/providers/auth_provider.dart b/lib/providers/auth_provider.dart index c77fcac..c8bcd93 100644 --- a/lib/providers/auth_provider.dart +++ b/lib/providers/auth_provider.dart @@ -7,7 +7,7 @@ import '../enums/user_role_enum.dart'; import '../models/user_model.dart'; //Services -import '../services/local_storage/prefs_service.dart'; +import '../services/local_storage/key_value_storage_service.dart'; import '../services/networking/network_exception.dart'; import '../services/repositories/auth_repository.dart'; import 'states/auth_state.dart'; @@ -22,17 +22,16 @@ final changePasswordStateProvider = StateProvider( class AuthProvider extends StateNotifier { late UserModel? _currentUser; final AuthRepository _authRepository; - final PrefsService _prefsService; + final KeyValueStorageService _keyValueStorageService; final Reader _reader; - String _token = ""; - String _password = ""; + String _password = ''; AuthProvider({ required AuthRepository authRepository, - required PrefsService prefsService, + required KeyValueStorageService keyValueStorageService, required Reader reader, }) : _authRepository = authRepository, - _prefsService = prefsService, + _keyValueStorageService = keyValueStorageService, _reader = reader, super(const AuthState.unauthenticated()) { init(); @@ -40,8 +39,6 @@ class AuthProvider extends StateNotifier { int get currentUserId => _currentUser!.userId!; - String get token => _token; - String get currentUserFullName => _currentUser!.fullName; String get currentUserEmail => _currentUser!.email; @@ -53,21 +50,19 @@ class AuthProvider extends StateNotifier { String get currentUserPassword => _password; void updateToken(String value) { - _token = value; - _prefsService.setAuthToken(value); + _keyValueStorageService.setAuthToken(value); } void _updatePassword(String value) { _password = value; - _prefsService.setAuthPassword(value); + _keyValueStorageService.setAuthPassword(value); } - void init() { - final authenticated = _prefsService.getAuthState(); - _currentUser = _prefsService.getAuthUser(); - _password = _prefsService.getAuthPassword(); - _token = _prefsService.getAuthToken(); - if (!authenticated || _currentUser == null) { + void init() async { + final authenticated = _keyValueStorageService.getAuthState(); + _currentUser = _keyValueStorageService.getAuthUser(); + _password = await _keyValueStorageService.getAuthPassword(); + if (!authenticated || _currentUser == null || _password.isEmpty) { logout(); } else { state = AuthState.authenticated(fullName: _currentUser!.fullName); @@ -78,7 +73,7 @@ class AuthProvider extends StateNotifier { required String email, required String password, }) async { - final data = {"email": email, "password": password}; + final data = {'email': email, 'password': password}; state = const AuthState.authenticating(); try { _currentUser = await _authRepository.sendLoginData( @@ -87,7 +82,7 @@ class AuthProvider extends StateNotifier { ); state = AuthState.authenticated(fullName: _currentUser!.fullName); _updatePassword(password); - _updatePreferences(); + _updateAuthProfile(); } on NetworkException catch (e) { state = AuthState.failed(reason: e.message); } @@ -101,8 +96,8 @@ class AuthProvider extends StateNotifier { required String address, UserRole role = UserRole.API_USER, }) async { - if (contact.startsWith("0")) contact = contact.substring(1); - contact = "+92$contact"; + if (contact.startsWith('0')) contact = contact.substring(1); + contact = '+92$contact'; final user = UserModel( userId: null, fullName: fullName, @@ -119,14 +114,14 @@ class AuthProvider extends StateNotifier { ); state = AuthState.authenticated(fullName: _currentUser!.fullName); _updatePassword(password); - _updatePreferences(); + _updateAuthProfile(); } on NetworkException catch (e) { state = AuthState.failed(reason: e.message); } } Future forgotPassword(String email) async { - final data = {"email": email}; + final data = {'email': email}; return await _authRepository.sendForgotPasswordData(data: data); } @@ -134,7 +129,7 @@ class AuthProvider extends StateNotifier { required String email, required String password, }) async { - final data = {"email": email, "password": password}; + final data = {'email': email, 'password': password}; final result = await _authRepository.sendResetPasswordData(data: data); if (result) _updatePassword(password); return result; @@ -142,9 +137,9 @@ class AuthProvider extends StateNotifier { Future changePassword({required String newPassword}) async { final data = { - "email": currentUserEmail, - "password": currentUserPassword, - "new_password": newPassword, + 'email': currentUserEmail, + 'password': currentUserPassword, + 'new_password': newPassword, }; final _changePasswordState = _reader(changePasswordStateProvider); _changePasswordState.state = const FutureState.loading(); @@ -159,23 +154,21 @@ class AuthProvider extends StateNotifier { Future verifyOtp({required String email, required int otp}) async { final data = { - "email": email, - "OTP": otp, + 'email': email, + 'OTP': otp, }; return await _authRepository.sendOtpData(data: data); } - void _updatePreferences() { - _prefsService.setAuthState(state); - _prefsService.setAuthUser(_currentUser!); - _prefsService.setAuthToken(token); + void _updateAuthProfile() { + _keyValueStorageService.setAuthState(state); + _keyValueStorageService.setAuthUser(_currentUser!); } void logout() { - _token = ""; _currentUser = null; - _password = ""; + _password = ''; state = const AuthState.unauthenticated(); - _prefsService.resetPrefs(); + _keyValueStorageService.resetKeys(); } } diff --git a/lib/providers/bookings_provider.dart b/lib/providers/bookings_provider.dart index c08ac3d..2d37cc9 100644 --- a/lib/providers/bookings_provider.dart +++ b/lib/providers/bookings_provider.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; //Enums @@ -45,12 +46,12 @@ class BookingsProvider { int? userId, int? showId, }) async { - final Map? queryParams = { - if (bookingStatus != null) "booking_status": bookingStatus.toJson, + final Map? queryParams = { + if (bookingStatus != null) 'booking_status': bookingStatus.toJson, if (bookingDatetime != null) - "booking_datetime": bookingDatetime.toString(), - if (userId != null) "user_id": userId, - if (showId != null) "show_id": showId, + 'booking_datetime': bookingDatetime.toString(), + if (userId != null) 'user_id': userId, + if (showId != null) 'show_id': showId, }; return await _bookingsRepository.fetchFilteredBookings( queryParameters: queryParams); @@ -87,7 +88,7 @@ class BookingsProvider { seatNumber: seat.seatNumber, price: Constants.ticketPrice, bookingStatus: BookingStatus.RESERVED, - bookingDatetime: DateTime.now(), + bookingDatetime: clock.now(), ); bookingIds.add(newBooking.bookingId!); } @@ -108,7 +109,7 @@ class BookingsProvider { bookingId: null, userId: userId, showId: showId, - seat: "$seatRow-$seatNumber", + seat: '$seatRow-$seatNumber', price: price, bookingStatus: bookingStatus, bookingDatetime: bookingDatetime, diff --git a/lib/providers/movies_provider.dart b/lib/providers/movies_provider.dart index 294c711..3d40e02 100644 --- a/lib/providers/movies_provider.dart +++ b/lib/providers/movies_provider.dart @@ -48,7 +48,7 @@ class MoviesProvider { MovieType? movieType, }) async { final Map? queryParams = { - if (movieType != null) "movie_type": movieType.toJson, + if (movieType != null) 'movie_type': movieType.toJson, }; return await _moviesRepository.fetchAll(queryParameters: queryParams); } @@ -91,7 +91,7 @@ class MoviesProvider { movieRoles.map((movieRole) => movieRole.toCustomJson()).toList(); final data = { ...movie.toJson(), - "roles": roles, + 'roles': roles, }; final movieId = await _moviesRepository.create(data: data); return movie.copyWith(movieId: movieId); @@ -116,7 +116,7 @@ class MoviesProvider { rating: rating, movieType: movieType, ); - if (data.isEmpty) return "Nothing to update!"; + if (data.isEmpty) return 'Nothing to update!'; return await _moviesRepository.update(movieId: movie.movieId!, data: data); } diff --git a/lib/providers/payments_provider.dart b/lib/providers/payments_provider.dart index fd8668b..7fa1d10 100644 --- a/lib/providers/payments_provider.dart +++ b/lib/providers/payments_provider.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; //Enums @@ -45,7 +46,7 @@ class PaymentsProvider { PaymentMethod? paymentMethod, }) async { final Map? queryParams = { - if (paymentMethod != null) "payment_method": paymentMethod.toJson, + if (paymentMethod != null) 'payment_method': paymentMethod.toJson, }; return await _paymentsRepository.fetchAll(queryParameters: queryParams); } @@ -65,7 +66,7 @@ class PaymentsProvider { Future makePayment() async { final _paymentStateProv = _reader(paymentStateProvider); _paymentStateProv.state = const PaymentState.unprocessed(); - await Future.delayed(const Duration(seconds: 3)).then((_) { + await Future.delayed(const Duration(seconds: 3)).then((_) { _paymentStateProv.state = const PaymentState.processing(); }); final _activePaymentMethod = _reader(activePaymentModeProvider).state; @@ -107,7 +108,7 @@ class PaymentsProvider { // userId: userId, // showId: showId, // amount: amount, - // paymentDatetime: DateTime.now(), + // paymentDatetime: clock.now(), // bookingIds: bookingIds, // paymentMethod: _reader(activePaymentModeProvider).state, // ); @@ -124,7 +125,7 @@ class PaymentsProvider { userId: userId, showId: showId, amount: amount, - paymentDatetime: DateTime.now(), + paymentDatetime: clock.now(), bookingIds: bookingIds, paymentMethod: PaymentMethod.CARD, ); diff --git a/lib/providers/shows_provider.dart b/lib/providers/shows_provider.dart index 338b70e..bfc9e43 100644 --- a/lib/providers/shows_provider.dart +++ b/lib/providers/shows_provider.dart @@ -47,8 +47,8 @@ class ShowsProvider { Future> getAllShows({ required int movieId, }) async { - final Map? queryParams = { - "movie_id": movieId, + final Map? queryParams = { + 'movie_id': movieId, }; return await _showsRepository.fetchAll(queryParameters: queryParams); } @@ -70,13 +70,13 @@ class ShowsProvider { }) async { //TODO: Improve API for Show times and Show final data = { - "movie_id": movieId, - "theater_id": theaterId, - "start_time": startTime, - "end_time": endTime, - "date": date, - "show_type": showType.toJson, - "show_status": showStatus.toJson, + 'movie_id': movieId, + 'theater_id': theaterId, + 'start_time': startTime, + 'end_time': endTime, + 'date': date, + 'show_type': showType.toJson, + 'show_status': showStatus.toJson, }; final showId = await _showsRepository.create(data: data); final showTime = ShowTimeModel( @@ -103,15 +103,15 @@ class ShowsProvider { ShowStatus? showStatus, }) async { final data = { - if (movieId != null) "movie_id": movieId, - if (theaterId != null) "theater_id": theaterId, - if (startTime != null) "start_time": startTime, - if (endTime != null) "end_time": endTime, - if (date != null) "date": date, - if (showType != null) "show_type": showType.toJson, - if (showStatus != null) "show_status": showStatus.toJson, + if (movieId != null) 'movie_id': movieId, + if (theaterId != null) 'theater_id': theaterId, + if (startTime != null) 'start_time': startTime, + if (endTime != null) 'end_time': endTime, + if (date != null) 'date': date, + if (showType != null) 'show_type': showType.toJson, + if (showStatus != null) 'show_status': showStatus.toJson, }; - if (data.isEmpty) return "Nothing to update!"; + if (data.isEmpty) return 'Nothing to update!'; return await _showsRepository.update(showId: showId, data: data); } diff --git a/lib/providers/theaters_provider.dart b/lib/providers/theaters_provider.dart index 505b148..2b23f4f 100644 --- a/lib/providers/theaters_provider.dart +++ b/lib/providers/theaters_provider.dart @@ -17,7 +17,7 @@ import 'all_providers.dart'; //Providers import 'shows_provider.dart'; -final selectedTheaterNameProvider = StateProvider((_) => ""); +final selectedTheaterNameProvider = StateProvider((_) => ''); /// Does not use `ref.maintainState = true` bcz we wanted to load theater seats /// everytime because it can receive frequent updates. @@ -53,7 +53,7 @@ class TheatersProvider extends ChangeNotifier { UnmodifiableListView(_selectedSeats); List get selectedSeatNames => _selectedSeats - .map((seat) => "${seat.seatRow}-${seat.seatNumber}") + .map((seat) => '${seat.seatRow}-${seat.seatNumber}') .toList(); TheatersProvider(this._theatersRepository); @@ -73,7 +73,7 @@ class TheatersProvider extends ChangeNotifier { TheaterType? theaterType, }) async { final Map? queryParams = { - if (theaterType != null) "theater_type": theaterType.toJson, + if (theaterType != null) 'theater_type': theaterType.toJson, }; final theaters = await _theatersRepository.fetchAll(queryParameters: queryParams); for(var theater in theaters) { @@ -133,7 +133,7 @@ class TheatersProvider extends ChangeNotifier { missing: missing, blocked: blocked, ); - if (data.isEmpty) return "Nothing to update!"; + if (data.isEmpty) return 'Nothing to update!'; return await _theatersRepository.update( theaterId: theater.theaterId!, data: data); } diff --git a/lib/routes/app_router.dart b/lib/routes/app_router.dart index 2b23d12..0357e02 100644 --- a/lib/routes/app_router.dart +++ b/lib/routes/app_router.dart @@ -1,4 +1,5 @@ import 'package:auto_route/annotations.dart'; +import 'package:flutter/material.dart'; import '../views/screens/app_startup_screen.dart'; import '../views/screens/login_screen.dart'; @@ -16,19 +17,19 @@ import '../views/screens/change_password_screen.dart'; @MaterialAutoRouter( routes: [ - AutoRoute(page: AppStartupScreen, initial: true), - AutoRoute(page: RegisterScreen), - AutoRoute(page: LoginScreen), - AutoRoute(page: MoviesScreen), - AutoRoute(page: MovieDetailsScreen), - AutoRoute(page: TrailerScreen), - AutoRoute(page: ShowsScreen), - AutoRoute(page: TheaterScreen), - AutoRoute(page: TicketSummaryScreen), - AutoRoute(page: PaymentScreen), - AutoRoute(page: ConfirmationScreen), - AutoRoute(page: UserBookingsScreen), - AutoRoute(page: ChangePasswordScreen), + AutoRoute(page: AppStartupScreen, initial: true), + AutoRoute(page: RegisterScreen), + AutoRoute(page: LoginScreen), + AutoRoute(page: MoviesScreen), + AutoRoute(page: MovieDetailsScreen), + AutoRoute(page: TrailerScreen), + AutoRoute(page: ShowsScreen), + AutoRoute(page: TheaterScreen), + AutoRoute(page: TicketSummaryScreen), + AutoRoute(page: PaymentScreen), + AutoRoute(page: ConfirmationScreen), + AutoRoute(page: UserBookingsScreen), + AutoRoute(page: ChangePasswordScreen), ], ) class $AppRouter{} diff --git a/lib/services/local_storage/key_value_storage_base.dart b/lib/services/local_storage/key_value_storage_base.dart new file mode 100644 index 0000000..35899bb --- /dev/null +++ b/lib/services/local_storage/key_value_storage_base.dart @@ -0,0 +1,93 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Base class containing a unified API for key-value pairs' storage. +/// This class provides low level methods for storing: +/// - Sensitive keys using [FlutterSecureStorage] +/// - Insensitive keys using [SharedPreferences] +class KeyValueStorageBase{ + /// Instance of shared preferences + static SharedPreferences? _sharedPrefs; + + /// Instance of flutter secure storage + static FlutterSecureStorage? _secureStorage; + + /// Singleton instance of KeyValueStorage Helper + static KeyValueStorageBase? _instance; + + /// Private constructor + const KeyValueStorageBase._(); + + /// Get instance of this class + static KeyValueStorageBase get instance => _instance ?? const KeyValueStorageBase._(); + + /// Initializer for shared prefs and flutter secure storage + /// Should be called in main before runApp and + /// after WidgetsBinding.FlutterInitialized(), to allow for synchronous tasks + /// when possible. + static Future init() async { + _sharedPrefs ??= await SharedPreferences.getInstance(); + _secureStorage ??= const FlutterSecureStorage(); + } + + /// Reads the value for the key from common preferences storage + T? getCommon(String key) { + try{ + switch(T){ + case String: return _sharedPrefs!.getString(key) as T?; + case int: return _sharedPrefs!.getInt(key) as T?; + case bool: return _sharedPrefs!.getBool(key) as T?; + case double: return _sharedPrefs!.getDouble(key) as T?; + default: return _sharedPrefs!.get(key) as T?; + } + } on Exception { + return null; + } + } + + /// Reads the decrypted value for the key from secure storage + Future getEncrypted(String key) { + try { + return _secureStorage!.read(key: key); + } on PlatformException { + return Future.value(null); + } + } + + /// Sets the value for the key to common preferences storage + Future setCommon(String key, T value) { + switch(T){ + case String: return _sharedPrefs!.setString(key, value as String); + case int: return _sharedPrefs!.setInt(key, value as int); + case bool: return _sharedPrefs!.setBool(key, value as bool); + case double: return _sharedPrefs!.setDouble(key, value as double); + default: return _sharedPrefs!.setString(key, value as String); + } + } + + /// Sets the encrypted value for the key to secure storage + Future setEncrypted(String key, String value) { + try { + _secureStorage!.write(key: key, value: value); + return Future.value(true); + } on PlatformException catch (_) { + return Future.value(false); + } + } + + /// Erases common preferences keys + Future clearCommon() => _sharedPrefs!.clear(); + + /// Erases encrypted keys + Future clearEncrypted() async { + try { + await _secureStorage!.deleteAll(); + return true; + } on PlatformException catch (_) { + return false; + } + } +} diff --git a/lib/services/local_storage/key_value_storage_service.dart b/lib/services/local_storage/key_value_storage_service.dart new file mode 100644 index 0000000..a80ee35 --- /dev/null +++ b/lib/services/local_storage/key_value_storage_service.dart @@ -0,0 +1,90 @@ +import 'dart:convert'; + +//services +import 'key_value_storage_base.dart'; + +//models +import '../../models/user_model.dart'; + +//states +import '../../providers/states/auth_state.dart'; + +/// A service class for providing methods to store and retrieve key-value data +/// from common or secure storage. +class KeyValueStorageService { + + /// The name of auth token key + static const _authTokenKey = 'authToken'; + + /// The name of auth state key + static const _authStateKey = 'authStateKey'; + + /// The name of user password key + static const _authPasswordKey = 'authPasswordKey'; + + /// The name of user model key + static const _authUserKey = 'authUserKey'; + + /// Instance of key-value storage base class + final _keyValueStorage = KeyValueStorageBase.instance; + + /// Returns logged in user password + Future getAuthPassword() async { + return await _keyValueStorage.getEncrypted(_authPasswordKey) ?? ''; + } + + /// Returns last authentication status + bool getAuthState() { + return _keyValueStorage.getCommon(_authStateKey) ?? false; + } + + /// Returns last authenticated user + UserModel? getAuthUser() { + final user = _keyValueStorage.getCommon(_authUserKey); + if(user == null) return null; + return UserModel.fromJson(jsonDecode(user) as Map); + } + + /// Returns last authentication token + Future getAuthToken() async { + return await _keyValueStorage.getEncrypted(_authTokenKey) ?? ''; + } + + /// Sets the authentication password to this value. Even though this method is + /// asynchronous, we don't care about it's completion which is why we don't + /// use `await` and let it execute in the background. + void setAuthPassword(String password) { + _keyValueStorage.setEncrypted(_authPasswordKey, password); + } + + /// Sets the authentication status to this value. Even though this method is + /// asynchronous, we don't care about it's completion which is why we don't + /// use `await` and let it execute in the background. + void setAuthState(AuthState authState) { + if(authState is AUTHENTICATED) { + _keyValueStorage.setCommon(_authStateKey, true); + } + } + + /// Sets the authenticated user to this value. Even though this method is + /// asynchronous, we don't care about it's completion which is why we don't + /// use `await` and let it execute in the background. + void setAuthUser(UserModel user) { + _keyValueStorage.setCommon(_authUserKey, jsonEncode(user.toJson())); + } + + /// Sets the authentication token to this value. Even though this method is + /// asynchronous, we don't care about it's completion which is why we don't + /// use `await` and let it execute in the background. + void setAuthToken(String token) { + _keyValueStorage.setEncrypted(_authTokenKey, token); + } + + /// Resets the authentication. Even though these methods are asynchronous, we + /// don't care about their completion which is why we don't use `await` and + /// let them execute in the background. + void resetKeys() { + _keyValueStorage.clearCommon(); + _keyValueStorage.clearEncrypted(); + } +} diff --git a/lib/services/local_storage/prefs_base.dart b/lib/services/local_storage/prefs_base.dart deleted file mode 100644 index c61f56e..0000000 --- a/lib/services/local_storage/prefs_base.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:shared_preferences/shared_preferences.dart'; - -///Base class for shared preferences methods -///This class provides low level preferences methods -class PrefsBase{ - ///Instance of shared preferences - static SharedPreferences? _sharedPrefs; - - ///Singleton instance of Preferences Helper - static PrefsBase? _instance; - - ///Private constructor - const PrefsBase._(); - - ///Get instance of this class - static PrefsBase get instance => _instance ?? const PrefsBase._(); - - ///Initializer for shared prefs - ///Should be called in main before runApp and - ///after WidgetsBinding.FlutterInitialized() - static Future init() async { - if(_sharedPrefs == null ) { - _sharedPrefs = await SharedPreferences.getInstance(); - } - } - - ///Loads value for the key from preferences - T? get(String key) { - switch(T){ - case String: return _sharedPrefs!.getString(key) as T?; - case int: return _sharedPrefs!.getInt(key) as T?; - case bool: return _sharedPrefs!.getBool(key) as T?; - case double: return _sharedPrefs!.getDouble(key) as T?; - default: return _sharedPrefs!.getString(key) as T?; - } - } - - ///Sets the value for the key to preferences - Future set(String key, T value) { - switch(T){ - case String: return _sharedPrefs!.setString(key, value as String); - case int: return _sharedPrefs!.setInt(key, value as int); - case bool: return _sharedPrefs!.setBool(key, value as bool); - case double: return _sharedPrefs!.setDouble(key, value as double); - default: return _sharedPrefs!.setString(key, value as String); - } - } - - ///Resets preferences - void clear() => _sharedPrefs!.clear(); -} diff --git a/lib/services/local_storage/prefs_service.dart b/lib/services/local_storage/prefs_service.dart deleted file mode 100644 index a657384..0000000 --- a/lib/services/local_storage/prefs_service.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'dart:convert'; - -//services -import 'prefs_base.dart'; - -//models -import '../../models/user_model.dart'; - -//states -import '../../providers/states/auth_state.dart'; - -/// A service class for providing methods to store and retrieve data from -/// shared preferences. -class PrefsService { - - /// The name of auth token key - static const _authTokenKey = "authToken"; - - /// The name of auth state key - static const _authStateKey = "authStateKey"; - - /// The name of user password key - static const _authPasswordKey = "authPasswordKey"; - - /// The name of user model key - static const _authUserKey = "authUserKey"; - - ///Instance of prefs class - final _prefs = PrefsBase.instance; - - ///Returns logged in user password - String getAuthPassword() { - return _prefs.get(_authPasswordKey) ?? ''; - } - - ///Returns last authentication status - bool getAuthState() { - return _prefs.get(_authStateKey) ?? false; - } - - ///Returns last authenticated user - UserModel? getAuthUser() { - final user = _prefs.get(_authUserKey); - if(user == null) return null; - return UserModel.fromJson(jsonDecode(user)); - } - - ///Returns last authentication token - String getAuthToken() { - return _prefs.get(_authTokenKey) ?? ''; - } - - ///Sets the authentication password to this value - void setAuthPassword(String password) { - _prefs.set(_authPasswordKey, password); - } - - ///Sets the authentication status to this value - void setAuthState(AuthState authState) { - if(authState is AUTHENTICATED) { - _prefs.set(_authStateKey, true); - } - } - - ///Sets the authenticated user to this value - void setAuthUser(UserModel user) { - _prefs.set(_authUserKey, jsonEncode(user.toJson())); - } - - ///Sets the authentication token to this value - void setAuthToken(String token) { - _prefs.set(_authTokenKey, token); - } - - ///Resets the authentication - void resetPrefs() { - _prefs.clear(); - } -} diff --git a/lib/services/networking/api_endpoint.dart b/lib/services/networking/api_endpoint.dart index 6a6f3f3..20990fa 100644 --- a/lib/services/networking/api_endpoint.dart +++ b/lib/services/networking/api_endpoint.dart @@ -9,17 +9,31 @@ import 'package:flutter/material.dart'; class ApiEndpoint { const ApiEndpoint._(); + /// The base url of our REST API, to which all the requests will be sent. + /// It is supplied at the time of building the apk or running the app: + /// ``` + /// flutter build apk --debug --dart-define=BASE_URL=www.some_url.com + /// ``` + /// OR + /// ``` + /// flutter run --dart-define=BASE_URL=www.some_url.com + /// ``` + static const baseUrl = String.fromEnvironment( + 'BASE_URL', + defaultValue: 'localhost:3000/api/v1', + ); + /// Returns the path for an authentication [endpoint]. static String auth(AuthEndpoint endpoint) { - var path = "/auth"; + var path = '/auth'; switch (endpoint) { - case AuthEndpoint.REGISTER: return "$path/register"; - case AuthEndpoint.LOGIN: return "$path/login"; - case AuthEndpoint.REFRESH_TOKEN: return "$path/token"; - case AuthEndpoint.FORGOT_PASSWORD: return "$path/password/forgot"; - case AuthEndpoint.RESET_PASSWORD: return "$path/password/reset"; - case AuthEndpoint.CHANGE_PASSWORD: return "$path/password/change"; - case AuthEndpoint.VERIFY_OTP: return "$path/password/otp"; + case AuthEndpoint.REGISTER: return '$path/register'; + case AuthEndpoint.LOGIN: return '$path/login'; + case AuthEndpoint.REFRESH_TOKEN: return '$path/token'; + case AuthEndpoint.FORGOT_PASSWORD: return '$path/password/forgot'; + case AuthEndpoint.RESET_PASSWORD: return '$path/password/reset'; + case AuthEndpoint.CHANGE_PASSWORD: return '$path/password/change'; + case AuthEndpoint.VERIFY_OTP: return '$path/password/otp'; } } @@ -27,12 +41,12 @@ class ApiEndpoint { /// /// Specify user [id] to get the path for a specific user. static String users(UserEndpoint endpoint, {int? id}) { - var path = "/users"; + var path = '/users'; switch(endpoint){ case UserEndpoint.BASE: return path; case UserEndpoint.BY_ID: { - assert(id != null, "userId is required for BY_ID endpoint"); - return "$path/id/$id"; + assert(id != null, 'userId is required for BY_ID endpoint'); + return '$path/id/$id'; } } } @@ -41,16 +55,16 @@ class ApiEndpoint { /// /// Specify movie [id] for any endpoints involving a specific movie. static String movies(MovieEndpoint endpoint, {int? id}) { - var path = "/movies"; + var path = '/movies'; switch (endpoint) { case MovieEndpoint.BASE: return path; case MovieEndpoint.BY_ID: { - assert(id != null, "movieId is required for BY_ID endpoint"); - return "$path/id/$id"; + assert(id != null, 'movieId is required for BY_ID endpoint'); + return '$path/id/$id'; } case MovieEndpoint.ROLES: { - assert(id != null, "movieId is required for ROLES endpoint"); - return "$path/id/$id/roles"; + assert(id != null, 'movieId is required for ROLES endpoint'); + return '$path/id/$id/roles'; } } } @@ -59,16 +73,16 @@ class ApiEndpoint { /// /// Specify role [id] for any endpoints involving a specific role. static String roles(RoleEndpoint endpoint, {int? id}) { - var path = "/roles"; + var path = '/roles'; switch (endpoint) { case RoleEndpoint.BASE: return path; case RoleEndpoint.BY_ID: { - assert(id != null, "roleId is required for BY_ID endpoint"); - return "$path/id/$id"; + assert(id != null, 'roleId is required for BY_ID endpoint'); + return '$path/id/$id'; } case RoleEndpoint.MOVIES: { - assert(id != null, "roleId is required for MOVIES endpoint"); - return "$path/id/$id/movies"; + assert(id != null, 'roleId is required for MOVIES endpoint'); + return '$path/id/$id/movies'; } } } @@ -77,13 +91,13 @@ class ApiEndpoint { /// /// Specify show [id] for any endpoints involving an individual show. static String shows(ShowEndpoint endpoint, {int? id}) { - var path = "/shows"; + var path = '/shows'; switch(endpoint){ case ShowEndpoint.BASE: return path; - case ShowEndpoint.FILTERS: return "$path/filters"; + case ShowEndpoint.FILTERS: return '$path/filters'; case ShowEndpoint.BY_ID: { - assert(id != null, "showId is required for BY_ID endpoint"); - return "$path/id/$id"; + assert(id != null, 'showId is required for BY_ID endpoint'); + return '$path/id/$id'; } } } @@ -92,12 +106,12 @@ class ApiEndpoint { /// /// Specify theater [id] for any endpoints involving an individual theater. static String theaters(TheaterEndpoint endpoint, {int? id}) { - var path = "/theaters"; + var path = '/theaters'; switch(endpoint){ case TheaterEndpoint.BASE: return path; case TheaterEndpoint.BY_ID: { - assert(id != null, "theaterId is required for BY_ID endpoint"); - return "$path/id/$id"; + assert(id != null, 'theaterId is required for BY_ID endpoint'); + return '$path/id/$id'; } } } @@ -106,21 +120,21 @@ class ApiEndpoint { /// /// Specify booking [id] for any endpoints involving an individual booking. static String bookings(BookingEndpoint endpoint, {int? id}) { - var path = "/bookings"; + var path = '/bookings'; switch(endpoint){ case BookingEndpoint.BASE: return path; - case BookingEndpoint.FILTERS: return "$path/filters"; + case BookingEndpoint.FILTERS: return '$path/filters'; case BookingEndpoint.USERS: { - assert(id != null, "bookingId is required for USERS endpoint"); - return "$path/users/$id"; + assert(id != null, 'bookingId is required for USERS endpoint'); + return '$path/users/$id'; } case BookingEndpoint.SHOWS: { - assert(id != null, "bookingId is required for SHOWS endpoint"); - return "$path/shows/$id"; + assert(id != null, 'bookingId is required for SHOWS endpoint'); + return '$path/shows/$id'; } case BookingEndpoint.BY_ID: { - assert(id != null, "bookingId is required for BY_ID endpoint"); - return "$path/id/$id"; + assert(id != null, 'bookingId is required for BY_ID endpoint'); + return '$path/id/$id'; } } } @@ -129,16 +143,16 @@ class ApiEndpoint { /// /// Specify payment [id] for any endpoints involving an individual payment. static String payments(PaymentEndpoint endpoint, {int? id}) { - var path = "/payments"; + var path = '/payments'; switch(endpoint){ case PaymentEndpoint.BASE: return path; case PaymentEndpoint.USERS: { - assert(id != null, "paymentId is required for USERS endpoint"); - return "$path/users/$id"; + assert(id != null, 'paymentId is required for USERS endpoint'); + return '$path/users/$id'; } case PaymentEndpoint.BY_ID: { - assert(id != null, "paymentId is required for BY_ID endpoint"); - return "$path/id/$id"; + assert(id != null, 'paymentId is required for BY_ID endpoint'); + return '$path/id/$id'; } } } diff --git a/lib/services/networking/api_service.dart b/lib/services/networking/api_service.dart index c4497f9..40dda2b 100644 --- a/lib/services/networking/api_service.dart +++ b/lib/services/networking/api_service.dart @@ -10,15 +10,8 @@ class ApiService implements ApiInterface{ late final DioService _dioService; /// A public constructor that is used to initialize the API service - /// with [BaseOptions] and setup the underlying [_dioService]. - ApiService() { - final options = BaseOptions( - baseUrl: "https://ez-tickets-backend.herokuapp.com/api/v1", - ); - _dioService = DioService( - baseOptions: options, - ); - } + /// and setup the underlying [_dioService]. + ApiService(DioService dioService) : _dioService = dioService; /// An implementation of the base method for requesting collection of data /// from the [endpoint]. @@ -48,16 +41,16 @@ class ApiService implements ApiInterface{ //Entire map of response final data = await _dioService.get( endpoint: endpoint, - options: Options(headers: {"requiresAuthToken": requiresAuthToken}), + options: Options(headers: {'requiresAuthToken': requiresAuthToken}), queryParams: queryParams, cancelToken: cancelToken, ); //Items of table as json - final List body = data['body']; + final List body = data['body'] as List; //Returning the deserialized objects - return body.map((dataMap) => converter(dataMap)).toList(); + return body.map((dynamic dataMap) => converter(dataMap as Map)).toList(); } /// An implementation of the base method for requesting a document of data @@ -89,12 +82,12 @@ class ApiService implements ApiInterface{ final data = await _dioService.get( endpoint: endpoint, queryParams: queryParams, - options: Options(headers: {"requiresAuthToken": requiresAuthToken}), + options: Options(headers: {'requiresAuthToken': requiresAuthToken}), cancelToken: cancelToken, ); //Returning the deserialized object - return converter(data['body']); + return converter(data['body'] as Map); } /// An implementation of the base method for inserting [data] at @@ -125,7 +118,7 @@ class ApiService implements ApiInterface{ final dataMap = await _dioService.post( endpoint: endpoint, data: data, - options: Options(headers: {"requiresAuthToken": requiresAuthToken}), + options: Options(headers: {'requiresAuthToken': requiresAuthToken}), cancelToken: cancelToken, ); @@ -160,7 +153,7 @@ class ApiService implements ApiInterface{ final dataMap = await _dioService.patch( endpoint: endpoint, data: data, - options: Options(headers: {"requiresAuthToken": requiresAuthToken}), + options: Options(headers: {'requiresAuthToken': requiresAuthToken}), cancelToken: cancelToken, ); @@ -195,7 +188,7 @@ class ApiService implements ApiInterface{ final dataMap = await _dioService.delete( endpoint: endpoint, data: data, - options: Options(headers: {"requiresAuthToken": requiresAuthToken}), + options: Options(headers: {'requiresAuthToken': requiresAuthToken}), cancelToken: cancelToken, ); diff --git a/lib/services/networking/dio_service.dart b/lib/services/networking/dio_service.dart index edfc9b9..87b0699 100644 --- a/lib/services/networking/dio_service.dart +++ b/lib/services/networking/dio_service.dart @@ -1,19 +1,13 @@ import 'dart:async'; import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; -//Interceptors -import 'interceptors/api_interceptor.dart'; -import 'interceptors/logging_interceptor.dart'; -import 'interceptors/refresh_token_interceptor.dart'; //Exceptions import 'network_exception.dart'; /// A service class that wraps the [Dio] instance and provides methods for /// basic network requests. class DioService { - /// An instance of [Dio] for executing network requests. late final Dio _dio; @@ -21,30 +15,13 @@ class DioService { /// network requests. late final CancelToken _cancelToken; - /// A public constructor that is used to create the Dio service - /// with [baseOptions]. + /// A public constructor that is used to create a Dio service and initialize + /// the underlying [Dio] client. /// - /// Calls [createDio()] to setup the underlying [_dio] instance. - DioService({required BaseOptions baseOptions}) { - createDio(baseOptions); - } - - /// An method to create new instance of [Dio] with [baseOptions]. - /// - /// Attaches any external [Interceptors] to [_dio]. - /// - /// * [ApiInterceptor] handles token injection and response success validation - /// * [LoggingInterceptor] performs logging of all network requests, only - /// in debug mode - /// * [RefreshTokenInterceptor] refreshes an expired token. - void createDio(BaseOptions baseOptions) { - _cancelToken = CancelToken(); - _dio = Dio(baseOptions); - _dio.interceptors.addAll([ - ApiInterceptor(), - if (kDebugMode) LoggingInterceptor(), - RefreshTokenInterceptor(_dio), - ]); + /// Attaches any external [Interceptor]s to the underlying [_dio] client. + DioService({required Dio dioClient, Iterable? interceptors}) + : _dio = dioClient, _cancelToken = CancelToken() { + if (interceptors != null) _dio.interceptors.addAll(interceptors); } /// This method invokes the [cancel()] method on either the input @@ -77,13 +54,13 @@ class DioService { CancelToken? cancelToken, }) async { try { - final response = await _dio.get( + final response = await _dio.get>( endpoint, queryParameters: queryParams, options: options, cancelToken: cancelToken ?? _cancelToken, ); - return response.data; + return response.data as Map; } on Exception catch (ex) { throw NetworkException.getDioException(ex); } @@ -108,13 +85,13 @@ class DioService { CancelToken? cancelToken, }) async { try { - final response = await _dio.post( + final response = await _dio.post>( endpoint, data: data, options: options, cancelToken: cancelToken ?? _cancelToken, ); - return response.data; + return response.data as Map; } on Exception catch (ex) { throw NetworkException.getDioException(ex); } @@ -139,13 +116,13 @@ class DioService { CancelToken? cancelToken, }) async { try { - final response = await _dio.put( + final response = await _dio.put>( endpoint, data: data, options: options, cancelToken: cancelToken ?? _cancelToken, ); - return response.data; + return response.data as Map; } on Exception catch (ex) { throw NetworkException.getDioException(ex); } @@ -170,13 +147,13 @@ class DioService { CancelToken? cancelToken, }) async { try { - final response = await _dio.delete( + final response = await _dio.delete>( endpoint, data: data, options: options, cancelToken: cancelToken ?? _cancelToken, ); - return response.data; + return response.data as Map; } on Exception catch (ex) { throw NetworkException.getDioException(ex); } diff --git a/lib/services/networking/interceptors/api_interceptor.dart b/lib/services/networking/interceptors/api_interceptor.dart index d7f95bd..eb33a5a 100644 --- a/lib/services/networking/interceptors/api_interceptor.dart +++ b/lib/services/networking/interceptors/api_interceptor.dart @@ -1,14 +1,19 @@ import 'package:dio/dio.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../providers/all_providers.dart'; /// A class that holds intercepting logic for API related requests. This is /// the first interceptor in case of both request and response. /// +/// Primary purpose is to handle token injection and response success validation +/// /// Since this interceptor isn't responsible for error handling, if an exception /// occurs it is passed on the next [Interceptor] or to [Dio]. class ApiInterceptor extends Interceptor { + late final ProviderReference _ref; + + ApiInterceptor(this._ref) : super(); /// This method intercepts an out-going request before it reaches the /// destination. @@ -30,14 +35,14 @@ class ApiInterceptor extends Interceptor { void onRequest( RequestOptions options, RequestInterceptorHandler handler, - ) { - if (options.headers.containsKey("requiresAuthToken")) { - if(options.headers["requiresAuthToken"]){ - final token = ProviderContainer().read(authProvider.notifier).token; - options.headers.addAll({'Authorization': 'Bearer $token'}); + ) async { + if (options.headers.containsKey('requiresAuthToken')) { + if(options.headers['requiresAuthToken'] == true){ + final token = await _ref.read(keyValueStorageServiceProvider).getAuthToken(); + options.headers.addAll({'Authorization': 'Bearer $token'}); } - options.headers.remove("requiresAuthToken"); + options.headers.remove('requiresAuthToken'); } return handler.next(options); } @@ -72,7 +77,7 @@ class ApiInterceptor extends Interceptor { Response response, ResponseInterceptorHandler handler, ) { - final success = response.data["headers"]["success"] == 1; + final success = response.data['headers']['success'] == 1; if (success) return handler.next(response); diff --git a/lib/services/networking/interceptors/logging_interceptor.dart b/lib/services/networking/interceptors/logging_interceptor.dart index 678cb0e..1c94ecf 100644 --- a/lib/services/networking/interceptors/logging_interceptor.dart +++ b/lib/services/networking/interceptors/logging_interceptor.dart @@ -1,13 +1,13 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; - import 'package:dio/dio.dart'; /// A class that intercepts network requests for logging purposes only. This is /// the second interceptor in case of both request and response. /// -/// ** This interceptor doesn't modify the request or response in any way. ** +/// ** This interceptor doesn't modify the request or response in any way. And +/// only works in `debug` mode ** class LoggingInterceptor extends Interceptor { /// This method intercepts an out-going request before it reaches the @@ -36,21 +36,21 @@ class LoggingInterceptor extends Interceptor { final httpMethod = options.method.toUpperCase(); final url = options.baseUrl + options.path; - debugPrint("--> $httpMethod $url"); //GET www.example.com/mock_path/all + debugPrint('--> $httpMethod $url'); //GET www.example.com/mock_path/all - debugPrint("\tHeaders:"); - options.headers.forEach((k, v) => debugPrint('\t\t$k: $v')); + debugPrint('\tHeaders:'); + options.headers.forEach((k, dynamic v) => debugPrint('\t\t$k: $v')); if(options.queryParameters.isNotEmpty){ - debugPrint("\tqueryParams:"); - options.queryParameters.forEach((k, v) => debugPrint('\t\t$k: $v')); + debugPrint('\tqueryParams:'); + options.queryParameters.forEach((k, dynamic v) => debugPrint('\t\t$k: $v')); } if (options.data != null) { - debugPrint("\tBody: ${options.data}"); + debugPrint('\tBody: ${options.data}'); } - debugPrint("--> END $httpMethod"); + debugPrint('--> END $httpMethod'); return super.onRequest(options, handler); } @@ -78,13 +78,13 @@ class LoggingInterceptor extends Interceptor { ResponseInterceptorHandler handler, ) { - debugPrint("<-- RESPONSE"); + debugPrint('<-- RESPONSE'); - debugPrint("\tStatus code:${response.statusCode}"); + debugPrint('\tStatus code:${response.statusCode}'); - debugPrint("\tResponse: ${response.data}"); + debugPrint('\tResponse: ${response.data}'); - debugPrint("<-- END HTTP"); + debugPrint('<-- END HTTP'); return super.onResponse(response, handler); } @@ -116,36 +116,36 @@ class LoggingInterceptor extends Interceptor { DioError dioError, ErrorInterceptorHandler handler, ) { - debugPrint("--> ERROR"); + debugPrint('--> ERROR'); if(dioError.response != null){ - debugPrint("\tStatus code: ${dioError.response!.statusCode}"); + debugPrint('\tStatus code: ${dioError.response!.statusCode}'); if(dioError.response!.data != null){ - final Map headers = dioError.response!.data["headers"]; //API Dependant - String message = headers["message"]; //API Dependant - String error = headers["error"]; //API Dependant - debugPrint("\tException: $error"); - debugPrint("\tMessage: $message"); - if(headers.containsKey("data")){ //API Dependant - List data = headers["data"]; + final headers = dioError.response!.data['headers'] as Map; //API Dependant + var message = headers['message'] as String; //API Dependant + var error = headers['error'] as String; //API Dependant + debugPrint('\tException: $error'); + debugPrint('\tMessage: $message'); + if(headers.containsKey('data')){ //API Dependant + var data = headers['data'] as List; if(data.isNotEmpty) { - debugPrint("\tData: $data"); + debugPrint('\tData: $data'); } } } else { - debugPrint("${dioError.response!.data}"); + debugPrint('${dioError.response!.data}'); } } else if(dioError.error is SocketException){ - final message = "No internet connectivity"; - debugPrint("\tException: FetchDataException"); - debugPrint("\tMessage: $message"); + const message = 'No internet connectivity'; + debugPrint('\tException: FetchDataException'); + debugPrint('\tMessage: $message'); } else { - debugPrint("\tUnknown Error"); + debugPrint('\tUnknown Error'); } - debugPrint("<-- END ERROR"); + debugPrint('<-- END ERROR'); return super.onError(dioError, handler); } diff --git a/lib/services/networking/interceptors/refresh_token_interceptor.dart b/lib/services/networking/interceptors/refresh_token_interceptor.dart index c6aa86a..a039cba 100644 --- a/lib/services/networking/interceptors/refresh_token_interceptor.dart +++ b/lib/services/networking/interceptors/refresh_token_interceptor.dart @@ -1,6 +1,6 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; //Providers import '../../../providers/all_providers.dart'; @@ -8,18 +8,21 @@ import '../../../providers/all_providers.dart'; //Endpoints import '../api_endpoint.dart'; -/// A class that holds intercepting logic for refreshing tokens. This is -/// the last interceptor in the queue. +/// A class that holds intercepting logic for refreshing expired tokens. This +/// is the last interceptor in the queue. class RefreshTokenInterceptor extends Interceptor { - /// An instance of [Dio] for network requests final Dio _dio; + final ProviderReference _ref; - RefreshTokenInterceptor(this._dio); + RefreshTokenInterceptor({ + required Dio dioClient, + required ProviderReference ref, + }) : _dio = dioClient, _ref = ref; /// The name of the exception on which this interceptor is triggered. // ignore: non_constant_identifier_names - String get TokenExpiredException => "TokenExpiredException"; + String get TokenExpiredException => 'TokenExpiredException'; /// This method is used to send a refresh token request if the error /// indicates an expired token. @@ -42,10 +45,10 @@ class RefreshTokenInterceptor extends Interceptor { ) async { if (dioError.response != null) { if (dioError.response!.data != null) { - final Map headers = dioError.response!.data["headers"]; + final headers = dioError.response!.data['headers'] as Map; //Check error type to be token expired error - String error = headers["error"]; + var error = headers['error'] as String; if (error == TokenExpiredException) { //Make new dio and lock old one final tokenDio = Dio(); @@ -54,11 +57,11 @@ class RefreshTokenInterceptor extends Interceptor { _dio.lock(); //Get auth details for refresh token request - final authProv = ProviderContainer().read(authProvider.notifier); + final kVStorageService = _ref.read(keyValueStorageServiceProvider); final data = { - "email": authProv.currentUserEmail, - "password": authProv.currentUserPassword, - "oldToken": authProv.token + 'email': kVStorageService.getAuthUser()!.email, + 'password': await kVStorageService.getAuthPassword(), + 'oldToken': await kVStorageService.getAuthToken(), }; //Make refresh request and get new token @@ -69,20 +72,20 @@ class RefreshTokenInterceptor extends Interceptor { data: data, ); - if(newToken == null) return super.onError(dioError, handler); + if (newToken == null) return super.onError(dioError, handler); //Update auth and unlock old dio - authProv.updateToken(newToken); + kVStorageService.setAuthToken(newToken); _dio.unlock(); _dio.clear(); //Make original req with new token - final response = await _dio.request( + final response = await _dio.request>( dioError.requestOptions.path, data: dioError.requestOptions.data, cancelToken: dioError.requestOptions.cancelToken, options: Options( - headers: {"Authorization": "Bearer $newToken"}, + headers: {'Authorization': 'Bearer $newToken'}, ), ); return handler.resolve(response); @@ -105,46 +108,46 @@ class RefreshTokenInterceptor extends Interceptor { required Dio tokenDio, required Map data, }) async { - debugPrint("--> REFRESHING TOKEN"); + debugPrint('--> REFRESHING TOKEN'); try { - debugPrint("\tBody: $data"); + debugPrint('\tBody: $data'); - final response = await tokenDio.post( + final response = await tokenDio.post>( ApiEndpoint.auth(AuthEndpoint.REFRESH_TOKEN), data: data, ); - debugPrint("\tStatus code:${response.statusCode}"); - debugPrint("\tResponse: ${response.data}"); + debugPrint('\tStatus code:${response.statusCode}'); + debugPrint('\tResponse: ${response.data}'); //Check new token success - final success = response.data["headers"]["success"] == 1; + final success = response.data?['headers']['success'] == 1; if (success) { - debugPrint("<-- END REFRESH"); - return response.data["body"]["token"]; + debugPrint('<-- END REFRESH'); + return response.data?['body']['token'] as String; } else { throw Exception; } } on DioError catch (de) { //only caught here for logging //forward to try-catch in dio_service for handling - debugPrint("\t--> ERROR"); - debugPrint("\t\t--> Exception: ${de.error}"); - debugPrint("\t\t--> Message: ${de.message}"); - debugPrint("\t\t--> Response: ${de.response}"); - debugPrint("\t<-- END ERROR"); - debugPrint("<-- END REFRESH"); + debugPrint('\t--> ERROR'); + debugPrint('\t\t--> Exception: ${de.error}'); + debugPrint('\t\t--> Message: ${de.message}'); + debugPrint('\t\t--> Response: ${de.response}'); + debugPrint('\t<-- END ERROR'); + debugPrint('<-- END REFRESH'); _dio.unlock(); _dio.clear(); return null; } on Exception catch (ex) { //only caught here for logging //forward to try-catch in dio_service for handling - debugPrint("\t--> ERROR"); - debugPrint("\t\t--> Exception: $ex"); - debugPrint("\t<-- END ERROR"); - debugPrint("<-- END REFRESH"); + debugPrint('\t--> ERROR'); + debugPrint('\t\t--> Exception: $ex'); + debugPrint('\t<-- END ERROR'); + debugPrint('<-- END REFRESH'); _dio.unlock(); _dio.clear(); return null; diff --git a/lib/services/networking/network_exception.dart b/lib/services/networking/network_exception.dart index 9bc4e42..7ecd415 100644 --- a/lib/services/networking/network_exception.dart +++ b/lib/services/networking/network_exception.dart @@ -65,36 +65,36 @@ class NetworkException with _$NetworkException { switch (error.type) { case DioErrorType.cancel: return const NetworkException.CancelException( - name: "CancelException", - message: "Request cancelled prematurely", + name: 'CancelException', + message: 'Request cancelled prematurely', ); case DioErrorType.connectTimeout: return const NetworkException.ConnectTimeoutException( - name: "ConnectTimeoutException", - message: "Connection not established", + name: 'ConnectTimeoutException', + message: 'Connection not established', ); case DioErrorType.receiveTimeout: return const NetworkException.SendTimeoutException( - name: "SendTimeoutException", - message: "Failed to send", + name: 'SendTimeoutException', + message: 'Failed to send', ); case DioErrorType.sendTimeout: return const NetworkException.ReceiveTimeoutException( - name: "ReceiveTimeoutException", - message: "Failed to receive", + name: 'ReceiveTimeoutException', + message: 'Failed to receive', ); case DioErrorType.response: case DioErrorType.other: - if(error.message.contains("SocketException")) { + if(error.message.contains('SocketException')) { return const NetworkException.FetchDataException( - name: "FetchDataException", - message: "No internet connectivity", + name: 'FetchDataException', + message: 'No internet connectivity', ); } - final name = error.response?.data["headers"]["error"]; - final message = error.response?.data["headers"]["message"]; + final name = error.response?.data['headers']['error'] as String; + final message = error.response?.data['headers']['message'] as String; switch (name) { - case "TokenExpiredException": + case 'TokenExpiredException': return NetworkException.TokenExpiredException( name: name, message: message, @@ -108,19 +108,19 @@ class NetworkException with _$NetworkException { } } else { return const NetworkException.UnrecognizedException( - name: "UnrecognizedException", - message: "Error unrecognized", + name: 'UnrecognizedException', + message: 'Error unrecognized', ); } } on FormatException catch (e) { return NetworkException.FormatException( - name: "FormatException", + name: 'FormatException', message: e.message, ); } on Exception catch (_) { return const NetworkException.UnrecognizedException( - name: "UnrecognizedException", - message: "Error unrecognized", + name: 'UnrecognizedException', + message: 'Error unrecognized', ); } } diff --git a/lib/services/repositories/auth_repository.dart b/lib/services/repositories/auth_repository.dart index 97ddc29..f79d4de 100644 --- a/lib/services/repositories/auth_repository.dart +++ b/lib/services/repositories/auth_repository.dart @@ -19,8 +19,8 @@ class AuthRepository { data: data, requiresAuthToken: false, converter: (response) { - updateTokenCallback(response["body"]["token"]); - return UserModel.fromJson(response["body"]); + updateTokenCallback(response['body']['token'] as String); + return UserModel.fromJson(response['body'] as Map); }, ); } @@ -34,8 +34,8 @@ class AuthRepository { data: data, requiresAuthToken: false, converter: (response) { - updateTokenCallback(response["body"]["token"]); - data["user_id"] = response["body"]["user_id"]; + updateTokenCallback(response['body']['token'] as String); + data['user_id'] = response['body']['user_id']; return UserModel.fromJson(data); }, ); @@ -48,7 +48,7 @@ class AuthRepository { endpoint: ApiEndpoint.auth(AuthEndpoint.FORGOT_PASSWORD), data: data, requiresAuthToken: false, - converter: (response) => response["headers"]["message"], + converter: (response) => response['headers']['message'] as String, ); } @@ -59,7 +59,7 @@ class AuthRepository { endpoint: ApiEndpoint.auth(AuthEndpoint.RESET_PASSWORD), data: data, requiresAuthToken: false, - converter: (response) => response["headers"]["success"] == 1, + converter: (response) => response['headers']['success'] == 1, ); } @@ -70,7 +70,7 @@ class AuthRepository { endpoint: ApiEndpoint.auth(AuthEndpoint.CHANGE_PASSWORD), data: data, requiresAuthToken: false, - converter: (response) => response["headers"]["message"], + converter: (response) => response['headers']['message'] as String, ); } @@ -79,7 +79,7 @@ class AuthRepository { endpoint: ApiEndpoint.auth(AuthEndpoint.VERIFY_OTP), data: data, requiresAuthToken: false, - converter: (response) => response["headers"]["success"] == 1, + converter: (response) => response['headers']['success'] == 1, ); } } diff --git a/lib/services/repositories/bookings_repository.dart b/lib/services/repositories/bookings_repository.dart index 0e33ee9..8ce5fbc 100644 --- a/lib/services/repositories/bookings_repository.dart +++ b/lib/services/repositories/bookings_repository.dart @@ -44,7 +44,7 @@ class BookingsRepository { endpoint: ApiEndpoint.bookings(BookingEndpoint.BASE), data: data, cancelToken: _cancelToken, - converter: (response) => response["body"]["booking_id"], + converter: (response) => response['body']['booking_id'] as int, ); } @@ -56,7 +56,7 @@ class BookingsRepository { endpoint: ApiEndpoint.bookings(BookingEndpoint.BY_ID, id: bookingId), data: data, cancelToken: _cancelToken, - converter: (response) => response["headers"]["message"], + converter: (response) => response['headers']['message'] as String, ); } @@ -68,7 +68,7 @@ class BookingsRepository { endpoint: ApiEndpoint.bookings(BookingEndpoint.BY_ID, id: bookingId), data: data, cancelToken: _cancelToken, - converter: (response) => response["headers"]["message"], + converter: (response) => response['headers']['message'] as String, ); } @@ -79,9 +79,9 @@ class BookingsRepository { endpoint: ApiEndpoint.bookings(BookingEndpoint.SHOWS, id: showId), cancelToken: _cancelToken, converter: (responseBody) { - return responseBody["booked_seats"].map((seat) { + return responseBody['booked_seats'].map((Map seat) { return SeatModel.fromJson(seat); - }).toList(); + }).toList() as List; }, ); } diff --git a/lib/services/repositories/movies_repository.dart b/lib/services/repositories/movies_repository.dart index 36384bb..d01cca3 100644 --- a/lib/services/repositories/movies_repository.dart +++ b/lib/services/repositories/movies_repository.dart @@ -25,7 +25,7 @@ class MoviesRepository { endpoint: ApiEndpoint.movies(MovieEndpoint.BASE), data: data, cancelToken: _cancelToken, - converter: (response) => response["body"]["movie_id"], + converter: (response) => response['body']['movie_id'] as int, ); } @@ -37,7 +37,7 @@ class MoviesRepository { endpoint: ApiEndpoint.movies(MovieEndpoint.BY_ID, id: movieId), data: data, cancelToken: _cancelToken, - converter: (response) => response["headers"]["message"], + converter: (response) => response['headers']['message'] as String, ); } @@ -49,7 +49,7 @@ class MoviesRepository { endpoint: ApiEndpoint.movies(MovieEndpoint.BY_ID, id: movieId), data: data, cancelToken: _cancelToken, - converter: (response) => response["headers"]["message"], + converter: (response) => response['headers']['message'] as String, ); } diff --git a/lib/services/repositories/payments_repository.dart b/lib/services/repositories/payments_repository.dart index c2f167d..276b38d 100644 --- a/lib/services/repositories/payments_repository.dart +++ b/lib/services/repositories/payments_repository.dart @@ -46,7 +46,7 @@ class PaymentsRepository { endpoint: ApiEndpoint.payments(PaymentEndpoint.BASE), data: data, cancelToken: _cancelToken, - converter: (response) => response["body"]["payment_id"], + converter: (response) => response['body']['payment_id'] as int, ); } @@ -58,7 +58,7 @@ class PaymentsRepository { endpoint: ApiEndpoint.payments(PaymentEndpoint.BY_ID, id: paymentId), data: data, cancelToken: _cancelToken, - converter: (response) => response["headers"]["message"], + converter: (response) => response['headers']['message'] as String, ); } @@ -70,7 +70,7 @@ class PaymentsRepository { endpoint: ApiEndpoint.payments(PaymentEndpoint.BY_ID, id: paymentId), data: data, cancelToken: _cancelToken, - converter: (response) => response["headers"]["message"], + converter: (response) => response['headers']['message'] as String, ); } diff --git a/lib/services/repositories/shows_repository.dart b/lib/services/repositories/shows_repository.dart index 6c9dec4..9a731fe 100644 --- a/lib/services/repositories/shows_repository.dart +++ b/lib/services/repositories/shows_repository.dart @@ -47,7 +47,7 @@ class ShowsRepository { endpoint: ApiEndpoint.shows(ShowEndpoint.BASE), data: data, cancelToken: _cancelToken, - converter: (response) => response["body"]["show_id"], + converter: (response) => response['body']['show_id'] as int, ); } @@ -59,7 +59,7 @@ class ShowsRepository { endpoint: ApiEndpoint.shows(ShowEndpoint.BY_ID, id: showId), data: data, cancelToken: _cancelToken, - converter: (response) => response["headers"]["message"], + converter: (response) => response['headers']['message'] as String, ); } @@ -71,7 +71,7 @@ class ShowsRepository { endpoint: ApiEndpoint.shows(ShowEndpoint.BY_ID, id: showId), data: data, cancelToken: _cancelToken, - converter: (response) => response["headers"]["message"], + converter: (response) => response['headers']['message'] as String, ); } diff --git a/lib/services/repositories/theaters_repository.dart b/lib/services/repositories/theaters_repository.dart index 289436e..3f71212 100644 --- a/lib/services/repositories/theaters_repository.dart +++ b/lib/services/repositories/theaters_repository.dart @@ -45,7 +45,7 @@ class TheatersRepository { endpoint: ApiEndpoint.theaters(TheaterEndpoint.BASE), data: data, cancelToken: _cancelToken, - converter: (response) => response["body"]["theater_id"], + converter: (response) => response['body']['theater_id'] as int, ); } @@ -57,7 +57,7 @@ class TheatersRepository { endpoint: ApiEndpoint.theaters(TheaterEndpoint.BY_ID, id: theaterId), data: data, cancelToken: _cancelToken, - converter: (response) => response["headers"]["message"], + converter: (response) => response['headers']['message'] as String, ); } @@ -69,7 +69,7 @@ class TheatersRepository { endpoint: ApiEndpoint.theaters(TheaterEndpoint.BY_ID, id: theaterId), data: data, cancelToken: _cancelToken, - converter: (response) => response["headers"]["message"], + converter: (response) => response['headers']['message'] as String, ); } diff --git a/lib/views/screens/app_startup_screen.dart b/lib/views/screens/app_startup_screen.dart index 696784f..7ce45f7 100644 --- a/lib/views/screens/app_startup_screen.dart +++ b/lib/views/screens/app_startup_screen.dart @@ -12,6 +12,7 @@ import 'welcome_screen.dart'; class AppStartupScreen extends HookWidget { const AppStartupScreen(); + @override Widget build(BuildContext context) { final authState = useProvider(authProvider); return authState.maybeWhen( diff --git a/lib/views/screens/change_password_screen.dart b/lib/views/screens/change_password_screen.dart index 84b4854..a78a7ed 100644 --- a/lib/views/screens/change_password_screen.dart +++ b/lib/views/screens/change_password_screen.dart @@ -41,9 +41,9 @@ class ChangePasswordScreen extends HookWidget { context: context, barrierColor: Constants.barrierColor.withOpacity(0.75), builder: (ctx) => CustomDialog.alert( - title: "Change Password Success", + title: 'Change Password Success', body: message, - buttonText: "Okay", + buttonText: 'Okay', ), ); }, @@ -51,9 +51,9 @@ class ChangePasswordScreen extends HookWidget { context: context, barrierColor: Constants.barrierColor.withOpacity(0.75), builder: (ctx) => CustomDialog.alert( - title: "Change Password Failed", + title: 'Change Password Failed', body: reason, - buttonText: "Retry", + buttonText: 'Retry', ), ), orElse: () {}, @@ -70,7 +70,7 @@ class ChangePasswordScreen extends HookWidget { children: [ //Page name Text( - "Your profile", + 'Your profile', textAlign: TextAlign.center, style: context.headline3.copyWith(fontSize: 22), ), diff --git a/lib/views/screens/confirmation_screen.dart b/lib/views/screens/confirmation_screen.dart index 80cb52a..5e28680 100644 --- a/lib/views/screens/confirmation_screen.dart +++ b/lib/views/screens/confirmation_screen.dart @@ -20,7 +20,7 @@ class ConfirmationScreen extends StatelessWidget { return Scaffold( resizeToAvoidBottomInset: false, body: WillPopScope( - onWillPop: () async => await false, + onWillPop: () async => false, child: Container( decoration: const BoxDecoration( gradient: Constants.buttonGradientOrange, @@ -50,7 +50,7 @@ class ConfirmationScreen extends StatelessWidget { //Text Expanded( child: Text( - "Initializing payment", + 'Initializing payment', style: TextStyle( fontSize: 22, color: Colors.white, @@ -76,7 +76,7 @@ class ConfirmationScreen extends StatelessWidget { //Text Expanded( child: Text( - "Processing payment", + 'Processing payment', style: TextStyle( fontSize: 22, color: Colors.white, @@ -102,7 +102,7 @@ class ConfirmationScreen extends StatelessWidget { //Text Expanded( child: Text( - "Your tickets have been booked!", + 'Your tickets have been booked!', style: TextStyle( fontSize: 22, color: Colors.white, @@ -132,7 +132,7 @@ class ConfirmationScreen extends StatelessWidget { //Text Expanded( child: Text( - "Payment Failed", + 'Payment Failed', style: TextStyle( fontSize: 22, color: Colors.white, diff --git a/lib/views/screens/home_screen.dart b/lib/views/screens/home_screen.dart index f8380ad..50d8840 100644 --- a/lib/views/screens/home_screen.dart +++ b/lib/views/screens/home_screen.dart @@ -27,7 +27,7 @@ class HomeScreen extends StatelessWidget { children: [ //Heading text Text( - "EZ Tickets", + 'EZ Tickets', style: context.headline1.copyWith(color: Constants.primaryColor), ), @@ -35,7 +35,7 @@ class HomeScreen extends StatelessWidget { //Welcome msg Text( - "Welcome to\nthe new\nNueplex cinemas", + 'Welcome to\nthe new\nNueplex cinemas', style: context.headline3, ), @@ -43,7 +43,7 @@ class HomeScreen extends StatelessWidget { //Experience msg Text( - "New level of features\nwith the new app", + 'New level of features\nwith the new app', style: context.headline5.copyWith( color: Constants.textGreyColor, fontWeight: FontWeight.w400, @@ -67,7 +67,7 @@ class HomeScreen extends StatelessWidget { gradient: Constants.buttonGradientRed, child: const Center( child: Text( - "LOGIN", + 'LOGIN', style: TextStyle( color: Colors.white, fontSize: 15, @@ -105,7 +105,7 @@ class HomeScreen extends StatelessWidget { border: Border.all(color: Constants.primaryColor, width: 4), child: const Center( child: Text( - "REGISTER", + 'REGISTER', style: TextStyle( color: Constants.primaryColor, fontSize: 15, diff --git a/lib/views/screens/login_screen.dart b/lib/views/screens/login_screen.dart index 0ffa92c..ddabc79 100644 --- a/lib/views/screens/login_screen.dart +++ b/lib/views/screens/login_screen.dart @@ -4,11 +4,10 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../../helper/extensions/context_extensions.dart'; - //Helpers -import '../../helper/extensions/string_extension.dart'; +import '../../helper/extensions/context_extensions.dart'; import '../../helper/utils/constants.dart'; +import '../../helper/utils/form_validator.dart'; //Providers import '../../providers/all_providers.dart'; @@ -29,8 +28,8 @@ class LoginScreen extends HookWidget { @override Widget build(BuildContext context) { final formKey = useMemoized(()=>GlobalKey()); - final emailController = useTextEditingController(text: ""); - final passwordController = useTextEditingController(text: ""); + final emailController = useTextEditingController(text: ''); + final passwordController = useTextEditingController(text: ''); return Scaffold( body: ProviderListener( provider: authProvider, @@ -46,9 +45,9 @@ class LoginScreen extends HookWidget { context: context, barrierColor: Constants.barrierColor.withOpacity(0.75), builder: (ctx) => CustomDialog.alert( - title: "Login Failed", + title: 'Login Failed', body: reason, - buttonText: "Retry", + buttonText: 'Retry', ), ); }, @@ -65,7 +64,7 @@ class LoginScreen extends HookWidget { children: [ //Page name Text( - "Login", + 'Login', style: context.headline3.copyWith( color: Colors.white, fontSize: 32, @@ -78,14 +77,11 @@ class LoginScreen extends HookWidget { CustomTextField( controller: emailController, autofocus: true, - floatingText: "Email", - hintText: "Type your email address", + floatingText: 'Email', + hintText: 'Type your email address', keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, - validator: (email) { - if (email != null && email.isValidEmail) return null; - return "Please enter a valid email address"; - }, + validator: FormValidator.emailValidator, ), const SizedBox(height: 25), @@ -93,14 +89,11 @@ class LoginScreen extends HookWidget { //Password CustomTextField( controller: passwordController, - floatingText: "Password", - hintText: "Type your password", + floatingText: 'Password', + hintText: 'Type your password', keyboardType: TextInputType.visiblePassword, textInputAction: TextInputAction.done, - validator: (password) { - if (password!.isEmpty) return "Please enter a password"; - return null; - }, + validator: FormValidator.passwordValidator, ), ], ), @@ -140,7 +133,7 @@ class LoginScreen extends HookWidget { }, child: const Center( child: Text( - "LOGIN", + 'LOGIN', style: TextStyle( color: Colors.white, fontSize: 15, diff --git a/lib/views/screens/movie_details_screen.dart b/lib/views/screens/movie_details_screen.dart index 1a943da..641e584 100644 --- a/lib/views/screens/movie_details_screen.dart +++ b/lib/views/screens/movie_details_screen.dart @@ -35,7 +35,7 @@ class MovieDetailsScreen extends StatelessWidget{ color: Constants.scaffoldColor, child: Center( child: Text( - "VIEW SHOWS", + 'VIEW SHOWS', style: context.headline1.copyWith( color: Colors.white, fontSize: 15, diff --git a/lib/views/screens/register_screen.dart b/lib/views/screens/register_screen.dart index a6b564f..d750022 100644 --- a/lib/views/screens/register_screen.dart +++ b/lib/views/screens/register_screen.dart @@ -6,9 +6,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; //Helpers import '../../helper/extensions/context_extensions.dart'; -import '../../helper/extensions/string_extension.dart'; import '../../helper/utils/assets_helper.dart'; import '../../helper/utils/constants.dart'; +import '../../helper/utils/form_validator.dart'; //Providers import '../../providers/all_providers.dart'; @@ -33,7 +33,6 @@ class RegisterScreen extends StatefulHookWidget { class _RegisterScreenState extends State { bool _formHasData = false; late final formKey = GlobalKey(); - late final ValueNotifier userDetailsState = ValueNotifier(true); Future _showConfirmDialog() async { if (_formHasData) { @@ -41,10 +40,10 @@ class _RegisterScreenState extends State { context: context, barrierColor: Constants.barrierColor, builder: (ctx) => const CustomDialog.confirm( - title: "Are you sure?", - body: "Do you want to go back without saving your form data?", - trueButtonText: "Yes", - falseButtonText: "No", + title: 'Are you sure?', + body: 'Do you want to go back without saving your form data?', + trueButtonText: 'Yes', + falseButtonText: 'No', ), ); if (doPop == null || !doPop) return Future.value(false); @@ -52,7 +51,7 @@ class _RegisterScreenState extends State { return Future.value(true); } - CustomTextButton buildNextButton() { + CustomTextButton buildNextButton(ValueNotifier userDetailsState) { return CustomTextButton.outlined( width: double.infinity, onPressed: () { @@ -67,7 +66,7 @@ class _RegisterScreenState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: const [ Text( - "Next", + 'Next', style: TextStyle( color: Constants.primaryColor, fontSize: 15, @@ -100,12 +99,12 @@ class _RegisterScreenState extends State { if (formKey.currentState!.validate()) { formKey.currentState!.save(); context.read(authProvider.notifier).register( - email: email, - password: password, - fullName: fullName, - address: address, - contact: contact, - ); + email: email, + password: password, + fullName: fullName, + address: address, + contact: contact, + ); } }, gradient: Constants.buttonGradientOrange, @@ -126,7 +125,7 @@ class _RegisterScreenState extends State { }, child: const Center( child: Text( - "CONFIRM", + 'CONFIRM', style: TextStyle( color: Colors.white, fontSize: 15, @@ -139,42 +138,55 @@ class _RegisterScreenState extends State { ); } + VoidCallback? onBackTap(ValueNotifier userDetailsState){ + if(!userDetailsState.value) return () => userDetailsState.value = true; + } + + void onFormChanged() { + if (!_formHasData) _formHasData = true; + } + + void onAuthStateFailed(String reason) async { + await showDialog( + context: context, + barrierColor: Constants.barrierColor.withOpacity(0.75), + builder: (ctx) { + return CustomDialog.alert( + title: 'Register Failed', + body: reason, + buttonText: 'Retry', + ); + }, + ); + } + @override Widget build(BuildContext context) { - final emailController = useTextEditingController(text: ""); - final passwordController = useTextEditingController(text: ""); - final cPasswordController = useTextEditingController(text: ""); - final fullNameController = useTextEditingController(text: ""); - final addressController = useTextEditingController(text: ""); - final contactController = useTextEditingController(text: ""); + final userDetailsState = useState(true); + final emailController = useTextEditingController(text: ''); + final passwordController = useTextEditingController(text: ''); + final cPasswordController = useTextEditingController(text: ''); + final fullNameController = useTextEditingController(text: ''); + final addressController = useTextEditingController(text: ''); + final contactController = useTextEditingController(text: ''); + + void onAuthStateAuthenticated(String? currentUserFullName){ + emailController.clear(); + passwordController.clear(); + fullNameController.clear(); + addressController.clear(); + cPasswordController.clear(); + contactController.clear(); + _formHasData = false; + context.router.popUntilRoot(); + } return Scaffold( body: ProviderListener( provider: authProvider, onChange: (_, authState) async => (authState as AuthState).maybeWhen( - authenticated: (_) { - emailController.clear(); - passwordController.clear(); - fullNameController.clear(); - addressController.clear(); - cPasswordController.clear(); - contactController.clear(); - _formHasData = false; - context.router.popUntilRoot(); - }, - failed: (reason) async { - await showDialog( - context: context, - barrierColor: Constants.barrierColor.withOpacity(0.75), - builder: (ctx) { - return CustomDialog.alert( - title: "Register Failed", - body: reason, - buttonText: "Retry", - ); - }, - ); - }, + authenticated: onAuthStateAuthenticated, + failed: onAuthStateFailed, orElse: () {}, ), child: GestureDetector( @@ -184,18 +196,14 @@ class _RegisterScreenState extends State { //Input card Form( key: formKey, - onChanged: () { - if (!_formHasData) _formHasData = true; - }, + onChanged: onFormChanged, onWillPop: _showConfirmDialog, child: RoundedBottomContainer( - onBackTap: !userDetailsState.value - ? () => userDetailsState.value = true - : null, + onBackTap: onBackTap(userDetailsState), children: [ //Page name Text( - "Register", + 'Register', style: context.headline3.copyWith( color: Colors.white, fontSize: 32, @@ -224,12 +232,17 @@ class _RegisterScreenState extends State { //Button Padding( - padding: const EdgeInsets.fromLTRB(20, 40, 20, Constants.bottomInsets), + padding: const EdgeInsets.fromLTRB( + 20, + 40, + 20, + Constants.bottomInsets, + ), child: AnimatedSwitcher( duration: const Duration(milliseconds: 550), switchOutCurve: Curves.easeInBack, child: userDetailsState.value - ? buildNextButton() + ? buildNextButton(userDetailsState) : buildConfirmButton( email: emailController.text, password: passwordController.text, @@ -268,14 +281,11 @@ class _UserDetailFields extends StatelessWidget { CustomTextField( controller: fullNameController, autofocus: true, - floatingText: "Full name", - hintText: "Type your full name", + floatingText: 'Full name', + hintText: 'Type your full name', keyboardType: TextInputType.name, textInputAction: TextInputAction.next, - validator: (fullName) { - if (fullName != null && fullName.isValidFullName) return null; - return "Please enter a valid full name"; - }, + validator: FormValidator.fullNameValidator, ), const SizedBox(height: 25), @@ -283,14 +293,11 @@ class _UserDetailFields extends StatelessWidget { //Email CustomTextField( controller: emailController, - floatingText: "Email", - hintText: "Type your email address", + floatingText: 'Email', + hintText: 'Type your email address', keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, - validator: (email) { - if (email != null && email.isValidEmail) return null; - return "Please enter a valid email address"; - }, + validator: FormValidator.emailValidator, ), const SizedBox(height: 25), @@ -298,14 +305,11 @@ class _UserDetailFields extends StatelessWidget { //Address CustomTextField( controller: addressController, - floatingText: "Address", - hintText: "Type your full address", + floatingText: 'Address', + hintText: 'Type your full address', keyboardType: TextInputType.streetAddress, textInputAction: TextInputAction.next, - validator: (address) { - if (address!.isEmpty) return "Please enter a address"; - return null; - }, + validator: FormValidator.addressValidator, ), const SizedBox(height: 25), @@ -313,22 +317,20 @@ class _UserDetailFields extends StatelessWidget { //Contact CustomTextField( controller: contactController, - floatingText: "Contact", - hintText: "Type your mobile #", + floatingText: 'Contact', + hintText: 'Type your mobile #', keyboardType: TextInputType.phone, textInputAction: TextInputAction.done, + validator: FormValidator.contactValidator, prefix: Row( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.fromLTRB(17, 0, 5, 0), - child: Image.asset( - AssetsHelper.pkFlag, - width: 25, - ), + child: Image.asset(AssetsHelper.pkFlag, width: 25), ), const Text( - "+92", + '+92', style: TextStyle( fontSize: 18, color: Constants.textWhite80Color, @@ -336,17 +338,10 @@ class _UserDetailFields extends StatelessWidget { ), const Padding( padding: EdgeInsets.symmetric(vertical: 10), - child: VerticalDivider( - thickness: 1.1, - color: Colors.white, - ), + child: VerticalDivider(thickness: 1.1, color: Colors.white), ) ], ), - validator: (contact) { - if (contact != null && contact.isValidContact) return null; - return "Please enter a valid contact"; - }, ), ], ); @@ -370,14 +365,11 @@ class _PasswordDetailFields extends StatelessWidget { CustomTextField( controller: passwordController, autofocus: true, - floatingText: "Password", - hintText: "Type your password", + floatingText: 'Password', + hintText: 'Type your password', keyboardType: TextInputType.visiblePassword, textInputAction: TextInputAction.next, - validator: (password) { - if (password!.isEmpty) return "Please enter a password"; - return null; - }, + validator: FormValidator.passwordValidator, ), const SizedBox(height: 25), @@ -385,14 +377,14 @@ class _PasswordDetailFields extends StatelessWidget { //Confirm Password CustomTextField( controller: cPasswordController, - floatingText: "Confirm Password", - hintText: "Retype your password", + floatingText: 'Confirm Password', + hintText: 'Retype your password', keyboardType: TextInputType.visiblePassword, textInputAction: TextInputAction.done, - validator: (cPassword) { - if (passwordController.text.trim() == cPassword) return null; - return "Passwords don't match"; - }, + validator: (confirmPw) => FormValidator.confirmPasswordValidator( + confirmPw, + passwordController.text, + ), ), ], ); diff --git a/lib/views/screens/shows_screen.dart b/lib/views/screens/shows_screen.dart index e8e89c3..7c759ff 100644 --- a/lib/views/screens/shows_screen.dart +++ b/lib/views/screens/shows_screen.dart @@ -85,7 +85,7 @@ class ShowsScreen extends HookWidget { children: [ //Date Title Text( - "Select a date", + 'Select a date', style: context.headline5.copyWith( height: 1, color: Constants.textGreyColor, @@ -114,7 +114,7 @@ class ShowsScreen extends HookWidget { //Time Title Text( - "Select a time", + 'Select a time', style: context.headline5.copyWith( height: 1, color: Constants.textGreyColor, @@ -143,7 +143,7 @@ class ShowsScreen extends HookWidget { //Seats details title Text( - "Show details", + 'Show details', style: context.headline5.copyWith( height: 1, color: Constants.textGreyColor, @@ -175,7 +175,7 @@ class ShowsScreen extends HookWidget { gradient: Constants.buttonGradientOrange, child: const Center( child: Text( - "CONTINUE", + 'CONTINUE', style: TextStyle( color: Colors.white, fontSize: 15, diff --git a/lib/views/screens/trailer_screen.dart b/lib/views/screens/trailer_screen.dart index 5c2098c..7f538ab 100644 --- a/lib/views/screens/trailer_screen.dart +++ b/lib/views/screens/trailer_screen.dart @@ -108,11 +108,11 @@ class _TrailerScreenState extends State { ); } - Widget _buildErrorWidget(context, errorMessage) { + Widget _buildErrorWidget(BuildContext context, String? errorMessage) { debugPrint(errorMessage); return const Center( child: Text( - "Playback Error", + 'Playback Error', style: TextStyle(color: Colors.white), ), ); diff --git a/lib/views/screens/user_bookings_screen.dart b/lib/views/screens/user_bookings_screen.dart index 72ac107..4a64857 100644 --- a/lib/views/screens/user_bookings_screen.dart +++ b/lib/views/screens/user_bookings_screen.dart @@ -36,7 +36,7 @@ class UserBookingsScreen extends StatelessWidget { //Page Title Expanded( child: Text( - "Your bookings", + 'Your bookings', maxLines: 1, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, diff --git a/lib/views/screens/welcome_screen.dart b/lib/views/screens/welcome_screen.dart index 3592ab9..3dc50ff 100644 --- a/lib/views/screens/welcome_screen.dart +++ b/lib/views/screens/welcome_screen.dart @@ -72,7 +72,7 @@ class WelcomeScreen extends StatelessWidget { //Welcome Text( - "Welcome", + 'Welcome', style: context.headline1.copyWith( color: Constants.primaryColor, fontSize: 45, diff --git a/lib/views/skeletons/shows_skeleton_loader.dart b/lib/views/skeletons/shows_skeleton_loader.dart index aab379d..8773930 100644 --- a/lib/views/skeletons/shows_skeleton_loader.dart +++ b/lib/views/skeletons/shows_skeleton_loader.dart @@ -18,7 +18,7 @@ class ShowsSkeletonLoader extends StatelessWidget { children: [ //Date Title Text( - "Select a date", + 'Select a date', style: textTheme.headline5!.copyWith( height: 1, color: Constants.textGreyColor, @@ -78,7 +78,7 @@ class ShowsSkeletonLoader extends StatelessWidget { //Time Title Text( - "Select a time", + 'Select a time', style: textTheme.headline5!.copyWith( height: 1, color: Constants.textGreyColor, @@ -138,7 +138,7 @@ class ShowsSkeletonLoader extends StatelessWidget { //Seats details title Text( - "Show details", + 'Show details', style: textTheme.headline5!.copyWith( height: 1, color: Constants.textGreyColor, diff --git a/lib/views/widgets/change_password/change_password_fields.dart b/lib/views/widgets/change_password/change_password_fields.dart index ce50de2..c30ed9b 100644 --- a/lib/views/widgets/change_password/change_password_fields.dart +++ b/lib/views/widgets/change_password/change_password_fields.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +//Helpers +import '../../../helper/utils/form_validator.dart'; + //Providers import '../../../providers/all_providers.dart'; @@ -20,58 +23,51 @@ class ChangePasswordFields extends StatelessWidget { @override Widget build(BuildContext context) { + final authProv = context.read(authProvider.notifier); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - //Current Password Field CustomTextField( - hintText: "Enter current password", - floatingText: "Current Password", + hintText: 'Enter current password', + floatingText: 'Current Password', controller: currentPasswordController, keyboardType: TextInputType.visiblePassword, textInputAction: TextInputAction.next, - validator: (currPassword) { - final authProv = context.read(authProvider.notifier); - if (authProv.currentUserPassword == currPassword) return null; - return "Invalid current password!"; - }, + validator: (inputPw) => FormValidator.currentPasswordValidator( + inputPw, + authProv.currentUserPassword, + ), ), const SizedBox(height: 25), //New Password Field CustomTextField( - hintText: "Type your password", - floatingText: "New Password", + hintText: 'Type your password', + floatingText: 'New Password', controller: newPasswordController, keyboardType: TextInputType.visiblePassword, textInputAction: TextInputAction.next, - validator: (password) { - final authProv = context.read(authProvider.notifier); - if (password!.isEmpty) { - return "Please enter a password"; - } - else if(authProv.currentUserPassword == password) { - return "Current and new password can't be same"; - } - return null; - }, + validator: (newPw) => FormValidator.newPasswordValidator( + newPw, + authProv.currentUserPassword, + ), ), const SizedBox(height: 25), //Confirm New Password Field CustomTextField( - hintText: "Retype your password", - floatingText: "New Password", + hintText: 'Retype your password', + floatingText: 'New Password', controller: cNewPasswordController, keyboardType: TextInputType.visiblePassword, textInputAction: TextInputAction.done, - validator: (cPassword) { - if (newPasswordController.text.trim() == cPassword) return null; - return "Passwords don't match"; - }, + validator: (confirmPw) => FormValidator.confirmPasswordValidator( + confirmPw, + newPasswordController.text, + ), ), ], ); diff --git a/lib/views/widgets/change_password/save_password_button.dart b/lib/views/widgets/change_password/save_password_button.dart index 064b51b..17c3219 100644 --- a/lib/views/widgets/change_password/save_password_button.dart +++ b/lib/views/widgets/change_password/save_password_button.dart @@ -42,7 +42,7 @@ class SavePasswordButton extends StatelessWidget { }, child: const Center( child: Text( - "SAVE", + 'SAVE', style: TextStyle( color: Colors.white, fontSize: 15, diff --git a/lib/views/widgets/common/custom_error_widget.dart b/lib/views/widgets/common/custom_error_widget.dart index f457d4f..84b4b25 100644 --- a/lib/views/widgets/common/custom_error_widget.dart +++ b/lib/views/widgets/common/custom_error_widget.dart @@ -51,7 +51,7 @@ class CustomErrorWidget extends StatelessWidget { child: Column( children: [ Text( - "Oops", + 'Oops', style: textTheme.headline1!.copyWith( color: Constants.primaryColor, fontSize: 45, @@ -68,7 +68,7 @@ class CustomErrorWidget extends StatelessWidget { width: double.infinity, child: Center( child: Text( - "RETRY", + 'RETRY', style: textTheme.bodyText2!.copyWith( color: Colors.white, fontSize: 16, diff --git a/lib/views/widgets/common/custom_network_image.dart b/lib/views/widgets/common/custom_network_image.dart index b0f2026..b6472a3 100644 --- a/lib/views/widgets/common/custom_network_image.dart +++ b/lib/views/widgets/common/custom_network_image.dart @@ -37,7 +37,7 @@ class CustomNetworkImage extends StatelessWidget { padding: margin ?? EdgeInsets.zero, child: placeholder ?? const SizedBox.shrink(), ), - errorWidget: (_,__,___) => Padding( + errorWidget: (_,__,dynamic ___) => Padding( padding: margin ?? EdgeInsets.zero, child: errorWidget ?? const SizedBox.shrink(), ), diff --git a/lib/views/widgets/common/custom_textfield.dart b/lib/views/widgets/common/custom_textfield.dart index f19ffd2..11398e4 100644 --- a/lib/views/widgets/common/custom_textfield.dart +++ b/lib/views/widgets/common/custom_textfield.dart @@ -147,7 +147,7 @@ class _CustomTextFieldState extends State { contentPadding: const EdgeInsets.fromLTRB(17, 10, 1, 10), isDense: true, filled: true, - counterText: "", + counterText: '', border: _normalBorder(), focusedBorder: _focusedBorder(), focusedErrorBorder: _focusedBorder(), diff --git a/lib/views/widgets/common/ratings.dart b/lib/views/widgets/common/ratings.dart index ccfd31b..5637792 100644 --- a/lib/views/widgets/common/ratings.dart +++ b/lib/views/widgets/common/ratings.dart @@ -13,8 +13,8 @@ class Ratings extends StatelessWidget { }) : super(key: key); int numStars(double rating) { - final currentRange = 10 - 1; //max - min of current range - final targetRange = 5 - 0; //max - min of target range + const currentRange = 10 - 1; //max - min of current range + const targetRange = 5 - 0; //max - min of target range final currentRatio = (rating - 1) / currentRange; return (currentRatio * targetRange + 0).toInt(); } @@ -27,7 +27,7 @@ class Ratings extends StatelessWidget { children: [ //Rating number Text( - rating == 0 ? "N/A" : rating.toString(), + rating == 0 ? 'N/A' : rating.toString(), style: context.bodyText2.copyWith( color: Colors.black, fontWeight: FontWeight.bold, diff --git a/lib/views/widgets/confirmation/more_bookings_button.dart b/lib/views/widgets/confirmation/more_bookings_button.dart index 2146cd1..8aac0fc 100644 --- a/lib/views/widgets/confirmation/more_bookings_button.dart +++ b/lib/views/widgets/confirmation/more_bookings_button.dart @@ -30,7 +30,7 @@ class MoreBookingsButton extends StatelessWidget { color: Constants.textWhite80Color, child: const Center( child: Text( - "MAKE MORE BOOKINGS", + 'MAKE MORE BOOKINGS', style: TextStyle( color: Constants.primaryColor, fontSize: 15, diff --git a/lib/views/widgets/confirmation/retry_payment_button.dart b/lib/views/widgets/confirmation/retry_payment_button.dart index eef715e..9f317f2 100644 --- a/lib/views/widgets/confirmation/retry_payment_button.dart +++ b/lib/views/widgets/confirmation/retry_payment_button.dart @@ -23,7 +23,7 @@ class RetryPaymentButton extends StatelessWidget { border: Border.all(color: Constants.textWhite80Color,width: 4), child: const Center( child: Text( - "RETRY PAYMENT", + 'RETRY PAYMENT', style: TextStyle( color: Constants.textWhite80Color, fontSize: 15, diff --git a/lib/views/widgets/movie_details/movie_actors_list.dart b/lib/views/widgets/movie_details/movie_actors_list.dart index 5c08cd1..449eff4 100644 --- a/lib/views/widgets/movie_details/movie_actors_list.dart +++ b/lib/views/widgets/movie_details/movie_actors_list.dart @@ -65,7 +65,7 @@ class MovieActorsList extends HookWidget { child: Align( alignment: Alignment.centerLeft, child: Text( - "Cast And Crew", + 'Cast And Crew', style: context.headline2.copyWith( color: Colors.black, fontSize: 17, diff --git a/lib/views/widgets/movie_details/movie_details_column.dart b/lib/views/widgets/movie_details/movie_details_column.dart index e2126f6..3062ba1 100644 --- a/lib/views/widgets/movie_details/movie_details_column.dart +++ b/lib/views/widgets/movie_details/movie_details_column.dart @@ -49,7 +49,7 @@ class MovieDetailsColumn extends HookWidget { //Year Text( - "${movie.year}", + '${movie.year}', style: context.headline4.copyWith( color: Colors.black, fontSize: 14, diff --git a/lib/views/widgets/movie_details/movie_details_sheet.dart b/lib/views/widgets/movie_details/movie_details_sheet.dart index 7cd9c52..f7d930b 100644 --- a/lib/views/widgets/movie_details/movie_details_sheet.dart +++ b/lib/views/widgets/movie_details/movie_details_sheet.dart @@ -28,14 +28,14 @@ class _MovieDetailsSheetState extends State { late final PanelController panelController = PanelController(); final snapPoint = 0.2; - double _playBtnPos(minHeight, maxHeight, panelPosition) { + double _playBtnPos(double minHeight,double maxHeight,double panelPosition) { return minHeight - 28.5 + panelPosition * (maxHeight - minHeight); } double getPlayBtnScaleRatio(double slide) { // vanish the button at extent of 0.65 // appear the button at extent of 0.50 - final range = 0.65 - 0.50; + const range = 0.65 - 0.50; // goes from 1.0 -> 0.0 return ((0.65 - slide) / range).clamp(0.0, 1.0); } @@ -46,7 +46,7 @@ class _MovieDetailsSheetState extends State { var endExtent = 0.0; final extentRange = startExtent - endExtent; // scaleRatio goes from 1.0 -> 2.2 - final scaleRange = 1 - 2.2; + const scaleRange = 1 - 2.2; final extentRatio = (slide - endExtent) / extentRange; return extentRatio * scaleRange + 2.2; } @@ -92,7 +92,7 @@ class _MovieDetailsSheetState extends State { ); }); }, const []); - var child; + Widget? child; return Stack( clipBehavior: Clip.none, alignment: Alignment.topCenter, @@ -108,8 +108,7 @@ class _MovieDetailsSheetState extends State { ), onPanelSlide: (slide) => _onPanelSlide(slide, _animationController), panelBuilder: (controller) { - if (child == null) { - child = Padding( + child ??= Padding( padding: const EdgeInsets.only( bottom: Constants.bottomInsetsLow + 54, ), @@ -133,8 +132,7 @@ class _MovieDetailsSheetState extends State { ], ), ); - } - return child; + return child!; }, ), diff --git a/lib/views/widgets/movie_details/movie_summary_box.dart b/lib/views/widgets/movie_details/movie_summary_box.dart index dd8adf3..0cf370e 100644 --- a/lib/views/widgets/movie_details/movie_summary_box.dart +++ b/lib/views/widgets/movie_details/movie_summary_box.dart @@ -23,7 +23,7 @@ class MovieSummaryBox extends HookWidget { child: Align( alignment: Alignment.centerLeft, child: Text( - "Introduction", + 'Introduction', style: context.headline2.copyWith( color: Colors.black, fontSize: 17, diff --git a/lib/views/widgets/movie_details/play_button_widget.dart b/lib/views/widgets/movie_details/play_button_widget.dart index d82c655..b646e45 100644 --- a/lib/views/widgets/movie_details/play_button_widget.dart +++ b/lib/views/widgets/movie_details/play_button_widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:auto_route/auto_route.dart'; -import "package:hooks_riverpod/hooks_riverpod.dart"; +import 'package:hooks_riverpod/hooks_riverpod.dart'; //Providers import 'movie_details_sheet.dart' show btnScaleRatioProvider; diff --git a/lib/views/widgets/movies/movie_backdrop_view.dart b/lib/views/widgets/movies/movie_backdrop_view.dart index 694cf14..b558989 100644 --- a/lib/views/widgets/movies/movie_backdrop_view.dart +++ b/lib/views/widgets/movies/movie_backdrop_view.dart @@ -34,7 +34,7 @@ class MovieBackdropView extends HookWidget { iconSize: 85, borderRadius: 0, ), - errorWidget: (_, __, ___) => const MoviePosterPlaceholder( + errorWidget: (_, __, dynamic ___) => const MoviePosterPlaceholder( childXAlign: Alignment.topCenter, borderRadius: 0, iconSize: 85, diff --git a/lib/views/widgets/movies/movie_overview_column.dart b/lib/views/widgets/movies/movie_overview_column.dart index d19d18a..a2ff538 100644 --- a/lib/views/widgets/movies/movie_overview_column.dart +++ b/lib/views/widgets/movies/movie_overview_column.dart @@ -49,7 +49,7 @@ class MovieOverviewColumn extends StatelessWidget { //Elipses const Text( - "...", + '...', style: TextStyle( fontWeight: FontWeight.bold, color: Colors.black, diff --git a/lib/views/widgets/movies/white_movie_container.dart b/lib/views/widgets/movies/white_movie_container.dart index 5a954b5..b9c07ed 100644 --- a/lib/views/widgets/movies/white_movie_container.dart +++ b/lib/views/widgets/movies/white_movie_container.dart @@ -71,7 +71,7 @@ class WhiteMovieContainer extends HookWidget { color: Constants.scaffoldColor, child: const Center( child: Text( - "VIEW DETAILS", + 'VIEW DETAILS', style: TextStyle( color: Colors.white, fontSize: 15, diff --git a/lib/views/widgets/payment/billing_details.dart b/lib/views/widgets/payment/billing_details.dart index 2804851..735286d 100644 --- a/lib/views/widgets/payment/billing_details.dart +++ b/lib/views/widgets/payment/billing_details.dart @@ -19,7 +19,7 @@ class BillingDetails extends StatelessWidget { children: [ //Billing Details Label const Text( - "Billing Details", + 'Billing Details', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 23), ), @@ -31,7 +31,7 @@ class BillingDetails extends StatelessWidget { SizedBox( width: 40, child: Text( - "Qty", + 'Qty', textAlign: TextAlign.center, style: TextStyle( fontSize: 16, @@ -41,12 +41,12 @@ class BillingDetails extends StatelessWidget { ), SizedBox(width: 10), Text( - "Ticket Type", + 'Ticket Type', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), Spacer(), Text( - "Price", + 'Price', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, @@ -67,7 +67,7 @@ class BillingDetails extends StatelessWidget { builder: (ctx,watch,_) { final numSeats = watch(theatersProvider).selectedSeats.length; return Text( - "$numSeats", + '$numSeats', textAlign: TextAlign.center, style: const TextStyle( fontSize: 16, @@ -82,7 +82,7 @@ class BillingDetails extends StatelessWidget { //Seat type const Text( - "Normal Seat", + 'Normal Seat', style: TextStyle( fontSize: 16, color: Constants.textGreyColor, @@ -92,7 +92,7 @@ class BillingDetails extends StatelessWidget { //Price const Text( - "${Constants.ticketPrice}", + '${Constants.ticketPrice}', style: TextStyle( fontSize: 16, color: Constants.textGreyColor, @@ -111,7 +111,7 @@ class BillingDetails extends StatelessWidget { builder: (ctx,watch,_) { final numSeats = watch(theatersProvider).selectedSeats.length; return Text( - "Total - Rs. ${numSeats * Constants.ticketPrice}", + 'Total - Rs. ${numSeats * Constants.ticketPrice}', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, diff --git a/lib/views/widgets/payment/mode_details_input.dart b/lib/views/widgets/payment/mode_details_input.dart index 42b2801..d0dcd8a 100644 --- a/lib/views/widgets/payment/mode_details_input.dart +++ b/lib/views/widgets/payment/mode_details_input.dart @@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; //Helpers import '../../../enums/payment_method_enum.dart'; -import '../../../helper/extensions/string_extension.dart'; +import '../../../helper/utils/form_validator.dart'; import '../../../helper/utils/constants.dart'; //Providers @@ -31,10 +31,10 @@ class _ModeDetailsInputState extends State { context: context, barrierColor: Constants.barrierColor, builder: (ctx) => const CustomDialog.confirm( - title: "Are you sure?", - body: "Do you want to go back without saving your form data?", - trueButtonText: "Yes", - falseButtonText: "No", + title: 'Are you sure?', + body: 'Do you want to go back without saving your form data?', + trueButtonText: 'Yes', + falseButtonText: 'No', ), ); if (doPop == null || !doPop) return Future.value(false); @@ -42,23 +42,25 @@ class _ModeDetailsInputState extends State { return Future.value(true); } + void onFormChanged() { + if (!_formHasData) _formHasData = true; + } + @override Widget build(BuildContext context) { - final deliveryAddressController = useTextEditingController(text: ""); - final branchNameController = useTextEditingController(text: ""); - final zipcodeController = useTextEditingController(text: ""); - final promoCodeController = useTextEditingController(text: ""); - final creditCardNumberController = useTextEditingController(text: ""); - final creditCardCVVController = useTextEditingController(text: ""); - final creditCardExpiryController = useTextEditingController(text: ""); + final deliveryAddressController = useTextEditingController(text: ''); + final branchNameController = useTextEditingController(text: ''); + final zipcodeController = useTextEditingController(text: ''); + final promoCodeController = useTextEditingController(text: ''); + final creditCardNumberController = useTextEditingController(text: ''); + final creditCardCVVController = useTextEditingController(text: ''); + final creditCardExpiryController = useTextEditingController(text: ''); final activeMode = useProvider(activePaymentModeProvider).state; return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Form( key: formKey, - onChanged: () { - if (!_formHasData) _formHasData = true; - }, + onChanged: onFormChanged, onWillPop: _showConfirmDialog, child: AnimatedSwitcher( duration: const Duration(milliseconds: 550), @@ -109,16 +111,11 @@ class _CashOnDeliveryDetailFields extends StatelessWidget { //Delivery Address CustomTextField( controller: deliveryAddressController, - floatingText: "Delivery address", - hintText: "Enter delivery address", + floatingText: 'Delivery address', + hintText: 'Enter delivery address', keyboardType: TextInputType.streetAddress, textInputAction: TextInputAction.next, - validator: (deliveryAddress) { - if (deliveryAddress!.isEmpty) { - return "Please enter a delivery address"; - } - return null; - }, + validator: FormValidator.addressValidator, ), const SizedBox(height: 5), @@ -126,14 +123,11 @@ class _CashOnDeliveryDetailFields extends StatelessWidget { //Zipcode CustomTextField( controller: zipcodeController, - floatingText: "Zip Code", - hintText: "Enter zip code", + floatingText: 'Zip Code', + hintText: 'Enter zip code', keyboardType: TextInputType.number, textInputAction: TextInputAction.next, - validator: (zipCode) { - if (zipCode != null && zipCode.isValidZipCode) return null; - return "Please enter a valid zip code"; - }, + validator: FormValidator.zipCodeValidator, ), const SizedBox(height: 5), @@ -141,11 +135,11 @@ class _CashOnDeliveryDetailFields extends StatelessWidget { //Promo Code CustomTextField( controller: promoCodeController, - floatingText: "Promo code", - hintText: "Enter promo code", + floatingText: 'Promo code', + hintText: 'Enter promo code', keyboardType: TextInputType.text, textInputAction: TextInputAction.done, - validator: (_) {}, + validator: FormValidator.promoCodeValidator, prefix: const Icon( Icons.local_activity_rounded, color: Constants.primaryColor, @@ -177,16 +171,11 @@ class _CashOnHandDetailFields extends StatelessWidget { //Branch Name CustomTextField( controller: branchNameController, - floatingText: "Branch Name", - hintText: "Enter the branch name", + floatingText: 'Branch Name', + hintText: 'Enter the branch name', keyboardType: TextInputType.text, textInputAction: TextInputAction.next, - validator: (branchName) { - if (branchName!.isEmpty) { - return "Please enter the branch name"; - } - return null; - }, + validator: FormValidator.branchNameValidator, ), const SizedBox(height: 5), @@ -194,11 +183,11 @@ class _CashOnHandDetailFields extends StatelessWidget { //Promo Code CustomTextField( controller: promoCodeController, - floatingText: "Promo code", - hintText: "Enter promo code", + floatingText: 'Promo code', + hintText: 'Enter promo code', keyboardType: TextInputType.text, textInputAction: TextInputAction.done, - validator: (_) {}, + validator: FormValidator.promoCodeValidator, prefix: const Icon( Icons.local_activity_rounded, color: Constants.primaryColor, @@ -232,17 +221,12 @@ class _CardDetailFields extends StatelessWidget { //Credit Card Number CustomTextField( controller: creditCardNumberController, - floatingText: "Credit Card Number", - hintText: "Enter credit card number", + floatingText: 'Credit Card Number', + hintText: 'Enter credit card number', maxLength: 16, keyboardType: TextInputType.number, textInputAction: TextInputAction.next, - validator: (ccNumber) { - if (ccNumber != null && ccNumber.isValidCreditCardNumber) { - return null; - } - return "Invalid credit card number"; - }, + validator: FormValidator.creditCardNumberValidator, ), const SizedBox(height: 5), @@ -250,15 +234,12 @@ class _CardDetailFields extends StatelessWidget { //Credit Card CVV CustomTextField( controller: creditCardCVVController, - floatingText: "CVV", - hintText: "Enter CVV", + floatingText: 'CVV', + hintText: 'Enter CVV', maxLength: 3, keyboardType: TextInputType.number, textInputAction: TextInputAction.next, - validator: (cvv) { - if (cvv != null && cvv.isValidCreditCardCVV) return null; - return "Please enter a valid zip code"; - }, + validator: FormValidator.creditCardCVVValidator, ), const SizedBox(height: 5), @@ -266,14 +247,11 @@ class _CardDetailFields extends StatelessWidget { //Credit Card Expiry Date CustomTextField( controller: creditCardExpiryController, - floatingText: "Expiry Date (MM/YYYY)", - hintText: "Enter expiry date", + floatingText: 'Expiry Date (MM/YYYY)', + hintText: 'Enter expiry date', keyboardType: TextInputType.datetime, textInputAction: TextInputAction.done, - validator: (expiry) { - if (expiry != null && expiry.isValidCreditCardExpiry) return null; - return "Please enter an expiry date"; - }, + validator: FormValidator.creditCardExpiryValidator, ), ], ); diff --git a/lib/views/widgets/payment/pay_button.dart b/lib/views/widgets/payment/pay_button.dart index e1a3357..a9f6181 100644 --- a/lib/views/widgets/payment/pay_button.dart +++ b/lib/views/widgets/payment/pay_button.dart @@ -30,7 +30,7 @@ class PayButton extends StatelessWidget { gradient: Constants.buttonGradientOrange, child: const Center( child: Text( - "PAY", + 'PAY', style: TextStyle( color: Colors.white, fontSize: 15, diff --git a/lib/views/widgets/payment/payment_options.dart b/lib/views/widgets/payment/payment_options.dart index 20b6431..e709d7a 100644 --- a/lib/views/widgets/payment/payment_options.dart +++ b/lib/views/widgets/payment/payment_options.dart @@ -23,7 +23,7 @@ class PaymentOptions extends HookWidget { const Padding( padding: EdgeInsets.symmetric(horizontal: 20), child: Text( - "Payment Mode", + 'Payment Mode', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 23, @@ -55,7 +55,7 @@ class PaymentOptions extends HookWidget { //On Hand Cash Label Text( - "On Hand Cash", + 'On Hand Cash', style: context.bodyText1.copyWith( color: Constants.textGreyColor, fontSize: 17, @@ -79,7 +79,7 @@ class PaymentOptions extends HookWidget { //Card Label Text( - "Card", + 'Card', style: context.bodyText1.copyWith( color: Constants.textGreyColor, fontSize: 17, @@ -109,7 +109,7 @@ class PaymentOptions extends HookWidget { //COD Label Text( - "Cash On Delivery", + 'Cash On Delivery', style: context.bodyText1.copyWith( color: Constants.textGreyColor, fontSize: 17, diff --git a/lib/views/widgets/theater/purchase_seats_button.dart b/lib/views/widgets/theater/purchase_seats_button.dart index bbe18bf..28a3b3d 100644 --- a/lib/views/widgets/theater/purchase_seats_button.dart +++ b/lib/views/widgets/theater/purchase_seats_button.dart @@ -33,7 +33,7 @@ class PurchaseSeatsButton extends StatelessWidget { gradient: Constants.buttonGradientOrange, child: Center( child: Text( - "PURCHASE - $theaterSeats SEATS", + 'PURCHASE - $theaterSeats SEATS', style: const TextStyle( color: Colors.white, fontSize: 15, diff --git a/lib/views/widgets/theater/seat_color_indicators.dart b/lib/views/widgets/theater/seat_color_indicators.dart index 19f2584..88abd7f 100644 --- a/lib/views/widgets/theater/seat_color_indicators.dart +++ b/lib/views/widgets/theater/seat_color_indicators.dart @@ -14,9 +14,9 @@ class SeatColorIndicators extends StatelessWidget { const SeatColorIndicators(); static const _indicators = [ - _Indicator("Available", Colors.white), - _Indicator("Taken", Color(0xFF5A5A5A)), - _Indicator("Selected", Constants.redColor), + _Indicator('Available', Colors.white), + _Indicator('Taken', Color(0xFF5A5A5A)), + _Indicator('Selected', Constants.redColor), ]; @override diff --git a/lib/views/widgets/ticket_summary/confirm_bookings_button.dart b/lib/views/widgets/ticket_summary/confirm_bookings_button.dart index d08f049..0feed8e 100644 --- a/lib/views/widgets/ticket_summary/confirm_bookings_button.dart +++ b/lib/views/widgets/ticket_summary/confirm_bookings_button.dart @@ -5,7 +5,7 @@ import 'package:auto_route/auto_route.dart'; import '../../../helper/utils/constants.dart'; //Routes -import "../../../routes/app_router.gr.dart"; +import '../../../routes/app_router.gr.dart'; //Widgets import '../common/custom_text_button.dart'; @@ -25,7 +25,7 @@ class ConfirmBookingsButton extends StatelessWidget { gradient: Constants.buttonGradientOrange, child: const Center( child: Text( - "CONFIRM", + 'CONFIRM', style: TextStyle( color: Colors.white, fontSize: 15, diff --git a/lib/views/widgets/ticket_summary/show_details_section.dart b/lib/views/widgets/ticket_summary/show_details_section.dart index 854ee19..339c9f7 100644 --- a/lib/views/widgets/ticket_summary/show_details_section.dart +++ b/lib/views/widgets/ticket_summary/show_details_section.dart @@ -32,14 +32,14 @@ class ShowDetailsSection extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - "Date", + 'Date', style: TextStyle( fontSize: 13, color: Constants.textGreyColor, ), ), Text( - "${DateFormat("E, d MMMM y").format(_selectedShow.date)}", + DateFormat('E, d MMMM y').format(_selectedShow.date), style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, @@ -53,14 +53,14 @@ class ShowDetailsSection extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - "Time", + 'Time', style: TextStyle( fontSize: 13, color: Constants.textGreyColor, ), ), Text( - "${DateFormat.Hm().format(_selectedShowTime.startTime)}", + DateFormat.Hm().format(_selectedShowTime.startTime), style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, @@ -74,14 +74,14 @@ class ShowDetailsSection extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - "Theater", + 'Theater', style: TextStyle( fontSize: 13, color: Constants.textGreyColor, ), ), Text( - "$_theaterName", + _theaterName, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, diff --git a/lib/views/widgets/ticket_summary/ticket_details_list.dart b/lib/views/widgets/ticket_summary/ticket_details_list.dart index 97df521..e86588e 100644 --- a/lib/views/widgets/ticket_summary/ticket_details_list.dart +++ b/lib/views/widgets/ticket_summary/ticket_details_list.dart @@ -47,7 +47,7 @@ class TicketDetailsList extends HookWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - "Seat", + 'Seat', style: TextStyle( fontSize: 13, color: Constants.textGreyColor, @@ -68,14 +68,14 @@ class TicketDetailsList extends HookWidget { crossAxisAlignment: CrossAxisAlignment.start, children: const [ Text( - "Price", + 'Price', style: TextStyle( fontSize: 13, color: Constants.textGreyColor, ), ), Text( - "Rs. ${Constants.ticketPrice}", + 'Rs. ${Constants.ticketPrice}', style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, diff --git a/lib/views/widgets/user_bookings/booking_details_dialog.dart b/lib/views/widgets/user_bookings/booking_details_dialog.dart index 36665c1..bcf572b 100644 --- a/lib/views/widgets/user_bookings/booking_details_dialog.dart +++ b/lib/views/widgets/user_bookings/booking_details_dialog.dart @@ -65,7 +65,7 @@ class BookingDetailsDialog extends StatelessWidget { SizedBox( width: 50, child: Text( - "Seat", + 'Seat', style: TextStyle( color: Constants.textWhite80Color, ), @@ -75,7 +75,7 @@ class BookingDetailsDialog extends StatelessWidget { //Price label Expanded( child: Text( - "Price", + 'Price', style: TextStyle( color: Constants.textWhite80Color, ), @@ -86,7 +86,7 @@ class BookingDetailsDialog extends StatelessWidget { SizedBox( width: 100, child: Text( - "Seat Status", + 'Seat Status', style: TextStyle( color: Constants.textWhite80Color, ), @@ -152,7 +152,7 @@ class _BookingSeatsListItem extends StatelessWidget { SizedBox( width: 50, child: Text( - "${booking.seatRow}-${booking.seatNumber}", + '${booking.seatRow}-${booking.seatNumber}', style: const TextStyle( color: Constants.textGreyColor, fontSize: 13, @@ -178,7 +178,7 @@ class _BookingSeatsListItem extends StatelessWidget { children: [ //Booking Status value Text( - "${booking.bookingStatus.name}", + booking.bookingStatus.name, style: const TextStyle( color: Constants.textGreyColor, fontSize: 13, diff --git a/lib/views/widgets/user_bookings/booking_summary_row.dart b/lib/views/widgets/user_bookings/booking_summary_row.dart index 837ccd6..70e42e7 100644 --- a/lib/views/widgets/user_bookings/booking_summary_row.dart +++ b/lib/views/widgets/user_bookings/booking_summary_row.dart @@ -70,7 +70,7 @@ class BookingSummaryRow extends StatelessWidget { //Show status Text( - "${showType.inString}", + showType.inString, style: const TextStyle( fontSize: 14, color: Constants.textWhite80Color, @@ -95,7 +95,7 @@ class BookingSummaryRow extends StatelessWidget { //Show time data Text( - "${DateFormat("d MMMM,yy H:m").format(showDateTime)}", + DateFormat('d MMMM,yy H:m').format(showDateTime), style: const TextStyle( fontSize: 14, color: Constants.textWhite80Color, @@ -120,7 +120,7 @@ class BookingSummaryRow extends StatelessWidget { //Total data Text( - "Rs. $total", + 'Rs. $total', style: const TextStyle( fontSize: 14, color: Constants.textWhite80Color, @@ -158,7 +158,7 @@ class BookingSummaryRow extends StatelessWidget { //No. of seats Text( - "$noOfSeats", + '$noOfSeats', style: const TextStyle( fontSize: 16, color: Constants.textWhite80Color, diff --git a/lib/views/widgets/welcome/browse_movies_button.dart b/lib/views/widgets/welcome/browse_movies_button.dart index a7cb42a..ea35499 100644 --- a/lib/views/widgets/welcome/browse_movies_button.dart +++ b/lib/views/widgets/welcome/browse_movies_button.dart @@ -23,7 +23,7 @@ class BrowseMoviesButton extends StatelessWidget { gradient: Constants.buttonGradientOrange, child: const Center( child: Text( - "BROWSE MOVIES", + 'BROWSE MOVIES', style: TextStyle( color: Colors.white, fontSize: 15, diff --git a/lib/views/widgets/welcome/user_profile_details.dart b/lib/views/widgets/welcome/user_profile_details.dart index 51453dc..d5eda81 100644 --- a/lib/views/widgets/welcome/user_profile_details.dart +++ b/lib/views/widgets/welcome/user_profile_details.dart @@ -23,7 +23,7 @@ class UserProfileDetails extends HookWidget { children: [ //Full Name Label Text( - "Full Name", + 'Full Name', style: context.bodyText1.copyWith( color: Constants.primaryColor, fontSize: 26, @@ -33,7 +33,7 @@ class UserProfileDetails extends HookWidget { //Full Name Text( - "${authProv.currentUserFullName}", + authProv.currentUserFullName, style: context.bodyText1.copyWith( color: Constants.textWhite80Color, fontSize: 18, @@ -44,7 +44,7 @@ class UserProfileDetails extends HookWidget { //Email Label Text( - "Email", + 'Email', style: context.bodyText1.copyWith( color: Constants.primaryColor, fontSize: 26, @@ -54,7 +54,7 @@ class UserProfileDetails extends HookWidget { //Email Data Text( - "${authProv.currentUserEmail}", + authProv.currentUserEmail, style: context.bodyText1.copyWith( color: Constants.textWhite80Color, fontSize: 18, @@ -65,7 +65,7 @@ class UserProfileDetails extends HookWidget { //Address Label Text( - "Address", + 'Address', style: context.bodyText1.copyWith( color: Constants.primaryColor, fontSize: 26, @@ -75,7 +75,7 @@ class UserProfileDetails extends HookWidget { //Address Data Text( - "${authProv.currentUserAddress}", + authProv.currentUserAddress, style: context.bodyText1.copyWith( color: Constants.textWhite80Color, fontSize: 18, @@ -86,7 +86,7 @@ class UserProfileDetails extends HookWidget { //Contact Label Text( - "Contact", + 'Contact', style: context.bodyText1.copyWith( color: Constants.primaryColor, fontSize: 26, @@ -96,7 +96,7 @@ class UserProfileDetails extends HookWidget { //Contact Data Text( - "${authProv.currentUserContact}", + authProv.currentUserContact, style: context.bodyText1.copyWith( color: Constants.textWhite80Color, fontSize: 18, diff --git a/lib/views/widgets/welcome/view_bookings_button.dart b/lib/views/widgets/welcome/view_bookings_button.dart index de8cd27..08924a6 100644 --- a/lib/views/widgets/welcome/view_bookings_button.dart +++ b/lib/views/widgets/welcome/view_bookings_button.dart @@ -21,7 +21,7 @@ class ViewBookingsButton extends StatelessWidget { border: Border.all(color: Constants.primaryColor,width: 4), child: const Center( child: Text( - "VIEW BOOKINGS", + 'VIEW BOOKINGS', style: TextStyle( color: Constants.primaryColor, fontSize: 15, diff --git a/pubspec.lock b/pubspec.lock index 956c9e7..b07893c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "21.0.0" + version: "22.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.7.1" archive: dependency: transitive description: @@ -28,7 +28,7 @@ packages: name: args url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.2.0" async: dependency: transitive description: @@ -56,7 +56,7 @@ packages: name: better_player url: "https://pub.dartlang.org" source: hosted - version: "0.0.69" + version: "0.0.72" boolean_selector: dependency: transitive description: @@ -70,7 +70,7 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.3" build_config: dependency: transitive description: @@ -91,49 +91,63 @@ packages: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.4" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.6" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "7.0.0" + version: "7.0.1" built_collection: dependency: transitive description: name: built_collection url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "5.1.0" built_value: dependency: transitive description: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "8.0.5" + version: "8.1.1" cached_network_image: dependency: "direct main" description: name: cached_network_image url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.1.0" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" carousel_slider: dependency: "direct main" description: name: carousel_slider url: "https://pub.dartlang.org" source: hosted - version: "4.0.0-nullsafety.0" + version: "4.0.0" characters: dependency: transitive description: @@ -161,9 +175,9 @@ packages: name: cli_util url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.3.3" clock: - dependency: transitive + dependency: "direct main" description: name: clock url: "https://pub.dartlang.org" @@ -175,7 +189,7 @@ packages: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.1.0" collection: dependency: transitive description: @@ -189,7 +203,7 @@ packages: name: convert url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" crypto: dependency: transitive description: @@ -217,14 +231,14 @@ packages: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.3" dartdoc: dependency: "direct main" description: name: dartdoc url: "https://pub.dartlang.org" source: hosted - version: "0.44.0" + version: "1.0.0" dio: dependency: "direct main" description: @@ -232,13 +246,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.0" - effective_dart: - dependency: "direct dev" - description: - name: effective_dart - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" fake_async: dependency: transitive description: @@ -252,14 +259,14 @@ packages: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.2" file: dependency: transitive description: name: file url: "https://pub.dartlang.org" source: hosted - version: "6.1.0" + version: "6.1.2" fixnum: dependency: transitive description: @@ -285,7 +292,7 @@ packages: name: flutter_cache_manager url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.1.2" flutter_hooks: dependency: "direct main" description: @@ -299,14 +306,21 @@ packages: name: flutter_launcher_icons url: "https://pub.dartlang.org" source: hosted - version: "0.9.0" + version: "0.9.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" flutter_native_splash: dependency: "direct dev" description: name: flutter_native_splash url: "https://pub.dartlang.org" source: hosted - version: "1.1.8+4" + version: "1.2.0" flutter_riverpod: dependency: transitive description: @@ -314,6 +328,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.14.0+3" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.1" flutter_spinkit: dependency: "direct main" description: @@ -337,7 +358,7 @@ packages: name: flutter_widget_from_html_core url: "https://pub.dartlang.org" source: hosted - version: "0.6.1+1" + version: "0.6.1+4" freezed: dependency: "direct dev" description: @@ -435,7 +456,7 @@ packages: name: io url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.3" js: dependency: transitive description: @@ -456,7 +477,14 @@ packages: name: json_serializable url: "https://pub.dartlang.org" source: hosted - version: "4.1.3" + version: "4.1.4" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" logging: dependency: transitive description: @@ -519,7 +547,7 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" path_provider_linux: dependency: transitive description: @@ -554,7 +582,7 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.11.0" + version: "1.11.1" petitparser: dependency: transitive description: @@ -575,7 +603,7 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" pool: dependency: transitive description: @@ -617,7 +645,7 @@ packages: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.26.0" + version: "0.27.1" shared_preferences: dependency: "direct main" description: @@ -666,7 +694,7 @@ packages: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.2.0" shelf_web_socket: dependency: transitive description: @@ -692,7 +720,7 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.3" source_span: dependency: transitive description: @@ -874,7 +902,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.2.5" xdg_directories: dependency: transitive description: @@ -888,7 +916,7 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "5.1.1" + version: "5.1.2" yaml: dependency: transitive description: @@ -897,5 +925,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.13.0 <3.0.0" flutter: ">=2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 0e4c095..a0e835b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,20 +11,22 @@ dependencies: auto_route: ^2.2.0 cupertino_icons: ^1.0.3 time: ^2.0.0 - cached_network_image: ^3.0.0 + cached_network_image: ^3.1.0 freezed_annotation: ^0.14.2 google_fonts: ^2.1.0 flutter_hooks: ^0.17.0 hooks_riverpod: ^0.14.0+4 - flutter_launcher_icons: ^0.9.0 - carousel_slider: 4.0.0-nullsafety.0 + flutter_launcher_icons: ^0.9.1 + carousel_slider: ^4.0.0 dio: ^4.0.0 shared_preferences: ^2.0.6 sliding_up_panel: ^2.0.0+1 intl: ^0.17.0 - dartdoc: ^0.44.0 - better_player: ^0.0.69 + dartdoc: ^1.0.0 + better_player: ^0.0.72 flutter_spinkit: 5.0.0 + flutter_secure_storage: ^4.2.1 + clock: ^1.1.0 dev_dependencies: flutter_test: sdk: flutter @@ -32,8 +34,8 @@ dev_dependencies: freezed: ^0.14.2 json_serializable: ^4.1.3 build_runner: null - effective_dart: ^1.3.1 - flutter_native_splash: ^1.1.8+4 + flutter_native_splash: ^1.2.0 + flutter_lints: ^1.0.4 flutter: uses-material-design: true assets: @@ -43,16 +45,7 @@ flutter_icons: ios: false image_path: assets/app_icon.png flutter_native_splash: - color: "#141414" + color: '#141414' image: assets/app_icon.png - - #android: false ios: false web: false - - # bottom, center, - # center_horizontal, center_vertical, clip_horizontal, clip_vertical, end, fill, fill_horizontal, - # fill_vertical, left, right, start, or top. - #android_gravity: center - - #fullscreen: true diff --git a/test/enums/booking_status_enum_test.dart b/test/enums/booking_status_enum_test.dart new file mode 100644 index 0000000..dd7310f --- /dev/null +++ b/test/enums/booking_status_enum_test.dart @@ -0,0 +1,46 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:ez_ticketz_app/enums/booking_status_enum.dart'; + +void main() { + group('BookingStatusEnum', () { + group('name', () { + test( + 'GIVEN a booking status enum ' + 'WHEN `.name` extension method is called ' + 'THEN the name of the enum is returned', + () { + //given + const enumValue = BookingStatus.RESERVED; + + final enumName = enumValue.toString().split('.').last; + + //when + final name = enumValue.name; + + //then + expect(name, enumName); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN a booking status enum ' + 'WHEN `.toJson` extension method is called ' + 'THEN the json key of the enum value is returned', + () { + //given + const enumValue = BookingStatus.CONFIRMED; + + final enumJson = enumValue.toString().split('.').last.toLowerCase(); + + //when + final toJson = enumValue.toJson; + + //then + expect(toJson, enumJson); + }, + ); + }); + }); +} diff --git a/test/enums/movie_type_enum_test.dart b/test/enums/movie_type_enum_test.dart new file mode 100644 index 0000000..1462f64 --- /dev/null +++ b/test/enums/movie_type_enum_test.dart @@ -0,0 +1,46 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:ez_ticketz_app/enums/movie_type_enum.dart'; + +void main() { + group('MovieTypeEnum', () { + group('name', () { + test( + 'GIVEN a movie type enum ' + 'WHEN `.name` extension method is called ' + 'THEN the name of the enum is returned', + () { + //given + const enumValue = MovieType.COMING_SOON; + + final enumName = enumValue.toString().split('.').last; + + //when + final name = enumValue.name; + + //then + expect(name, enumName); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN a movie type enum ' + 'WHEN `.toJson` extension method is called ' + 'THEN the json key of the enum value is returned', + () { + //given + const enumValue = MovieType.NOW_SHOWING; + + final enumJson = enumValue.toString().split('.').last.toLowerCase(); + + //when + final toJson = enumValue.toJson; + + //then + expect(toJson, enumJson); + }, + ); + }); + }); +} diff --git a/test/enums/payment_method_enum_test.dart b/test/enums/payment_method_enum_test.dart new file mode 100644 index 0000000..4eb64e6 --- /dev/null +++ b/test/enums/payment_method_enum_test.dart @@ -0,0 +1,68 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:ez_ticketz_app/enums/payment_method_enum.dart'; +import 'package:ez_ticketz_app/helper/extensions/string_extension.dart'; + +void main() { + group('PaymentMethodEnum', () { + group('name', () { + test( + 'GIVEN a payment method enum ' + 'WHEN `.name` extension method is called ' + 'THEN the name of the enum is returned', + () { + //given + const enumValue = PaymentMethod.CARD; + + final enumName = enumValue.toString().split('.').last; + + //when + final name = enumValue.name; + + //then + expect(name, enumName); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN a payment method enum ' + 'WHEN `.toJson` extension method is called ' + 'THEN the json key of the enum value is returned', + () { + //given + const enumValue = PaymentMethod.CASH; + + final enumJson = enumValue.toString().split('.').last.toLowerCase(); + + //when + final toJson = enumValue.toJson; + + //then + expect(toJson, enumJson); + }, + ); + }); + + group('inString', () { + test( + 'GIVEN a payment method enum ' + 'WHEN `.inString` extension method is called ' + 'THEN the string representation of the enum value is returned', + () { + //given + const enumValue = PaymentMethod.COD; + + final enumString = enumValue.toString().split('.').last.capitalize; + + //when + final inString = enumValue.inString; + + //then + expect(inString, enumString); + }, + ); + }); + }); +} diff --git a/test/enums/role_type_enum_test.dart b/test/enums/role_type_enum_test.dart new file mode 100644 index 0000000..7a86a63 --- /dev/null +++ b/test/enums/role_type_enum_test.dart @@ -0,0 +1,68 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:ez_ticketz_app/enums/role_type_enum.dart'; +import 'package:ez_ticketz_app/helper/extensions/string_extension.dart'; + +void main() { + group('RoleTypeEnum', () { + group('name', () { + test( + 'GIVEN a role type enum ' + 'WHEN `.name` extension method is called ' + 'THEN the name of the enum is returned', + () { + //given + const enumValue = RoleType.DIRECTOR; + + final enumName = enumValue.toString().split('.').last; + + //when + final name = enumValue.name; + + //then + expect(name, enumName); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN a role type enum ' + 'WHEN `.toJson` extension method is called ' + 'THEN the json key of the enum value is returned', + () { + //given + const enumValue = RoleType.DIRECTOR; + + final enumJson = enumValue.toString().split('.').last.toLowerCase(); + + //when + final toJson = enumValue.toJson; + + //then + expect(toJson, enumJson); + }, + ); + }); + + group('inString', () { + test( + 'GIVEN a role type enum ' + 'WHEN `.inString` extension method is called ' + 'THEN the string representation of the enum value is returned', + () { + //given + const enumValue = RoleType.DIRECTOR; + + final enumString = enumValue.toString().split('.').last.capitalize; + + //when + final inString = enumValue.inString; + + //then + expect(inString, enumString); + }, + ); + }); + }); +} diff --git a/test/enums/show_status_enum_test.dart b/test/enums/show_status_enum_test.dart new file mode 100644 index 0000000..e3dcc26 --- /dev/null +++ b/test/enums/show_status_enum_test.dart @@ -0,0 +1,69 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:ez_ticketz_app/enums/show_status_enum.dart'; +import 'package:ez_ticketz_app/helper/extensions/string_extension.dart'; + +void main() { + group('ShowStatusEnum', () { + group('name',(){ + test( + 'GIVEN a show status enum ' + 'WHEN `.name` extension method is called ' + 'THEN the name of the enum is returned', + () { + //given + const enumValue = ShowStatus.ALMOST_FULL; + + final enumName = enumValue.toString().split('.').last; + + //when + final name = enumValue.name; + + //then + expect(name, enumName); + }, + ); + }); + + group('toJson',(){ + test( + 'GIVEN a show status enum ' + 'WHEN `.toJson` extension method is called ' + 'THEN the json key of the enum value is returned', + () { + //given + const enumValue = ShowStatus.ALMOST_FULL; + + final enumJson = enumValue.toString().split('.').last.toLowerCase(); + + //when + final toJson = enumValue.toJson; + + //then + expect(toJson, enumJson); + }, + ); + }); + + group('inString',(){ + test( + 'GIVEN a show status enum ' + 'WHEN `.inString` extension method is called ' + 'THEN the string representation of the enum value is returned', + () { + //given + const enumValue = ShowStatus.ALMOST_FULL; + + final enumString = + enumValue.toString().split('.').last.removeUnderScore.toUpperCase(); + + //when + final inString = enumValue.inString; + + //then + expect(inString, enumString); + }, + ); + }); + }); +} diff --git a/test/enums/show_type_enum_test.dart b/test/enums/show_type_enum_test.dart new file mode 100644 index 0000000..c48a48c --- /dev/null +++ b/test/enums/show_type_enum_test.dart @@ -0,0 +1,46 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:ez_ticketz_app/enums/show_type_enum.dart'; + +void main() { + group('ShowTypeEnum', () { + group('name', () { + test( + 'GIVEN a show type enum ' + 'WHEN `.name` extension method is called ' + 'THEN the name of the enum is returned', + () { + //given + const enumValue = ShowType.i2D; + + final enumName = enumValue.toString().split('.').last; + + //when + final name = enumValue.name; + + //then + expect(name, enumName); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN a show type enum ' + 'WHEN `.toJson` extension method is called ' + 'THEN the json key of the enum value is returned', + () { + //given + const enumValue = ShowType.i2D; + + final enumJson = enumValue.toString().split('.').last.substring(1); + + //when + final toJson = enumValue.toJson; + + //then + expect(toJson, enumJson); + }, + ); + }); + }); +} diff --git a/test/enums/theater_type_enum_test.dart b/test/enums/theater_type_enum_test.dart new file mode 100644 index 0000000..342aee4 --- /dev/null +++ b/test/enums/theater_type_enum_test.dart @@ -0,0 +1,46 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:ez_ticketz_app/enums/theater_type_enum.dart'; + +void main() { + group('TheaterTypeEnum', () { + group('name', () { + test( + 'GIVEN a theater type enum ' + 'WHEN `.name` extension method is called ' + 'THEN the name of the enum is returned', + () { + //given + const enumValue = TheaterType.NORMAL; + + final enumName = enumValue.toString().split('.').last; + + //when + final name = enumValue.name; + + //then + expect(name, enumName); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN a theater type enum ' + 'WHEN `.toJson` extension method is called ' + 'THEN the json key of the enum value is returned', + () { + //given + const enumValue = TheaterType.NORMAL; + + final enumJson = enumValue.toString().split('.').last.toLowerCase(); + + //when + final toJson = enumValue.toJson; + + //then + expect(toJson, enumJson); + }, + ); + }); + }); +} diff --git a/test/enums/user_role_enum_test.dart b/test/enums/user_role_enum_test.dart new file mode 100644 index 0000000..9f713a7 --- /dev/null +++ b/test/enums/user_role_enum_test.dart @@ -0,0 +1,46 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:ez_ticketz_app/enums/user_role_enum.dart'; + +void main() { + group('UserRoleEnum', () { + group('name', () { + test( + 'GIVEN a user role enum ' + 'WHEN `.name` extension method is called ' + 'THEN the name of the enum is returned', + () { + //given + const enumValue = UserRole.API_USER; + + final enumName = enumValue.toString().split('.').last; + + //when + final name = enumValue.name; + + //then + expect(name, enumName); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN a user role enum ' + 'WHEN `.toJson` extension method is called ' + 'THEN the json key of the enum value is returned', + () { + //given + const enumValue = UserRole.API_USER; + + final enumJson = enumValue.toString().split('.').last.toLowerCase(); + + //when + final toJson = enumValue.toJson; + + //then + expect(toJson, enumJson); + }, + ); + }); + }); +} diff --git a/test/helper/utils/form_validator_test.dart b/test/helper/utils/form_validator_test.dart new file mode 100644 index 0000000..65806e8 --- /dev/null +++ b/test/helper/utils/form_validator_test.dart @@ -0,0 +1,242 @@ +import 'package:flutter_test/flutter_test.dart'; + +//Helpers +import 'package:ez_ticketz_app/helper/utils/constants.dart'; +import 'package:ez_ticketz_app/helper/utils/form_validator.dart'; + +void main(){ + group('FormValidator',(){ + group('email input validations', (){ + test('non-empty valid email returns null', (){ + var result = FormValidator.emailValidator('test.email123@gmail.com'); + expect(result, null); + }); + + test('empty email returns error string', (){ + var result = FormValidator.emailValidator(''); + expect(result, Constants.emptyEmailInputError); + }); + + test('invalid email return error string', (){ + var result = FormValidator.emailValidator('email'); + expect(result, Constants.invalidEmailError); + }); + }); + + group("password inputs' validations", (){ + test('non-empty password returns null', (){ + var result = FormValidator.passwordValidator('pass'); + expect(result, null); + }); + + test('empty password returns error string', (){ + var result = FormValidator.passwordValidator(''); + expect(result, Constants.emptyPasswordInputError); + }); + + test('matching confirm password returns null', (){ + var result = FormValidator.confirmPasswordValidator('pass','pass'); + expect(result, null); + }); + + test('invalid confirm password returns error string', (){ + var result = FormValidator.confirmPasswordValidator('','pass'); + expect(result, Constants.invalidConfirmPwError); + + result = FormValidator.confirmPasswordValidator('pass','fail'); + expect(result, Constants.invalidConfirmPwError); + }); + + test('matching current password returns null', (){ + var result = FormValidator.currentPasswordValidator('pass','pass'); + expect(result, null); + }); + + test('invalid current password returns error string', (){ + var result = FormValidator.currentPasswordValidator('','pass'); + expect(result, Constants.invalidCurrentPwError); + + result = FormValidator.currentPasswordValidator('pass','fail'); + expect(result, Constants.invalidCurrentPwError); + }); + + test('non-empty valid new password returns null', (){ + var result = FormValidator.newPasswordValidator('oldPass','newPass'); + expect(result, null); + }); + + test('empty new password returns error string', (){ + var result = FormValidator.newPasswordValidator('','pass'); + expect(result, Constants.emptyPasswordInputError); + }); + + test('invalid new password returns error string', (){ + var result = FormValidator.newPasswordValidator('samePass','samePass'); + expect(result, Constants.invalidNewPwError); + }); + }); + + group("register user inputs' validations", (){ + test('a valid full name returns null', (){ + var result = FormValidator.fullNameValidator('full name'); + expect(result, null); + }); + + test('invalid full name returns error string', (){ + var result = FormValidator.fullNameValidator(''); + expect(result, Constants.invalidFullNameError); + + result = FormValidator.fullNameValidator('ful| n4m3'); + expect(result, Constants.invalidFullNameError); + }); + + test('non-empty address returns null', (){ + var result = FormValidator.addressValidator('123-A, Street Test'); + expect(result, null); + }); + + test('empty address returns error string', (){ + var result = FormValidator.addressValidator(''); + expect(result, Constants.emptyAddressInputError); + }); + + test('a valid contact returns null', (){ + var result = FormValidator.contactValidator('03001234567'); + expect(result, null); + }); + + test('invalid contact returns error string', (){ + var result = FormValidator.contactValidator('0300123'); + expect(result, Constants.invalidContactError); + + result = FormValidator.contactValidator('01001234567'); + expect(result, Constants.invalidContactError); + + result = FormValidator.contactValidator('abc123defg'); + expect(result, Constants.invalidContactError); + }); + }); + + group("payment inputs' validations", (){ + test('a valid zip code returns null', (){ + var result = FormValidator.zipCodeValidator('75400'); + expect(result, null); + }); + + test('invalid zip code returns error string', (){ + var result = FormValidator.zipCodeValidator(''); + expect(result, Constants.invalidZipCodeError); + + result = FormValidator.zipCodeValidator('800'); + expect(result, Constants.invalidZipCodeError); + + result = FormValidator.zipCodeValidator('abc'); + expect(result, Constants.invalidZipCodeError); + + result = FormValidator.zipCodeValidator('123456'); + expect(result, Constants.invalidZipCodeError); + }); + + test('valid promo code returns null', (){ + var result = FormValidator.promoCodeValidator('123456'); + expect(result, null); + + result = FormValidator.promoCodeValidator('abc123'); + expect(result, null); + }); + + test('invalid promo code returns error string', (){ + var result = FormValidator.promoCodeValidator(''); + expect(result, Constants.invalidPromoCodeError); + + result = FormValidator.promoCodeValidator('12345'); + expect(result, Constants.invalidPromoCodeError); + }); + + test('a non-empty branch name returns null', (){ + var result = FormValidator.branchNameValidator('Defence'); + expect(result, null); + }); + + test('empty branch name returns error string', (){ + var result = FormValidator.branchNameValidator(''); + expect(result, Constants.emptyBranchInputError); + }); + + test('a valid credit cart number returns null', (){ + var result = FormValidator.creditCardNumberValidator('5555555555550000'); + expect(result, null); + + result = FormValidator.creditCardNumberValidator('4555555555550000'); + expect(result, null); + }); + + test('invalid credit card number returns error string', (){ + var result = FormValidator.creditCardNumberValidator(''); + expect(result, Constants.invalidCreditCardNumberError); + + result = FormValidator.creditCardNumberValidator('asd'); + expect(result, Constants.invalidCreditCardNumberError); + + result = FormValidator.creditCardNumberValidator('5655555555550000'); + expect(result, Constants.invalidCreditCardNumberError); + + result = FormValidator.creditCardNumberValidator('455555555555'); + expect(result, Constants.invalidCreditCardNumberError); + + result = FormValidator.creditCardNumberValidator('1234567890000000'); + expect(result, Constants.invalidCreditCardNumberError); + }); + + test('a valid credit cart CVV returns null', (){ + var result = FormValidator.creditCardCVVValidator('178'); + expect(result, null); + }); + + test('invalid credit card CVV returns error string', (){ + var result = FormValidator.creditCardCVVValidator(''); + expect(result, Constants.invalidCreditCardCVVError); + + result = FormValidator.creditCardCVVValidator('1'); + expect(result, Constants.invalidCreditCardCVVError); + + result = FormValidator.creditCardCVVValidator('1234'); + expect(result, Constants.invalidCreditCardCVVError); + + result = FormValidator.creditCardCVVValidator('asd'); + expect(result, Constants.invalidCreditCardCVVError); + }); + + test('a valid credit cart expiry returns null', (){ + var result = FormValidator.creditCardExpiryValidator('09/2021'); + expect(result, null); + + result = FormValidator.creditCardExpiryValidator('12/2030'); + expect(result, null); + }); + + test('invalid credit card expiry returns error string', (){ + var result = FormValidator.creditCardExpiryValidator(''); + expect(result, Constants.invalidCreditCardExpiryError); + + result = FormValidator.creditCardExpiryValidator('123'); + expect(result, Constants.invalidCreditCardExpiryError); + + result = FormValidator.creditCardExpiryValidator('asd'); + expect(result, Constants.invalidCreditCardExpiryError); + + result = FormValidator.creditCardExpiryValidator('09-2021'); + expect(result, Constants.invalidCreditCardExpiryError); + + result = FormValidator.creditCardExpiryValidator('09/1900'); + expect(result, Constants.invalidCreditCardExpiryError); + + result = FormValidator.creditCardExpiryValidator('00/2021'); + expect(result, Constants.invalidCreditCardExpiryError); + + result = FormValidator.creditCardExpiryValidator('13/2021'); + expect(result, Constants.invalidCreditCardExpiryError); + }); + }); + }); +} diff --git a/test/models/booking_model_test.dart b/test/models/booking_model_test.dart new file mode 100644 index 0000000..6881c53 --- /dev/null +++ b/test/models/booking_model_test.dart @@ -0,0 +1,423 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:ez_ticketz_app/enums/booking_status_enum.dart'; +import 'package:ez_ticketz_app/models/booking_model.dart'; + +void main() { + group('fromJson', () { + test( + 'GIVEN a valid booking json ' + 'WHEN json deserialization is performed' + 'THEN a booking model is output', + () { + //given + final json = { + 'booking_id': 1, + 'user_id': 1, + 'show_id': 1, + 'seat_row': 'A', + 'seat_number': 10, + 'price': 700.2, + 'booking_datetime': '2012-02-27T13:27:00.000', + 'booking_status': 'confirmed', + }; + + //when + final actual = BookingModel.fromJson(json); + final matcher = BookingModel( + bookingId: 1, + userId: 1, + showId: 1, + seatRow: 'A', + seatNumber: 10, + price: 700.2, + bookingDatetime: DateTime(2012, 2, 27, 13, 27, 0), + bookingStatus: BookingStatus.CONFIRMED, + ); + + //then + expect(actual, matcher); + }, + ); + + test( + 'GIVEN a valid booking json ' + 'AND seat is non-null ' + 'WHEN a json deserialization is performed ' + 'THEN a booking model is output' + "AND it's seat is null", + () { + //given + final json = { + 'booking_id': 1, + 'user_id': 1, + 'show_id': 1, + 'seat_row': 'A', + 'seat': 'A-10', + 'seat_number': 10, + 'price': 700.2, + 'booking_datetime': '2012-02-27T13:27:00.000', + 'booking_status': 'confirmed', + }; + + //when + final model = BookingModel.fromJson(json); + + //then + expect(model.seat, null); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN a booking model ' + "AND it's booking id is non-null " + "AND it's seat row is null or non-null " + "AND it's seat number is null or non-null" + 'WHEN json serialization is performed ' + 'THEN a booking json is output' + "AND it doesn't contain a key booking_id " + "AND it doesn't contain a key seat_row " + "AND it doesn't contain a key seat_number", + () { + //given + final booking = BookingModel( + bookingId: 1, + seatRow: 'A', + seatNumber: 10, + userId: 1, + showId: 1, + price: 700.2, + bookingDatetime: DateTime(2012, 2, 27, 13, 27, 0), + bookingStatus: BookingStatus.CONFIRMED, + ); + + //when + final actual = booking.toJson(); + final matcher = { + 'user_id': 1, + 'show_id': 1, + 'price': 700.2, + 'booking_datetime': '2012-02-27T13:27:00.000', + 'booking_status': 'confirmed', + }; + + //then + expect(actual, matcher); + }, + ); + + test( + 'GIVEN a booking model ' + "AND it's booking id is null " + "AND it's seat row is null or non-null " + "AND it's seat number is null or non-null" + 'WHEN json serialization is performed ' + 'THEN a booking json is output' + "AND it doesn't contain a key booking_id " + "AND it doesn't contain a key seat_row " + "AND it doesn't contain a key seat_number", + () { + //given + final booking = BookingModel( + bookingId: null, + seatRow: 'A', + seatNumber: 10, + userId: 1, + showId: 1, + price: 700.2, + bookingDatetime: DateTime(2012, 2, 27, 13, 27, 0), + bookingStatus: BookingStatus.CONFIRMED, + ); + + //when + final actual = booking.toJson(); + final matcher = { + 'user_id': 1, + 'show_id': 1, + 'price': 700.2, + 'booking_datetime': '2012-02-27T13:27:00.000', + 'booking_status': 'confirmed', + }; + + //then + expect(actual, matcher); + }, + ); + + test( + 'GIVEN a booking model ' + "AND it's seat row is null or non-null " + "AND it's seat number is null or non-null" + 'WHEN json serialization is performed ' + 'THEN a booking json is output' + "AND it doesn't contain a key seat_row " + "AND it doesn't contain a key seat_number", + () { + //given + final booking = BookingModel( + bookingId: null, + userId: 1, + showId: 1, + price: 700.2, + bookingDatetime: DateTime(2012, 2, 27, 13, 27, 0), + bookingStatus: BookingStatus.CONFIRMED, + ); + + //when + final actual = booking.toJson(); + final matcher = { + 'user_id': 1, + 'show_id': 1, + 'price': 700.2, + 'booking_datetime': '2012-02-27T13:27:00.000', + 'booking_status': 'confirmed', + }; + + //then + expect(actual, matcher); + }, + ); + + test( + 'GIVEN a booking model ' + "AND it's seat is non-null " + 'WHEN json serialization is performed ' + 'THEN a booking json is output ' + 'AND it contains a seat key', + () { + //given + final booking = BookingModel( + bookingId: null, + userId: 1, + showId: 1, + seat: 'A-10', + price: 700.2, + bookingDatetime: DateTime(2012, 2, 27, 13, 27, 0), + bookingStatus: BookingStatus.CONFIRMED, + ); + + //when + final actual = booking.toJson(); + + //then + expect(actual.containsKey('seat'), true); + expect(actual['seat'], 'A-10'); + }, + ); + + test( + 'GIVEN a booking model ' + "AND it's user id is null " + 'WHEN json serialization is performed ' + 'THEN a booking json is output ' + "AND it doesn't contain a key user_id", + () { + //given + final booking = BookingModel( + bookingId: null, + userId: null, + showId: 1, + price: 700.2, + bookingDatetime: DateTime(2012, 2, 27, 13, 27, 0), + bookingStatus: BookingStatus.CONFIRMED, + ); + + //when + final model = booking.toJson(); + + //then + expect(model.containsKey('user_id'), false); + }, + ); + + test( + 'GIVEN a booking model ' + "AND it's show id is null " + 'WHEN json serialization is performed ' + 'THEN a booking json is output ' + "AND it doesn't contain a key show_id", + () { + //given + final booking = BookingModel( + bookingId: null, + showId: null, + userId: 1, + price: 700.2, + bookingDatetime: DateTime(2012, 2, 27, 13, 27, 0), + bookingStatus: BookingStatus.CONFIRMED, + ); + + //when + final model = booking.toJson(); + + //then + expect(model.containsKey('show_id'), false); + }, + ); + + test( + 'GIVEN a booking model ' + "AND it's show id is null " + "AND it's user id is null " + 'WHEN json serialization is performed ' + 'THEN a booking json is output ' + "AND it doesn't contain a key show_id " + "AND it doesn't contain a key user_id", + () { + //given + final booking = BookingModel( + bookingId: null, + userId: null, + showId: null, + price: 700.2, + bookingDatetime: DateTime(2012, 2, 27, 13, 27, 0), + bookingStatus: BookingStatus.CONFIRMED, + ); + + //when + final model = booking.toJson(); + + //then + expect(model.containsKey('user_id'), false); + expect(model.containsKey('show_id'), false); + }, + ); + }); + + group('toUpdateJson', () { + test( + 'GIVEN a booking model ' + 'WHEN json serialization is performed for updating' + 'AND all arguments are null ' + 'THEN an empty json is output', + () { + //given + final model = BookingModel( + bookingId: 1, + seatRow: 'A', + seatNumber: 10, + userId: 1, + showId: 1, + price: 700.2, + bookingDatetime: DateTime(2012, 2, 27, 13, 27, 0), + bookingStatus: BookingStatus.CONFIRMED, + ); + + //when + final actual = model.toUpdateJson(); + + //then + expect(actual.isEmpty, true); + }, + ); + + test( + 'GIVEN a booking model ' + 'WHEN json serialization is performed for updating' + 'AND some arguments with new values are given ' + 'THEN a booking json is output ' + 'AND it has new values for the provided arguments', + () { + //given + final model = BookingModel( + bookingId: 1, + seatRow: 'A', + seatNumber: 10, + userId: null, + showId: 1, + price: 700.2, + bookingDatetime: DateTime(2012, 2, 27, 13, 27, 0), + bookingStatus: BookingStatus.CONFIRMED, + ); + + //when + const newBookingStatus = BookingStatus.RESERVED; + final actual = model.toUpdateJson( + bookingStatus: newBookingStatus, + ); + final matcher = { + 'show_id': 1, + 'price': 700.2, + 'booking_datetime': '2012-02-27T13:27:00.000', + 'booking_status': newBookingStatus.toJson, + }; + + //then + expect(actual, matcher); + }, + ); + }); + + group('equality', () { + test( + 'GIVEN two booking models ' + 'WHEN properties are different ' + 'THEN equality returns false', + () { + //given + final booking1 = BookingModel( + bookingId: 1, + seatRow: 'A', + seatNumber: 10, + userId: 1, + showId: 1, + price: 700.2, + bookingDatetime: DateTime(2012, 2, 27, 13, 27, 0), + bookingStatus: BookingStatus.CONFIRMED, + ); + + //when + final booking2 = BookingModel( + bookingId: 1, + seatRow: 'A', + seatNumber: 10, + userId: 1, + showId: 1, + price: 700.2, + bookingDatetime: DateTime(2012, 3, 27, 13, 27, 0), + //different month + bookingStatus: BookingStatus.CONFIRMED, + ); + + //then + expect(booking1 == booking2, false); + }, + ); + + test( + 'GIVEN two booking models ' + 'WHEN properties are same ' + 'THEN equality returns true', + () { + //given + final booking1 = BookingModel( + bookingId: 1, + seatRow: 'A', + seatNumber: 10, + userId: 1, + showId: 1, + price: 700.2, + bookingDatetime: DateTime(2012, 2, 27, 13, 27, 0), + bookingStatus: BookingStatus.CONFIRMED, + ); + + //when + final booking2 = BookingModel( + bookingId: 1, + seatRow: 'A', + seatNumber: 10, + userId: 1, + showId: 1, + price: 700.2, + bookingDatetime: DateTime(2012, 2, 27, 13, 27, 0), + bookingStatus: BookingStatus.CONFIRMED, + ); + + //then + expect(booking1 == booking2, true); + }, + ); + }); +} diff --git a/test/models/genre_model_test.dart b/test/models/genre_model_test.dart new file mode 100644 index 0000000..a170574 --- /dev/null +++ b/test/models/genre_model_test.dart @@ -0,0 +1,101 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:ez_ticketz_app/models/genre_model.dart'; + +void main() { + group('fromJson', () { + test( + 'GIVEN a valid genre json ' + 'WHEN json deserialization is performed ' + 'THEN a genre model is output', + () { + //given + final json = { + 'genre_id': 1, + 'genre': 'Horror', + }; + + //when + final actual = GenreModel.fromJson(json); + const matcher = GenreModel( + genreId: 1, + genre: 'Horror', + ); + + //then + expect(actual, matcher); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN a genre model ' + 'WHEN json serialization is performed ' + 'THEN a genre json is output', + () { + //given + const genre = GenreModel( + genreId: 1, + genre: 'Horror', + ); + + //when + final actual = genre.toJson(); + final matcher = { + 'genre_id': 1, + 'genre': 'Horror', + }; + + //then + expect(actual, matcher); + }, + ); + }); + + group('equality', () { + test( + 'GIVEN two genre models ' + 'WHEN properties are different ' + 'THEN equality returns false', + () { + //given + const genre1 = GenreModel( + genreId: 1, + genre: 'Horror', + ); + + //when + const genre2 = GenreModel( + genreId: 2, + genre: 'Crime', + ); + + //then + expect(genre1 == genre2, false); + }, + ); + + test( + 'GIVEN two genre models ' + 'WHEN properties are same ' + 'THEN equality returns true', + () { + //given + const genre1 = GenreModel( + genreId: 1, + genre: 'Horror', + ); + + //when + const genre2 = GenreModel( + genreId: 1, + genre: 'Horror', + ); + + //then + expect(genre1 == genre2, true); + }, + ); + }); +} diff --git a/test/models/movie_model_test.dart b/test/models/movie_model_test.dart new file mode 100644 index 0000000..05626a7 --- /dev/null +++ b/test/models/movie_model_test.dart @@ -0,0 +1,451 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:ez_ticketz_app/enums/movie_type_enum.dart'; +import 'package:ez_ticketz_app/models/genre_model.dart'; +import 'package:ez_ticketz_app/models/movie_model.dart'; + +void main() { + const _genresJson = [ + {'genre_id': 1, 'genre': 'Horror'}, + {'genre_id': 2, 'genre': 'Comedy'}, + {'genre_id': 3, 'genre': 'Sci-Fi'}, + ]; + + const _genreModels = [ + GenreModel(genreId: 1, genre: 'Horror'), + GenreModel(genreId: 2, genre: 'Comedy'), + GenreModel(genreId: 3, genre: 'Sci-Fi'), + ]; + + group('fromJson', () { + test( + 'GIVEN a valid movie json ' + 'WHEN json deserialization is performed ' + 'THEN a movie model is output', + () { + //given + const json = { + 'movie_id': 1, + 'title': 'Test Movie', + 'summary': 'Some summary', + 'trailer_url': 'www.placeholders.com/test_video', + 'poster_url': 'www.placeholders.com/test_image', + 'year': 2021, + 'rating': 4.5, + 'movie_type': 'now_showing', + 'genres': _genresJson, + }; + + //when + final actual = MovieModel.fromJson(json); + final matcher = MovieModel( + movieId: 1, + year: 2021, + rating: 4.5, + title: 'Test Movie', + summary: 'Some summary', + trailerUrl: 'www.placeholders.com/test_video', + posterUrl: 'www.placeholders.com/test_image', + genres: _genreModels, + movieType: MovieType.NOW_SHOWING, + ); + + //then + expect(actual, matcher); + }, + ); + + test( + 'GIVEN a valid movie json ' + 'AND rating key is missing' + 'WHEN json deserialization is performed ' + 'THEN a movie model is output ' + "AND it's rating defaults to 0.0", + () { + //given + const json = { + 'movie_id': 1, + 'title': 'Test Movie', + 'summary': 'Some summary', + 'trailer_url': 'www.placeholders.com/test_video', + 'poster_url': 'www.placeholders.com/test_image', + 'year': 2021, + 'movie_type': 'now_showing', + 'genres': _genresJson, + }; + + //when + final actual = MovieModel.fromJson(json); + final matcher = MovieModel( + movieId: 1, + year: 2021, + rating: 0.0, + title: 'Test Movie', + summary: 'Some summary', + trailerUrl: 'www.placeholders.com/test_video', + posterUrl: 'www.placeholders.com/test_image', + genres: _genreModels, + movieType: MovieType.NOW_SHOWING, + ); + + //then + expect(actual, matcher); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN a movie model ' + 'WHEN json serialization is performed ' + 'THEN a movie json is output', + () { + //given + final model = MovieModel( + movieId: 1, + year: 2021, + rating: 4.5, + title: 'Test Movie', + summary: 'Some summary', + trailerUrl: 'www.placeholders.com/test_video', + posterUrl: 'www.placeholders.com/test_image', + genres: _genreModels, + movieType: MovieType.NOW_SHOWING, + ); + + //when + final actual = model.toJson(); + const matcher = { + 'title': 'Test Movie', + 'summary': 'Some summary', + 'trailer_url': 'www.placeholders.com/test_video', + 'poster_url': 'www.placeholders.com/test_image', + 'year': 2021, + 'rating': 4.5, + 'movie_type': 'now_showing', + 'genres': [1, 2, 3], + }; + + //then + expect(actual, matcher); + }, + ); + + test( + 'GIVEN a movie model ' + 'WHEN json serialization is performed ' + 'THEN a movie json is output ' + "AND it's `genres` key contains a list of genre ids", + () { + //given + final model = MovieModel( + movieId: 1, + year: 2021, + rating: 4.5, + title: 'Test Movie', + summary: 'Some summary', + trailerUrl: 'www.placeholders.com/test_video', + posterUrl: 'www.placeholders.com/test_image', + genres: _genreModels, + movieType: MovieType.NOW_SHOWING, + ); + + //when + final actual = model.toJson(); + + //then + expect(actual['genres'], [1, 2, 3]); + }, + ); + + test( + 'GIVEN a movie model ' + "AND it's movie id is null" + 'WHEN json serialization is performed ' + 'THEN a movie json is output ' + "AND it doesn't have a movie_id key", + () { + //given + final model = MovieModel( + movieId: null, + year: 2021, + rating: 4.5, + title: 'Test Movie', + summary: 'Some summary', + trailerUrl: 'www.placeholders.com/test_video', + posterUrl: 'www.placeholders.com/test_image', + genres: _genreModels, + movieType: MovieType.NOW_SHOWING, + ); + + //when + final actual = model.toJson(); + + //then + expect(actual.containsKey('movie_id'), false); + }, + ); + + test( + 'GIVEN a movie model ' + "AND it's movie id is non-null" + 'WHEN json serialization is performed ' + 'THEN a movie json is output ' + "AND it doesn't have a movie_id key", + () { + //given + final model = MovieModel( + movieId: 1, + year: 2021, + rating: 4.5, + title: 'Test Movie', + summary: 'Some summary', + trailerUrl: 'www.placeholders.com/test_video', + posterUrl: 'www.placeholders.com/test_image', + genres: _genreModels, + movieType: MovieType.NOW_SHOWING, + ); + + //when + final actual = model.toJson(); + + //then + expect(actual.containsKey('movie_id'), false); + }, + ); + + test( + 'GIVEN a movie model ' + "AND it's rating isn't passed a value" + 'WHEN json serialization is performed ' + 'THEN a movie json is output ' + "AND it's rating key is 0.0", + () { + //given + final model = MovieModel( + movieId: 1, + year: 2021, + title: 'Test Movie', + summary: 'Some summary', + trailerUrl: 'www.placeholders.com/test_video', + posterUrl: 'www.placeholders.com/test_image', + genres: _genreModels, + movieType: MovieType.NOW_SHOWING, + ); + + //when + final actual = model.toJson(); + + //then + expect(actual['rating'], 0.0); + }, + ); + }); + + group('toUpdateJson', () { + test( + 'GIVEN a movie model ' + 'WHEN json serialization is performed for updating' + 'AND all arguments are null ' + 'THEN an empty json is output', + () { + //given + final model = MovieModel( + movieId: 1, + year: 2021, + rating: 4.5, + title: 'Test Movie', + summary: 'Some summary', + trailerUrl: 'www.placeholders.com/test_video', + posterUrl: 'www.placeholders.com/test_image', + genres: _genreModels, + movieType: MovieType.NOW_SHOWING, + ); + + //when + final actual = model.toUpdateJson(); + + //then + expect(actual.isEmpty, true); + }, + ); + + test( + 'GIVEN a movie model ' + 'WHEN json serialization is performed for updating' + 'AND some arguments with new values are given ' + 'THEN a movie json is output ' + 'AND it has new values for the provided arguments', + () { + //given + final model = MovieModel( + movieId: 1, + year: 2021, + rating: 4.5, + title: 'Test Movie', + summary: 'Some summary', + trailerUrl: 'www.placeholders.com/test_video', + posterUrl: 'www.placeholders.com/test_image', + genres: _genreModels, + movieType: MovieType.NOW_SHOWING, + ); + + //when + const newMovieType = MovieType.COMING_SOON; + final actual = model.toUpdateJson( + movieType: newMovieType, + ); + final matcher = { + 'title': 'Test Movie', + 'summary': 'Some summary', + 'trailer_url': 'www.placeholders.com/test_video', + 'poster_url': 'www.placeholders.com/test_image', + 'year': 2021, + 'rating': 4.5, + 'movie_type': 'coming_soon', + 'genres': [1, 2, 3], + }; + + //then + expect(actual, matcher); + }, + ); + }); + + group('initial', () { + test( + 'GIVEN a set of default values for different properties' + 'WHEN factory constructor `initial` is called' + 'THEN an movie model is output ' + "AND it's properties match those set of properties", + () { + //given + const defaultString = ''; + const defaultInt = 0; + const int? defaultMovieId = null; + const defaultList = []; + const defaultMovieType = MovieType.COMING_SOON; + + //when + final model = MovieModel.initial(); + + //then + expect(model.title, defaultString); + expect(model.summary, defaultString); + expect(model.posterUrl, defaultString); + expect(model.trailerUrl, defaultString); + expect(model.year, defaultInt); + expect(model.movieId, defaultMovieId); + expect(model.genres, defaultList); + expect(model.movieType, defaultMovieType); + }, + ); + }); + + group('getGenreName', () { + test( + 'GIVEN a movie model ' + 'WHEN the variable genreNames is accessed ' + 'THEN a list of genre names is output', + () { + //given + final model = MovieModel( + movieId: 1, + year: 2021, + rating: 4.5, + title: 'Test Movie', + summary: 'Some summary', + trailerUrl: 'www.placeholders.com/test_video', + posterUrl: 'www.placeholders.com/test_image', + genres: _genreModels, + movieType: MovieType.NOW_SHOWING, + ); + + //when + final genreNames = model.genreNames; + + //then + expect(genreNames, ['Horror', 'Comedy', 'Sci-Fi']); + }, + ); + }); + + group('equality', () { + test( + 'GIVEN two movie models ' + 'WHEN properties are different ' + 'THEN equality returns false', + () { + //given + final model1 = MovieModel( + movieId: 1, + year: 2021, + rating: 4.5, + title: 'Test Movie', + summary: 'Some summary', + trailerUrl: 'www.placeholders.com/test_video', + posterUrl: 'www.placeholders.com/test_image', + genres: _genreModels, + movieType: MovieType.NOW_SHOWING, + ); + + //when + final model2 = MovieModel( + movieId: 1, + year: 2022, + rating: 6.5, + title: 'Test Movie 2', + summary: 'Some summary', + trailerUrl: 'www.placeholders.com/test_video', + posterUrl: 'www.placeholders.com/test_image', + genres: _genreModels, + movieType: MovieType.COMING_SOON, + ); + + //then + expect(model1 == model2, false); + }, + ); + + test( + 'GIVEN two movie models ' + 'WHEN properties are same ' + 'THEN equality returns true', + () { + //given + final model1 = MovieModel( + movieId: 1, + year: 2021, + rating: 4.5, + title: 'Test Movie', + summary: 'Some summary', + trailerUrl: 'www.placeholders.com/test_video', + posterUrl: 'www.placeholders.com/test_image', + genres: _genreModels, + movieType: MovieType.NOW_SHOWING, + ); + + //when + const genreModels2 = [ + GenreModel(genreId: 1, genre: 'Horror'), + GenreModel(genreId: 2, genre: 'Comedy'), + GenreModel(genreId: 3, genre: 'Sci-Fi'), + ]; + final model2 = MovieModel( + movieId: 1, + year: 2021, + rating: 4.5, + title: 'Test Movie', + summary: 'Some summary', + trailerUrl: 'www.placeholders.com/test_video', + posterUrl: 'www.placeholders.com/test_image', + genres: genreModels2, + movieType: MovieType.NOW_SHOWING, + ); + + //then + expect(model1 == model2, true); + }, + ); + }); +} diff --git a/test/models/movie_role_model_test.dart b/test/models/movie_role_model_test.dart new file mode 100644 index 0000000..745d477 --- /dev/null +++ b/test/models/movie_role_model_test.dart @@ -0,0 +1,133 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:ez_ticketz_app/models/role_model.dart'; +import 'package:ez_ticketz_app/enums/role_type_enum.dart'; +import 'package:ez_ticketz_app/models/movie_role_model.dart'; + +void main() { + late RoleModel _roleModel; + + setUp((){ + _roleModel = const RoleModel( + roleId: 1, + fullName: 'Mr.Test', + age: 30, + pictureUrl: 'www.placeholders.com/test_image', + ); + }); + + group('fromJson', () { + test( + 'GIVEN a valid movie role json ' + 'WHEN json deserialization is performed ' + 'THEN a movie role model is output', + () { + //given + const roleJson = { + 'role_id': 1, + 'full_name': 'Mr.Test', + 'age': 30, + 'picture_url': 'www.placeholders.com/test_image', + }; + final movieRoleJson = { + 'movie_id': 1, + 'role': roleJson, + 'role_type': 'cast', + }; + + //when + final actual = MovieRoleModel.fromJson(movieRoleJson); + final matcher = MovieRoleModel( + movieId: 1, + role: _roleModel, + roleType: RoleType.CAST, + ); + + //then + expect(actual, matcher); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN a movie role model ' + 'WHEN json serialization is performed ' + 'THEN a movie role json is output', + () { + //given + final movieRole = MovieRoleModel( + movieId: 1, + role: _roleModel, + roleType: RoleType.CAST, + ); + + //when + final actual = movieRole.toCustomJson(); + final matcher = { + 'role_id': 1, + 'role_type': 'cast', + }; + + //then + expect(actual, matcher); + }, + ); + }); + + group('equality', () { + test( + 'GIVEN two movie role models ' + 'WHEN properties are different ' + 'THEN equality returns false', + () { + //given + final movieRole1 = MovieRoleModel( + movieId: 1, + role: _roleModel, + roleType: RoleType.DIRECTOR, + ); + + //when + final movieRole2 = MovieRoleModel( + movieId: 1, + role: _roleModel, + roleType: RoleType.PRODUCER, + ); + + //then + expect(movieRole1 == movieRole2, false); + }, + ); + + test( + 'GIVEN two movie role models ' + 'WHEN properties are same ' + 'THEN equality returns true', + () { + //given + final movieRole1 = MovieRoleModel( + movieId: 1, + role: _roleModel, + roleType: RoleType.DIRECTOR, + ); + + //when + const roleModel2 = RoleModel( + roleId: 1, + fullName: 'Mr.Test', + age: 30, + pictureUrl: 'www.placeholders.com/test_image', + ); + const movieRole2 = MovieRoleModel( + movieId: 1, + role: roleModel2, + roleType: RoleType.DIRECTOR, + ); + + //then + expect(movieRole1 == movieRole2, true); + }, + ); + }); +} diff --git a/test/models/payment_model_test.dart b/test/models/payment_model_test.dart new file mode 100644 index 0000000..52e71a5 --- /dev/null +++ b/test/models/payment_model_test.dart @@ -0,0 +1,362 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:ez_ticketz_app/enums/payment_method_enum.dart'; +import 'package:ez_ticketz_app/models/payment_model.dart'; + +void main() { + group('fromJson', () { + test( + 'GIVEN a valid payment json ' + 'WHEN json deserialization is performed ' + 'THEN a payment model is output', + () { + //given + const json = { + 'payment_id': 1, + 'show_id': 1, + 'user_id': 1, + 'amount': 999.9, + 'payment_datetime': '2012-02-27T13:27:00.000', + 'payment_method': 'card', + 'bookings': [0, 1, 2], + }; + + //when + final actual = PaymentModel.fromJson(json); + final matcher = PaymentModel( + paymentId: 1, + amount: 999.9, + userId: 1, + showId: 1, + paymentDatetime: DateTime(2012, 2, 27, 13, 27, 0), + paymentMethod: PaymentMethod.CARD, + bookings: null, + ); + + //then + expect(actual, matcher); + }, + ); + + test( + 'GIVEN a valid payment json ' + "AND it's bookings key is non-null " + 'WHEN json deserialization is performed ' + 'THEN a payment model is output ' + "AND it's bookings are null", + () { + //given + const json = { + 'payment_id': 1, + 'show_id': 1, + 'user_id': 1, + 'amount': 999.9, + 'payment_datetime': '2012-02-27T13:27:00.000', + 'payment_method': 'card', + 'bookings': [0, 1, 2], + }; + + //when + final actual = PaymentModel.fromJson(json); + + //then + expect(actual.bookings, null); + }, + ); + + test( + 'GIVEN a valid payment json ' + "AND it's bookings key is null " + 'WHEN json deserialization is performed ' + 'THEN a payment model is output ' + "AND it's bookings are null", + () { + //given + const json = { + 'payment_id': 1, + 'show_id': 1, + 'user_id': 1, + 'amount': 999.9, + 'payment_datetime': '2012-02-27T13:27:00.000', + 'payment_method': 'card', + 'bookings': null, + }; + + final actual = PaymentModel.fromJson(json); + + //then + expect(actual.bookings, null); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN a payment model ' + 'WHEN json serialization is performed ' + 'THEN a payment json is output', + () { + //given + final model = PaymentModel( + paymentId: null, + amount: 999.9, + userId: 1, + showId: 1, + paymentDatetime: DateTime(2012, 2, 27, 13, 27, 0), + paymentMethod: PaymentMethod.CARD, + bookings: [0, 1, 2], + ); + + //when + final actual = model.toJson(); + final matcher = { + 'show_id': 1, + 'user_id': 1, + 'amount': 999.9, + 'payment_datetime': '2012-02-27T13:27:00.000', + 'payment_method': 'card', + 'bookings': [0, 1, 2], + }; + + //then + expect(actual, matcher); + }, + ); + + test( + 'GIVEN a payment model ' + "AND it's payment id is null" + 'WHEN json serialization is performed ' + 'THEN a payment json is output ' + "AND it doesn't have a payment_id key", + () { + //given + final model = PaymentModel( + paymentId: null, + amount: 999.9, + userId: 1, + showId: 1, + paymentDatetime: DateTime(2012, 2, 27, 13, 27, 0), + paymentMethod: PaymentMethod.CARD, + bookings: [0, 1, 2], + ); + + //when + final actual = model.toJson(); + + //then + expect(actual.containsKey('payment_id'), false); + }, + ); + + test( + 'GIVEN a payment model ' + "AND it's payment id is non-null" + 'WHEN json serialization is performed ' + 'THEN a payment json is output ' + "AND it doesn't have a payment_id key", + () { + //given + final model = PaymentModel( + paymentId: 1, + amount: 999.9, + userId: 1, + showId: 1, + paymentDatetime: DateTime(2012, 2, 27, 13, 27, 0), + paymentMethod: PaymentMethod.CARD, + bookings: [0, 1, 2], + ); + + //when + final actual = model.toJson(); + + //then + expect(actual.containsKey('payment_id'), false); + }, + ); + + test( + 'GIVEN a payment model ' + "AND it's bookings is null" + 'WHEN json serialization is performed ' + 'THEN a payment json is output ' + "AND it doesn't have a bookings key", + () { + //given + final model = PaymentModel( + paymentId: 1, + amount: 999.9, + userId: 1, + showId: 1, + paymentDatetime: DateTime(2012, 2, 27, 13, 27, 0), + paymentMethod: PaymentMethod.CARD, + bookings: null, + ); + + //when + final actual = model.toJson(); + + //then + expect(actual.containsKey('bookings'), false); + }, + ); + + test( + 'GIVEN a payment model ' + "AND it's bookings is non-null" + 'WHEN json serialization is performed ' + 'THEN a payment json is output ' + 'AND it has a bookings key', + () { + //given + final bookings = [0, 1, 2]; + final model = PaymentModel( + paymentId: 1, + amount: 999.9, + userId: 1, + showId: 1, + paymentDatetime: DateTime(2012, 2, 27, 13, 27, 0), + paymentMethod: PaymentMethod.CARD, + bookings: bookings, + ); + + //when + final actual = model.toJson(); + + //then + expect(actual.containsKey('bookings'),true); + expect(actual['bookings'], bookings); + }, + ); + }); + + group('toUpdateJson', () { + test( + 'GIVEN a payment model ' + 'WHEN json serialization is performed for updating' + 'AND all arguments are null ' + 'THEN an empty json is output', + () { + //given + final model = PaymentModel( + paymentId: null, + amount: 999.9, + userId: 1, + showId: 1, + paymentDatetime: DateTime(2012, 2, 27, 13, 27, 0), + paymentMethod: PaymentMethod.CARD, + bookings: [0, 1, 2], + ); + + //when + final actual = model.toUpdateJson(); + + //then + expect(actual.isEmpty, true); + }, + ); + + test( + 'GIVEN a payment model ' + 'WHEN json serialization is performed for updating' + 'AND some arguments with new values are given ' + 'THEN a payment json is output ' + 'AND it has new values for the provided arguments', + () { + //given + final model = PaymentModel( + paymentId: null, + amount: 999.9, + userId: 1, + showId: 1, + paymentDatetime: DateTime(2012, 2, 27, 13, 27, 0), + paymentMethod: PaymentMethod.CARD, + bookings: [0, 1, 2], + ); + + //when + const newPaymentMethod = PaymentMethod.CASH; + final actual = model.toUpdateJson( + paymentMethod: newPaymentMethod, + ); + final matcher = { + 'show_id': 1, + 'user_id': 1, + 'amount': 999.9, + 'payment_datetime': '2012-02-27T13:27:00.000', + 'payment_method': 'cash', + 'bookings': [0, 1, 2], + }; + + //then + expect(actual, matcher); + }, + ); + }); + + group('equality', () { + test( + 'GIVEN two payment models ' + 'WHEN properties are different ' + 'THEN equality returns false', + () { + //given + final payment1 = PaymentModel( + paymentId: 1, + amount: 999.9, + userId: 1, + showId: 1, + paymentDatetime: DateTime(2012, 2, 27, 13, 27, 0), + paymentMethod: PaymentMethod.CARD, + bookings: null, + ); + + //when + final payment2 = PaymentModel( + paymentId: 1, + amount: 999.9, + userId: 1, + showId: 1, + paymentDatetime: DateTime(2012, 2, 27, 13, 27, 0), + paymentMethod: PaymentMethod.CARD, + bookings: [0, 1, 2], + ); + + //then + expect(payment1 == payment2, false); + }, + ); + + test( + 'GIVEN two payment models ' + 'WHEN properties are same ' + 'THEN equality returns true', + () { + //given + final payment1 = PaymentModel( + paymentId: 1, + amount: 999.9, + userId: 1, + showId: 1, + paymentDatetime: DateTime(2012, 2, 27, 13, 27, 0), + paymentMethod: PaymentMethod.CARD, + bookings: [0, 1, 2], + ); + + //when + final payment2 = PaymentModel( + paymentId: 1, + amount: 999.9, + userId: 1, + showId: 1, + paymentDatetime: DateTime(2012, 2, 27, 13, 27, 0), + paymentMethod: PaymentMethod.CARD, + bookings: [0, 1, 2], + ); + + //then + expect(payment1 == payment2, true); + }, + ); + }); +} diff --git a/test/models/role_model_test.dart b/test/models/role_model_test.dart new file mode 100644 index 0000000..fc320ef --- /dev/null +++ b/test/models/role_model_test.dart @@ -0,0 +1,117 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:ez_ticketz_app/models/role_model.dart'; + +void main() { + group('fromJson', () { + test( + 'GIVEN a valid role json ' + 'WHEN json deserialization is performed ' + 'THEN a role model is output', + () { + //given + final json = { + 'role_id': 1, + 'full_name': 'Mr.Test', + 'age': 30, + 'picture_url': 'www.placeholders.com/test_image', + }; + + //when + final actual = RoleModel.fromJson(json); + const matcher = RoleModel( + roleId: 1, + fullName: 'Mr.Test', + age: 30, + pictureUrl: 'www.placeholders.com/test_image', + ); + + //then + expect(actual, matcher); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN a role model ' + 'WHEN json serialization is performed ' + 'THEN a role json is output', + () { + //given + const role = RoleModel( + roleId: 1, + fullName: 'Mr.Test', + age: 30, + pictureUrl: 'www.placeholders.com/test_image', + ); + + //when + final actual = role.toJson(); + final matcher = { + 'role_id': 1, + 'full_name': 'Mr.Test', + 'age': 30, + 'picture_url': 'www.placeholders.com/test_image', + }; + + //then + expect(actual, matcher); + }, + ); + }); + + group('equality', () { + test( + 'GIVEN two role models ' + 'WHEN properties are different ' + 'THEN equality returns false', + () { + //given + const role1 = RoleModel( + roleId: 1, + fullName: 'Mr.Test', + age: 30, + pictureUrl: 'www.placeholders.com/test_image', + ); + + //when + const role2 = RoleModel( + roleId: 2, + fullName: 'Mrs.Test', + age: 30, + pictureUrl: 'www.placeholders.com/test_image', + ); + + //then + expect(role1 == role2, false); + }, + ); + + test( + 'GIVEN two role models ' + 'WHEN properties are same ' + 'THEN equality returns true', + () { + //given + const role1 = RoleModel( + roleId: 1, + fullName: 'Mr.Test', + age: 30, + pictureUrl: 'www.placeholders.com/test_image', + ); + + //when + const role2 = RoleModel( + roleId: 1, + fullName: 'Mr.Test', + age: 30, + pictureUrl: 'www.placeholders.com/test_image', + ); + + //then + expect(role1 == role2, true); + }, + ); + }); +} diff --git a/test/models/seat_model_test.dart b/test/models/seat_model_test.dart new file mode 100644 index 0000000..bb4bcf1 --- /dev/null +++ b/test/models/seat_model_test.dart @@ -0,0 +1,101 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:ez_ticketz_app/models/seat_model.dart'; + +void main() { + group('fromJson', () { + test( + 'GIVEN a valid seat json ' + 'WHEN json deserialization is performed ' + 'THEN a seat model is output', + () { + //given + final json = { + 'seat_row': 'A', + 'seat_number': 1, + }; + + //when + final actual = SeatModel.fromJson(json); + const matcher = SeatModel( + seatRow: 'A', + seatNumber: 1, + ); + + //then + expect(actual, matcher); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN a seat model ' + 'WHEN json serialization is performed ' + 'THEN a seat json is output', + () { + //given + const seat = SeatModel( + seatRow: 'A', + seatNumber: 1, + ); + + //when + final actual = seat.toJson(); + final matcher = { + 'seat_row': 'A', + 'seat_number': 1, + }; + + //then + expect(actual, matcher); + }, + ); + }); + + group('equality', () { + test( + 'GIVEN two seat models ' + 'WHEN properties are different ' + 'THEN equality returns false', + () { + //given + const seat1 = SeatModel( + seatRow: 'A', + seatNumber: 1, + ); + + //when + const seat2 = SeatModel( + seatRow: 'B', + seatNumber: 2, + ); + + //then + expect(seat1 == seat2, false); + }, + ); + + test( + 'GIVEN two seat models ' + 'WHEN properties are same ' + 'THEN equality returns true', + () { + //given + const seat1 = SeatModel( + seatRow: 'A', + seatNumber: 1, + ); + + //when + const seat2 = SeatModel( + seatRow: 'A', + seatNumber: 1, + ); + + //then + expect(seat1 == seat2, true); + }, + ); + }); +} diff --git a/test/models/show_model_test.dart b/test/models/show_model_test.dart new file mode 100644 index 0000000..a40df16 --- /dev/null +++ b/test/models/show_model_test.dart @@ -0,0 +1,210 @@ +import 'package:clock/clock.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:ez_ticketz_app/enums/show_status_enum.dart'; +import 'package:ez_ticketz_app/enums/show_type_enum.dart'; +import 'package:ez_ticketz_app/models/show_model.dart'; +import 'package:ez_ticketz_app/models/show_time_model.dart'; + +void main() { + late ShowTimeModel _showTimeModel1, _showTimeModel2; + + setUp(() { + _showTimeModel1 = ShowTimeModel( + showId: 1, + theaterId: 1, + showType: ShowType.i2D, + showStatus: ShowStatus.FULL, + startTime: DateTime(2012, 2, 27, 13, 27, 0), + endTime: DateTime(2012, 2, 27, 14, 27, 0), + ); + _showTimeModel2 = ShowTimeModel( + showId: 2, + theaterId: 5, + showType: ShowType.i3D, + showStatus: ShowStatus.FREE, + startTime: DateTime(2012, 2, 27, 13, 27, 0), + endTime: DateTime(2012, 2, 27, 14, 27, 0), + ); + }); + + group('fromJson', () { + test( + 'GIVEN a valid show json ' + 'WHEN a json deserialization is performed' + 'THEN a show model is output', + () { + //given + const json = { + 'movie_id': 1, + 'date': '2012-02-27T13:27:00.000', + 'show_times': [ + { + 'show_id': 1, + 'start_time': '2012-02-27T13:27:00.000', + 'end_time': '2012-02-27T14:27:00.000', + 'show_status': 'full', + 'show_type': '2D', + 'theater_id': 1, + }, + { + 'show_id': 2, + 'start_time': '2012-02-27T13:27:00.000', + 'end_time': '2012-02-27T14:27:00.000', + 'show_status': 'free', + 'show_type': '3D', + 'theater_id': 5, + }, + ], + }; + + //when + final actual = ShowModel.fromJson(json); + final matcher = ShowModel( + date: DateTime(2012, 2, 27, 13, 27, 0), + movieId: 1, + showTimes: [_showTimeModel1, _showTimeModel2], + ); + + //then + expect(actual, matcher); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN show model ' + 'WHEN a json serialization is performed ' + 'THEN show json is output', + () { + //given + final model = ShowModel( + date: DateTime(2012, 2, 27, 13, 27, 0), + movieId: 1, + showTimes: [_showTimeModel1, _showTimeModel2], + ); + + //when + final actual = model.toJson(); + final matcher = { + 'movie_id': 1, + 'date': '2012-02-27T13:27:00.000', + 'show_times': [ + { + 'start_time': '2012-02-27T13:27:00.000', + 'end_time': '2012-02-27T14:27:00.000', + 'show_status': 'full', + 'show_type': '2D', + 'theater_id': 1, + }, + { + 'start_time': '2012-02-27T13:27:00.000', + 'end_time': '2012-02-27T14:27:00.000', + 'show_status': 'free', + 'show_type': '3D', + 'theater_id': 5, + }, + ], + }; + + //then + expect(actual, matcher); + }, + ); + }); + + group('initial', () { + test( + 'GIVEN a set of default values for different properties' + 'WHEN factory constructor `initial` is called' + 'THEN an show model is output ' + "AND it's properties match those set of properties", + () { + //given + const defaultInt = 0; + final defaultDatetime = DateTime.now(); + const defaultList = []; + + //when + final model = withClock(Clock.fixed(defaultDatetime), () { + return ShowModel.initial(); + }); + + //then + expect(model.date, defaultDatetime); + expect(model.movieId, defaultInt); + expect(model.showTimes, defaultList); + }, + ); + }); + + group('equality', () { + test( + 'GIVEN two show models ' + 'WHEN properties are different ' + 'THEN equality returns false', + () { + //given + final model1 = ShowModel( + date: DateTime(2012, 2, 27, 13, 27, 0), + movieId: 1, + showTimes: [_showTimeModel1, _showTimeModel2], + ); + + //when + final model2 = ShowModel( + date: DateTime(2012, 2, 27, 15, 27, 0), //different hour + movieId: 1, + showTimes: [_showTimeModel1, _showTimeModel2], + ); + + //then + expect(model1 == model2, false); + }, + ); + + test( + 'GIVEN two show time models ' + 'WHEN properties are same ' + 'THEN equality returns true', + () { + //given + final model1 = ShowModel( + date: DateTime(2012, 2, 27, 13, 27, 0), + movieId: 1, + showTimes: [_showTimeModel1, _showTimeModel2], + ); + + //when + final showTimes2 = [ + ShowTimeModel( + showId: 1, + theaterId: 1, + showType: ShowType.i2D, + showStatus: ShowStatus.FULL, + startTime: DateTime(2012, 2, 27, 13, 27, 0), + endTime: DateTime(2012, 2, 27, 14, 27, 0), + ), + ShowTimeModel( + showId: 2, + theaterId: 5, + showType: ShowType.i3D, + showStatus: ShowStatus.FREE, + startTime: DateTime(2012, 2, 27, 13, 27, 0), + endTime: DateTime(2012, 2, 27, 14, 27, 0), + ) + ]; + + final model2 = ShowModel( + date: DateTime(2012, 2, 27, 13, 27, 0), + movieId: 1, + showTimes: showTimes2, + ); + + //then + expect(model1 == model2, true); + }, + ); + }); +} diff --git a/test/models/show_seating_model_test.dart b/test/models/show_seating_model_test.dart new file mode 100644 index 0000000..b69f9ec --- /dev/null +++ b/test/models/show_seating_model_test.dart @@ -0,0 +1,137 @@ +//Enums +import 'package:ez_ticketz_app/enums/show_status_enum.dart'; +import 'package:ez_ticketz_app/enums/show_type_enum.dart'; +import 'package:ez_ticketz_app/enums/theater_type_enum.dart'; + +//Models +import 'package:ez_ticketz_app/models/seat_model.dart'; +import 'package:ez_ticketz_app/models/show_seating_model.dart'; +import 'package:ez_ticketz_app/models/show_time_model.dart'; +import 'package:ez_ticketz_app/models/theater_model.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + const _theaterModel = TheaterModel( + theaterId: 1, + theaterName: 'A', + numOfRows: 10, + seatsPerRow: 10, + theaterType: TheaterType.NORMAL, + missing: [ + SeatModel(seatRow: 'A', seatNumber: 3), + SeatModel(seatRow: 'B', seatNumber: 3), + SeatModel(seatRow: 'C', seatNumber: 3), + SeatModel(seatRow: 'D', seatNumber: 3), + ], + blocked: [ + SeatModel(seatRow: 'E', seatNumber: 1), + SeatModel(seatRow: 'E', seatNumber: 2), + SeatModel(seatRow: 'E', seatNumber: 3), + SeatModel(seatRow: 'E', seatNumber: 4), + ], + ); + + final _showTimeModel = ShowTimeModel( + showId: 1, + theaterId: 1, + showType: ShowType.i2D, + showStatus: ShowStatus.FULL, + startTime: DateTime(2012, 2, 27, 13, 27, 0), + endTime: DateTime(2012, 2, 27, 14, 27, 0), + ); + + const _bookedSeatModels = [ + SeatModel(seatRow: 'A', seatNumber: 1), + SeatModel(seatRow: 'A', seatNumber: 2), + SeatModel(seatRow: 'A', seatNumber: 4), + SeatModel(seatRow: 'A', seatNumber: 5), + ]; + + group('equality', () { + test( + 'GIVEN two show seating models ' + 'WHEN properties are different ' + 'THEN equality returns false', + () { + //given + final model1 = ShowSeatingModel( + showTime: _showTimeModel, + theater: _theaterModel, + bookedSeats: _bookedSeatModels, + ); + + //when + final model2 = ShowSeatingModel( + showTime: _showTimeModel, + theater: _theaterModel, + bookedSeats: const [ + SeatModel(seatRow: 'B', seatNumber: 1), + SeatModel(seatRow: 'B', seatNumber: 2), + SeatModel(seatRow: 'B', seatNumber: 4), + SeatModel(seatRow: 'B', seatNumber: 5), + ], + ); + + //then + expect(model1 == model2, false); + }, + ); + + test( + 'GIVEN two show seating models ' + 'WHEN properties are same ' + 'THEN equality returns true', + () { + //given + final model1 = ShowSeatingModel( + showTime: _showTimeModel, + theater: _theaterModel, + bookedSeats: _bookedSeatModels, + ); + + //when + const theaterModel2 = TheaterModel( + theaterId: 1, + theaterName: 'A', + numOfRows: 10, + seatsPerRow: 10, + theaterType: TheaterType.NORMAL, + missing: [ + SeatModel(seatRow: 'A', seatNumber: 3), + SeatModel(seatRow: 'B', seatNumber: 3), + SeatModel(seatRow: 'C', seatNumber: 3), + SeatModel(seatRow: 'D', seatNumber: 3), + ], + blocked: [ + SeatModel(seatRow: 'E', seatNumber: 1), + SeatModel(seatRow: 'E', seatNumber: 2), + SeatModel(seatRow: 'E', seatNumber: 3), + SeatModel(seatRow: 'E', seatNumber: 4), + ], + ); + final showTimeModel2 = ShowTimeModel( + showId: 1, + theaterId: 1, + showType: ShowType.i2D, + showStatus: ShowStatus.FULL, + startTime: DateTime(2012, 2, 27, 13, 27, 0), + endTime: DateTime(2012, 2, 27, 14, 27, 0), + ); + const bookedSeatModels2 = [ + SeatModel(seatRow: 'A', seatNumber: 1), + SeatModel(seatRow: 'A', seatNumber: 2), + SeatModel(seatRow: 'A', seatNumber: 4), + SeatModel(seatRow: 'A', seatNumber: 5), + ]; + final model2 = ShowSeatingModel( + showTime: showTimeModel2, + theater: theaterModel2, + bookedSeats: bookedSeatModels2, + ); + + //then + expect(model1 == model2, true); + }, + ); + }); +} diff --git a/test/models/show_time_model_test.dart b/test/models/show_time_model_test.dart new file mode 100644 index 0000000..21970cb --- /dev/null +++ b/test/models/show_time_model_test.dart @@ -0,0 +1,212 @@ +import 'package:clock/clock.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:ez_ticketz_app/enums/show_status_enum.dart'; +import 'package:ez_ticketz_app/enums/show_type_enum.dart'; +import 'package:ez_ticketz_app/models/show_time_model.dart'; + +void main() { + group('fromJson', () { + test( + 'GIVEN a valid show time json ' + 'WHEN a json deserialization is performed' + 'THEN a show time model is output', + () { + //given + final json = { + 'show_id': 1, + 'start_time': '2012-02-27T13:27:00.000', + 'end_time': '2012-02-27T14:27:00.000', + 'show_status': 'full', + 'show_type': '2D', + 'theater_id': 1, + }; + + //when + final actual = ShowTimeModel.fromJson(json); + final matcher = ShowTimeModel( + showId: 1, + theaterId: 1, + showType: ShowType.i2D, + showStatus: ShowStatus.FULL, + startTime: DateTime(2012, 2, 27, 13, 27, 0), + endTime: DateTime(2012, 2, 27, 14, 27, 0), + ); + + //then + expect(actual, matcher); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN show time model ' + 'WHEN a json serialization is performed ' + 'THEN show time json is output', + () { + //given + final model = ShowTimeModel( + showId: 1, + theaterId: 1, + showType: ShowType.i2D, + showStatus: ShowStatus.FULL, + startTime: DateTime(2012, 2, 27, 13, 27, 0), + endTime: DateTime(2012, 2, 27, 14, 27, 0), + ); + + //when + final actual = model.toJson(); + final matcher = { + 'start_time': '2012-02-27T13:27:00.000', + 'end_time': '2012-02-27T14:27:00.000', + 'show_status': 'full', + 'show_type': '2D', + 'theater_id': 1, + }; + + //then + expect(actual, matcher); + }, + ); + + test( + 'GIVEN a show time model ' + 'AND without specifying a show id' + 'WHEN json serialization is performed ' + 'THEN a show time json is output ' + "AND it doesn't have a show_id key", + () { + //given + final model = ShowTimeModel( + theaterId: 1, + showType: ShowType.i2D, + showStatus: ShowStatus.FULL, + startTime: DateTime(2012, 2, 27, 13, 27, 0), + endTime: DateTime(2012, 2, 27, 14, 27, 0), + ); + + //when + final actual = model.toJson(); + + //then + expect(actual.containsKey('show_id'), false); + }, + ); + + test( + 'GIVEN a show time model ' + 'AND with a show id specified' + 'WHEN json serialization is performed ' + 'THEN a show time json is output ' + "AND it doesn't have a show_id key", + () { + //given + final model = ShowTimeModel( + showId: 10, + theaterId: 1, + showType: ShowType.i2D, + showStatus: ShowStatus.FULL, + startTime: DateTime(2012, 2, 27, 13, 27, 0), + endTime: DateTime(2012, 2, 27, 14, 27, 0), + ); + + //when + final actual = model.toJson(); + + //then + expect(actual.containsKey('show_id'), false); + }, + ); + }); + + group('initial', () { + test( + 'GIVEN a set of default values for different properties' + 'WHEN factory constructor `initial` is called' + 'THEN an show time model is output ' + "AND it's properties match those set of properties", + () { + //given + const defaultInt = 0; + final defaultDatetime = DateTime.now(); + const defaultShowStatus = ShowStatus.FREE; + const defaultShowType = ShowType.i2D; + + //when + final model = withClock(Clock.fixed(defaultDatetime), (){ + return ShowTimeModel.initial(); + }); + + //then + expect(model.startTime, defaultDatetime); + expect(model.endTime, defaultDatetime); + expect(model.theaterId, defaultInt); + expect(model.showType, defaultShowType); + expect(model.showStatus, defaultShowStatus); + }, + ); + }); + + group('equality', () { + test( + 'GIVEN two show time models ' + 'WHEN properties are different ' + 'THEN equality returns false', + () { + //given + final model1 = ShowTimeModel( + showId: 1, + theaterId: 1, + showType: ShowType.i2D, + showStatus: ShowStatus.FULL, + startTime: DateTime(2012, 2, 27, 13, 27, 0), + endTime: DateTime(2012, 2, 27, 14, 27, 0), + ); + + //when + final model2 = ShowTimeModel( + showId: 1, + theaterId: 1, + showType: ShowType.i2D, + showStatus: ShowStatus.FREE, + startTime: DateTime(2012, 2, 27, 13, 27, 0), + endTime: DateTime(2012, 2, 27, 14, 27, 0), + ); + + //then + expect(model1 == model2, false); + }, + ); + + test( + 'GIVEN two show time models ' + 'WHEN properties are same ' + 'THEN equality returns true', + () { + //given + final model1 = ShowTimeModel( + showId: 1, + theaterId: 1, + showType: ShowType.i2D, + showStatus: ShowStatus.FULL, + startTime: DateTime(2012, 2, 27, 13, 27, 0), + endTime: DateTime(2012, 2, 27, 14, 27, 0), + ); + + //when + final model2 = ShowTimeModel( + showId: 1, + theaterId: 1, + showType: ShowType.i2D, + showStatus: ShowStatus.FULL, + startTime: DateTime(2012, 2, 27, 13, 27, 0), + endTime: DateTime(2012, 2, 27, 14, 27, 0), + ); + + //then + expect(model1 == model2, true); + }, + ); + }); +} diff --git a/test/models/theater_model_test.dart b/test/models/theater_model_test.dart new file mode 100644 index 0000000..07218f8 --- /dev/null +++ b/test/models/theater_model_test.dart @@ -0,0 +1,370 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:ez_ticketz_app/enums/theater_type_enum.dart'; +import 'package:ez_ticketz_app/models/seat_model.dart'; +import 'package:ez_ticketz_app/models/theater_model.dart'; + +void main() { + const _missingSeatsJson = [ + { + 'seat_row': 'A', + 'seat_number': 3, + }, + { + 'seat_row': 'B', + 'seat_number': 3, + }, + { + 'seat_row': 'C', + 'seat_number': 3, + }, + { + 'seat_row': 'D', + 'seat_number': 3, + }, + ]; + + const _blockedSeatsJson = [ + { + 'seat_row': 'E', + 'seat_number': 1, + }, + { + 'seat_row': 'E', + 'seat_number': 2, + }, + { + 'seat_row': 'E', + 'seat_number': 3, + }, + { + 'seat_row': 'E', + 'seat_number': 4, + }, + ]; + + const _missingSeatModels = [ + SeatModel( + seatRow: 'A', + seatNumber: 3, + ), + SeatModel( + seatRow: 'B', + seatNumber: 3, + ), + SeatModel( + seatRow: 'C', + seatNumber: 3, + ), + SeatModel( + seatRow: 'D', + seatNumber: 3, + ), + ]; + + const _blockedSeatModels = [ + SeatModel( + seatRow: 'E', + seatNumber: 1, + ), + SeatModel( + seatRow: 'E', + seatNumber: 2, + ), + SeatModel( + seatRow: 'E', + seatNumber: 3, + ), + SeatModel( + seatRow: 'E', + seatNumber: 4, + ), + ]; + + group('fromJson', () { + test( + 'GIVEN a valid theater json ' + 'WHEN json deserialization is performed ' + 'THEN a theater model is output', + () { + //given + const json = { + 'theater_id': 1, + 'theater_name': 'A', + 'num_of_rows': 10, + 'seats_per_row': 10, + 'theater_type': 'normal', + 'missing': _missingSeatsJson, + 'blocked': _blockedSeatsJson, + }; + + //when + final actual = TheaterModel.fromJson(json); + const matcher = TheaterModel( + theaterId: 1, + theaterName: 'A', + numOfRows: 10, + seatsPerRow: 10, + theaterType: TheaterType.NORMAL, + missing: _missingSeatModels, + blocked: _blockedSeatModels, + ); + + //then + expect(actual, matcher); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN a theater model ' + 'WHEN json serialization is performed ' + 'THEN a theater json is output', + () { + //given + const model = TheaterModel( + theaterId: 1, + theaterName: 'A', + numOfRows: 10, + seatsPerRow: 10, + theaterType: TheaterType.NORMAL, + missing: _missingSeatModels, + blocked: _blockedSeatModels, + ); + + //when + final actual = model.toJson(); + const matcher = { + 'theater_name': 'A', + 'num_of_rows': 10, + 'seats_per_row': 10, + 'theater_type': 'normal', + 'missing': _missingSeatsJson, + 'blocked': _blockedSeatsJson, + }; + + //then + expect(actual, matcher); + }, + ); + + test( + 'GIVEN a theater model ' + "AND it's theater id is null" + 'WHEN json serialization is performed ' + 'THEN a theater json is output ' + "AND it doesn't have a theater_id key", + () { + //given + const model = TheaterModel( + theaterId: null, + theaterName: 'A', + numOfRows: 10, + seatsPerRow: 10, + theaterType: TheaterType.NORMAL, + missing: _missingSeatModels, + blocked: _blockedSeatModels, + ); + + //when + final actual = model.toJson(); + + //then + expect(actual.containsKey('theater_id'), false); + }, + ); + + test( + 'GIVEN a theater model ' + "AND it's theater id is non-null" + 'WHEN json serialization is performed ' + 'THEN a theater json is output ' + "AND it doesn't have a theater_id key", + () { + //given + const model = TheaterModel( + theaterId: 1, + theaterName: 'A', + numOfRows: 10, + seatsPerRow: 10, + theaterType: TheaterType.NORMAL, + missing: _missingSeatModels, + blocked: _blockedSeatModels, + ); + + //when + final actual = model.toJson(); + + //then + expect(actual.containsKey('theater_id'), false); + }, + ); + }); + + group('toUpdateJson', () { + test( + 'GIVEN a theater model ' + 'WHEN json serialization is performed for updating' + 'AND all arguments are null ' + 'THEN an empty json is output', + () { + //given + const model = TheaterModel( + theaterId: null, + theaterName: 'A', + numOfRows: 10, + seatsPerRow: 10, + theaterType: TheaterType.NORMAL, + missing: _missingSeatModels, + blocked: _blockedSeatModels, + ); + + //when + final actual = model.toUpdateJson(); + + //then + expect(actual.isEmpty, true); + }, + ); + + test( + 'GIVEN a theater model ' + 'WHEN json serialization is performed for updating' + 'AND some arguments with new values are given ' + 'THEN a theater json is output ' + 'AND it has new values for the provided arguments', + () { + //given + const model = TheaterModel( + theaterId: null, + theaterName: 'A', + numOfRows: 10, + seatsPerRow: 10, + theaterType: TheaterType.NORMAL, + missing: _missingSeatModels, + blocked: _blockedSeatModels, + ); + + //when + const newTheaterType = TheaterType.ROYAL; + final actual = model.toUpdateJson( + theaterType: newTheaterType, + ); + final matcher = { + 'theater_name': 'A', + 'num_of_rows': 10, + 'seats_per_row': 10, + 'theater_type': 'royal', + 'missing': _missingSeatsJson, + 'blocked': _blockedSeatsJson, + }; + + //then + expect(actual, matcher); + }, + ); + }); + + group('equality', () { + test( + 'GIVEN two theater models ' + 'WHEN properties are different ' + 'THEN equality returns false', + () { + //given + const model1 = TheaterModel( + theaterId: null, + theaterName: 'A', + numOfRows: 10, + seatsPerRow: 10, + theaterType: TheaterType.NORMAL, + missing: _missingSeatModels, + blocked: _blockedSeatModels, + ); + + //when + const model2 = TheaterModel( + theaterId: 1, + theaterName: 'B', + numOfRows: 12, + seatsPerRow: 8, + theaterType: TheaterType.ROYAL, + missing: _missingSeatModels, + blocked: _blockedSeatModels, + ); + + //then + expect(model1 == model2, false); + }, + ); + + test( + 'GIVEN two theater models ' + 'WHEN properties are same ' + 'THEN equality returns true', + () { + //given + const model1 = TheaterModel( + theaterId: null, + theaterName: 'A', + numOfRows: 10, + seatsPerRow: 10, + theaterType: TheaterType.NORMAL, + missing: _missingSeatModels, + blocked: _blockedSeatModels, + ); + + //when + const missingSeatModels2 = [ + SeatModel( + seatRow: 'A', + seatNumber: 3, + ), + SeatModel( + seatRow: 'B', + seatNumber: 3, + ), + SeatModel( + seatRow: 'C', + seatNumber: 3, + ), + SeatModel( + seatRow: 'D', + seatNumber: 3, + ), + ]; + const blockedSeatModels2 = [ + SeatModel( + seatRow: 'E', + seatNumber: 1, + ), + SeatModel( + seatRow: 'E', + seatNumber: 2, + ), + SeatModel( + seatRow: 'E', + seatNumber: 3, + ), + SeatModel( + seatRow: 'E', + seatNumber: 4, + ), + ]; + const model2 = TheaterModel( + theaterId: null, + theaterName: 'A', + numOfRows: 10, + seatsPerRow: 10, + theaterType: TheaterType.NORMAL, + missing: missingSeatModels2, + blocked: blockedSeatModels2, + ); + + //then + expect(model1 == model2, true); + }, + ); + }); +} diff --git a/test/models/user_booking_model_test.dart b/test/models/user_booking_model_test.dart new file mode 100644 index 0000000..89949a6 --- /dev/null +++ b/test/models/user_booking_model_test.dart @@ -0,0 +1,263 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:ez_ticketz_app/enums/booking_status_enum.dart'; +import 'package:ez_ticketz_app/enums/show_type_enum.dart'; + +import 'package:ez_ticketz_app/models/booking_model.dart'; +import 'package:ez_ticketz_app/models/user_booking_model.dart'; +import 'package:ez_ticketz_app/models/user_booking_show_model.dart'; + +void main() { + late UserBookingShowModel _userBookingShowModel; + late BookingModel _bookingModel1, _bookingModel2; + + const _userBookingShowJson = { + 'show_id': 1, + 'show_type': '2D', + 'show_datetime': '2012-02-27T13:27:00.000', + }; + const _bookingsJson = [ + { + 'booking_id': 1, + 'user_id': 1, + 'show_id': 1, + 'seat_row': 'A', + 'seat_number': 10, + 'price': 700.2, + 'booking_datetime': '2012-02-27T13:27:00.000', + 'booking_status': 'confirmed', + }, + { + 'booking_id': 2, + 'user_id': 1, + 'show_id': 1, + 'seat_row': 'A', + 'seat_number': 11, + 'price': 700.2, + 'booking_datetime': '2012-02-27T13:30:00.000', + 'booking_status': 'confirmed', + }, + ]; + + /// Always initialize non final variables using setUp, so it resets variables + /// before all test and hence any mutations by the previous ones won't carry. + setUp((){ + _userBookingShowModel = UserBookingShowModel( + showId: 1, + showType: ShowType.i2D, + showDatetime: DateTime(2012, 2, 27, 13, 27, 0), + ); + _bookingModel1 = BookingModel( + bookingId: 1, + userId: 1, + showId: 1, + seatRow: 'A', + seatNumber: 10, + price: 700.2, + bookingDatetime: DateTime(2012, 2, 27, 13, 27, 0), + bookingStatus: BookingStatus.CONFIRMED, + ); + _bookingModel2 = BookingModel( + bookingId: 2, + userId: 1, + showId: 1, + seatRow: 'A', + seatNumber: 11, + price: 700.2, + bookingDatetime: DateTime(2012, 2, 27, 13, 30, 0), + bookingStatus: BookingStatus.CONFIRMED, + ); + }); + + group('fromJson', () { + test( + 'GIVEN a valid user booking json ' + 'WHEN a json deserialization is performed ' + 'THEN a user booking model is output', + () { + //given + const userBookingJson = { + 'title': 'Some Movie', + 'poster_url': 'www.placeholder.com/some-poster', + 'show': _userBookingShowJson, + 'bookings': _bookingsJson, + }; + + //when + final actual = UserBookingModel.fromJson(userBookingJson); + + final bookingModels = [ _bookingModel1, _bookingModel2 ]; + + final matcher = UserBookingModel( + title: 'Some Movie', + posterUrl: 'www.placeholder.com/some-poster', + show: _userBookingShowModel, + bookings: bookingModels, + ); + + //then + expect(actual, matcher); + }, + ); + }); + + group('toJson', () { + /// Override booking models with new ones containing `seat` key + setUp((){ + _bookingModel1 = BookingModel( + bookingId: 1, + userId: 1, + showId: 1, + seat: 'A-10', + price: 700.2, + bookingDatetime: DateTime(2012, 2, 27, 13, 27, 0), + bookingStatus: BookingStatus.CONFIRMED, + ); + _bookingModel2 = BookingModel( + bookingId: 2, + userId: 1, + showId: 1, + seat: 'A-11', + price: 700.2, + bookingDatetime: DateTime(2012, 2, 27, 13, 30, 0), + bookingStatus: BookingStatus.CONFIRMED, + ); + }); + + test( + 'GIVEN a user booking model ' + 'WHEN a json serialization is performed ' + 'THEN a user booking json is output', + () { + //given + final bookingModels = [ _bookingModel1, _bookingModel2 ]; + + final userBookingModel = UserBookingModel( + title: 'Some Movie', + posterUrl: 'www.placeholder.com/some-poster', + show: _userBookingShowModel, + bookings: bookingModels, + ); + + //when + final actual = userBookingModel.toJson(); + + const bookingsJson = [ + { + 'user_id': 1, + 'show_id': 1, + 'seat': 'A-10', + 'price': 700.2, + 'booking_datetime': '2012-02-27T13:27:00.000', + 'booking_status': 'confirmed', + }, + { + 'user_id': 1, + 'show_id': 1, + 'seat': 'A-11', + 'price': 700.2, + 'booking_datetime': '2012-02-27T13:30:00.000', + 'booking_status': 'confirmed', + }, + ]; + + const matcher = { + 'title': 'Some Movie', + 'poster_url': 'www.placeholder.com/some-poster', + 'show': _userBookingShowJson, + 'bookings': bookingsJson, + }; + + //then + expect(actual, matcher); + }, + ); + }); + + group('equality', () { + late List _bookingModels; + + setUp(() => _bookingModels = [ _bookingModel1, _bookingModel2 ]); + + test( + 'GIVEN two user booking models ' + 'WHEN properties are different ' + 'THEN equality returns false', + () { + //given + final userBookingModel1 = UserBookingModel( + title: 'Some Movie', + posterUrl: 'www.placeholder.com/some-poster', + show: _userBookingShowModel, + bookings: _bookingModels, + ); + + //when + final userBookingModel2 = UserBookingModel( + title: 'Some Different Movie', //different name + posterUrl: 'www.placeholder.com/some-poster', + show: _userBookingShowModel, + bookings: _bookingModels, + ); + + //then + expect(userBookingModel1 == userBookingModel2, false); + }, + ); + + test( + 'GIVEN two user booking models ' + 'WHEN properties are same ' + 'THEN equality returns true', + () { + //given + final userBookingModel1 = UserBookingModel( + title: 'Some Movie', + posterUrl: 'www.placeholder.com/some-poster', + show: _userBookingShowModel, + bookings: _bookingModels, + ); + + //when + final userBookingShowModel2 = UserBookingShowModel( + showId: 1, + showType: ShowType.i2D, + showDatetime: DateTime(2012, 2, 27, 13, 27, 0), + ); + + final bookingModels2 = [ + BookingModel( + bookingId: 1, + userId: 1, + showId: 1, + seatRow: 'A', + seatNumber: 10, + price: 700.2, + bookingDatetime: DateTime(2012, 2, 27, 13, 27, 0), + bookingStatus: BookingStatus.CONFIRMED, + ), + BookingModel( + bookingId: 2, + userId: 1, + showId: 1, + seatRow: 'A', + seatNumber: 11, + price: 700.2, + bookingDatetime: DateTime(2012, 2, 27, 13, 30, 0), + bookingStatus: BookingStatus.CONFIRMED, + ) + ]; + + final userBookingModel2 = UserBookingModel( + title: 'Some Movie', + posterUrl: 'www.placeholder.com/some-poster', + show: userBookingShowModel2, + bookings: bookingModels2, + ); + + //then + expect(userBookingModel1 == userBookingModel2, true); + }, + ); + }); +} diff --git a/test/models/user_booking_show_model_test.dart b/test/models/user_booking_show_model_test.dart new file mode 100644 index 0000000..69f7a72 --- /dev/null +++ b/test/models/user_booking_show_model_test.dart @@ -0,0 +1,110 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:ez_ticketz_app/enums/show_type_enum.dart'; +import 'package:ez_ticketz_app/models/user_booking_show_model.dart'; + +void main() { + group('fromJson', () { + test( + 'GIVEN a valid user booking show json ' + 'WHEN a json deserialization is performed ' + 'THEN a user booking show model is output', + () { + //given + final json = { + 'show_id': 1, + 'show_type': '2D', + 'show_datetime': '2012-02-27T13:27:00.000', + }; + + //when + final actual = UserBookingShowModel.fromJson(json); + final matcher = UserBookingShowModel( + showId: 1, + showType: ShowType.i2D, + showDatetime: DateTime(2012, 2, 27, 13, 27, 0), + ); + + //then + expect(actual, matcher); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN a user booking show model ' + 'WHEN a json serialization is performed ' + 'THEN a user booking show json is output', + () { + //given + final userBookingShowModel = UserBookingShowModel( + showId: 1, + showType: ShowType.i2D, + showDatetime: DateTime(2012, 2, 27, 13, 27, 0), + ); + + //when + final actual = userBookingShowModel.toJson(); + final matcher = { + 'show_id': 1, + 'show_type': '2D', + 'show_datetime': '2012-02-27T13:27:00.000', + }; + + //then + expect(actual, matcher); + }, + ); + }); + + group('equality', () { + test( + 'GIVEN two user booking show models ' + 'WHEN properties are different ' + 'THEN equality returns false', + () { + //given + final userBookingShow1 = UserBookingShowModel( + showId: 1, + showType: ShowType.i2D, + showDatetime: DateTime(2012, 2, 27, 13, 27, 0), + ); + + //when + final userBookingShow2 = UserBookingShowModel( + showId: 1, + showType: ShowType.i3D, + showDatetime: DateTime(2012, 2, 27, 13, 27, 0), + ); + + //then + expect(userBookingShow1 == userBookingShow2, false); + }, + ); + + test( + 'GIVEN two user booking show models ' + 'WHEN properties are same ' + 'THEN equality returns true', + () { + //given + final userBookingShow1 = UserBookingShowModel( + showId: 1, + showType: ShowType.i2D, + showDatetime: DateTime(2012, 2, 27, 13, 27, 0), + ); + + //when + final userBookingShow2 = UserBookingShowModel( + showId: 1, + showType: ShowType.i2D, + showDatetime: DateTime(2012, 2, 27, 13, 27, 0), + ); + + //then + expect(userBookingShow1 == userBookingShow2, true); + }, + ); + }); +} diff --git a/test/models/user_model_test.dart b/test/models/user_model_test.dart new file mode 100644 index 0000000..540f093 --- /dev/null +++ b/test/models/user_model_test.dart @@ -0,0 +1,165 @@ +import 'package:ez_ticketz_app/enums/user_role_enum.dart'; +import 'package:ez_ticketz_app/models/user_model.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('fromJson', () { + test( + 'GIVEN a valid user json ' + 'WHEN a json deserialization is performed' + 'THEN a user model is output', + () { + //given + final json = { + 'user_id': 1, + 'full_name': 'Test User', + 'email': 'test.email@gmail.com', + 'address': 'ABC-1/BLOCK TEST', + 'contact': '03001234567', + 'role': 'api_user', + }; + + //when + final actual = UserModel.fromJson(json); + const matcher = UserModel( + userId: 1, + fullName: 'Test User', + email: 'test.email@gmail.com', + address: 'ABC-1/BLOCK TEST', + contact: '03001234567', + role: UserRole.API_USER, + ); + + //then + expect(actual, matcher); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN a user model with user id ' + 'WHEN a json serialization is performed ' + 'THEN a user json with user id is output', + () { + //given + const user = UserModel( + userId: 1, + fullName: 'Test User', + email: 'test.email@gmail.com', + address: 'ABC-1/BLOCK TEST', + contact: '03001234567', + role: UserRole.API_USER, + ); + + //when + final actual = user.toJson(); + final matcher = { + 'user_id': 1, + 'full_name': 'Test User', + 'email': 'test.email@gmail.com', + 'address': 'ABC-1/BLOCK TEST', + 'contact': '03001234567', + 'role': 'api_user', + }; + + //then + expect(actual, matcher); + }, + ); + + test( + 'GIVEN a user model with user id ' + "AND it's user id is null " + 'WHEN a json serialization is performed ' + 'THEN a user json is output ' + "AND it doesn't contain a user_id key", + () { + //given + const user = UserModel( + userId: null, + fullName: 'Test User', + email: 'test.email@gmail.com', + address: 'ABC-1/BLOCK TEST', + contact: '03001234567', + role: UserRole.API_USER, + ); + + //when + final actual = user.toJson(); + final matcher = { + 'full_name': 'Test User', + 'email': 'test.email@gmail.com', + 'address': 'ABC-1/BLOCK TEST', + 'contact': '03001234567', + 'role': 'api_user', + }; + + //then + expect(actual, matcher); + }, + ); + }); + + group('equality', () { + test( + 'GIVEN two user models ' + 'WHEN properties are different ' + 'THEN equality returns false', + () { + //given + const user1 = UserModel( + userId: 1, + fullName: 'Test User', + email: 'test.email@gmail.com', + address: 'ABC-1/BLOCK TEST', + contact: '03001234567', + role: UserRole.API_USER, + ); + + //when + const user2 = UserModel( + userId: 2, + fullName: 'Test User 2', + email: 'test.email@gmail.com', + address: 'ABC-1/BLOCK TEST', + contact: '03001234567', + role: UserRole.API_USER, + ); + + //then + expect(user1 == user2, false); + }, + ); + + test( + 'GIVEN two user models ' + 'WHEN properties are same ' + 'THEN equality returns true', + () { + //given + const user1 = UserModel( + userId: 1, + fullName: 'Test User', + email: 'test.email@gmail.com', + address: 'ABC-1/BLOCK TEST', + contact: '03001234567', + role: UserRole.API_USER, + ); + + //when + const user2 = UserModel( + userId: 1, + fullName: 'Test User', + email: 'test.email@gmail.com', + address: 'ABC-1/BLOCK TEST', + contact: '03001234567', + role: UserRole.API_USER, + ); + + //then + expect(user1 == user2, true); + }, + ); + }); +} diff --git a/test/models/user_payment_model_test.dart b/test/models/user_payment_model_test.dart new file mode 100644 index 0000000..82bd3b5 --- /dev/null +++ b/test/models/user_payment_model_test.dart @@ -0,0 +1,244 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:ez_ticketz_app/enums/payment_method_enum.dart'; +import 'package:ez_ticketz_app/models/user_payment_model.dart'; + +void main() { + group('UserPaymentMovieModel', () { + group('fromJson', () { + test( + 'GIVEN a valid user payment movie json ' + 'WHEN a json deserialization is performed ' + 'THEN a user payment movie model is output', + () { + //given + final json = { + 'title': 'Some Movie', + 'poster_url': 'www.placeholder.com/some-poster', + }; + + //when + final actual = UserPaymentMovieModel.fromJson(json); + const matcher = UserPaymentMovieModel( + title: 'Some Movie', + posterUrl: 'www.placeholder.com/some-poster', + ); + + //then + expect(actual, matcher); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN a user payment movie model ' + 'WHEN a json serialization is performed ' + 'THEN a user payment movie json is output', + () { + //given + const userPaymentMovie = UserPaymentMovieModel( + title: 'Some Movie', + posterUrl: 'www.placeholder.com/some-poster', + ); + + //when + final actual = userPaymentMovie.toJson(); + final matcher = { + 'title': 'Some Movie', + 'poster_url': 'www.placeholder.com/some-poster', + }; + + //then + expect(actual, matcher); + }, + ); + }); + + group('equality', () { + test( + 'GIVEN two user payment movie models ' + 'WHEN properties are different ' + 'THEN equality returns false', + () { + //given + const userPaymentMovie1 = UserPaymentMovieModel( + title: 'Some Movie', + posterUrl: 'www.placeholder.com/some-poster', + ); + + //when + const userPaymentMovie2 = UserPaymentMovieModel( + title: 'Some Different Movie', + posterUrl: 'www.placeholder.com/some-poster', + ); + + //then + expect(userPaymentMovie1 == userPaymentMovie2, false); + }, + ); + + test( + 'GIVEN two user payment movie models ' + 'WHEN properties are same ' + 'THEN equality returns true', + () { + //given + const userPaymentMovie1 = UserPaymentMovieModel( + title: 'Some Movie', + posterUrl: 'www.placeholder.com/some-poster', + ); + + //when + const userPaymentMovie2 = UserPaymentMovieModel( + title: 'Some Movie', + posterUrl: 'www.placeholder.com/some-poster', + ); + + //then + expect(userPaymentMovie1 == userPaymentMovie2, true); + }, + ); + }); + }); + + group('UserPaymentModel', () { + late UserPaymentMovieModel _userPaymentMovieModel; + + const _paymentMovieJson = { + 'title': 'Some Movie', + 'poster_url': 'www.placeholder.com/some-poster', + }; + + setUp((){ + _userPaymentMovieModel = const UserPaymentMovieModel( + title: 'Some Movie', + posterUrl: 'www.placeholder.com/some-poster', + ); + }); + + group('fromJson', () { + test( + 'GIVEN a json serialization is needed ' + 'WHEN a valid user payment json is input ' + 'THEN a user payment model is output', + () { + //given + const userPaymentJson = { + 'payment_id': 1, + 'amount': 1500.9, + 'payment_datetime': '2012-02-27T13:27:00.000', + 'payment_method': 'cash', + 'movie': _paymentMovieJson, + }; + + //when + final actual = UserPaymentModel.fromJson(userPaymentJson); + final matcher = UserPaymentModel( + paymentId: 1, + amount: 1500.9, + paymentDatetime: DateTime(2012, 2, 27, 13, 27, 0), + paymentMethod: PaymentMethod.CASH, + movie: _userPaymentMovieModel, + ); + + //then + expect(actual, matcher); + }, + ); + }); + + group('toJson', () { + test( + 'GIVEN a json deserialization is needed ' + 'WHEN a user payment model is converted ' + 'THEN a user payment json is output', + () { + //given + final userPaymentModel = UserPaymentModel( + paymentId: 1, + amount: 1500.9, + paymentDatetime: DateTime(2012, 2, 27, 13, 27, 0), + paymentMethod: PaymentMethod.CASH, + movie: _userPaymentMovieModel, + ); + + //when + final actual = userPaymentModel.toJson(); + const matcher = { + 'payment_id': 1, + 'amount': 1500.9, + 'payment_datetime': '2012-02-27T13:27:00.000', + 'payment_method': 'cash', + 'movie': _paymentMovieJson, + }; + + //then + expect(actual, matcher); + }, + ); + }); + + group('equality', () { + test( + 'GIVEN two user payment movie models ' + 'WHEN properties are different ' + 'THEN equality returns false', + () { + //given + final userPaymentModel1 = UserPaymentModel( + paymentId: 1, + amount: 1500.9, + paymentDatetime: DateTime(2012, 2, 27, 13, 27, 0), + paymentMethod: PaymentMethod.CASH, + movie: _userPaymentMovieModel, + ); + + //when + final userPaymentModel2 = UserPaymentModel( + paymentId: 1, + amount: 1500.9, + paymentDatetime: DateTime(2012, 2, 27, 13, 27, 0), + paymentMethod: PaymentMethod.COD, //different + movie: _userPaymentMovieModel, + ); + + //then + expect(userPaymentModel1 == userPaymentModel2, false); + }, + ); + + test( + 'GIVEN two user payment movie models ' + 'WHEN properties are same ' + 'THEN equality returns true', + () { + //given + final userPaymentModel1 = UserPaymentModel( + paymentId: 1, + amount: 1500.9, + paymentDatetime: DateTime(2012, 2, 27, 13, 27, 0), + paymentMethod: PaymentMethod.CASH, + movie: _userPaymentMovieModel, + ); + + //when + const paymentMovieModel2 = UserPaymentMovieModel( + title: 'Some Movie', + posterUrl: 'www.placeholder.com/some-poster', + ); + final userPaymentModel2 = UserPaymentModel( + paymentId: 1, + amount: 1500.9, + paymentDatetime: DateTime(2012, 2, 27, 13, 27, 0), + paymentMethod: PaymentMethod.CASH, + movie: paymentMovieModel2, + ); + + //then + expect(userPaymentModel1 == userPaymentModel2, true); + }, + ); + }); + }); +}