From 6f2c947687a4b67122335e0e2d55f39652ba8ff6 Mon Sep 17 00:00:00 2001 From: Zohaib Iqbal Kambrani <> Date: Tue, 16 Mar 2021 12:06:54 +0300 Subject: [PATCH] Packages And offers.. --- assets/images/svg/robort_svg.svg | 1 + assets/images/svg/success.svg | 67 +++ lib/config/config.dart | 3 +- lib/core/enum/PaymentOptions.dart | 34 ++ lib/core/model/ResponseModel.dart | 7 + .../PackagesCartItemsResponseModel.dart | 6 +- .../PackagesCustomerResponseModel.dart | 218 +++++++- .../responses/order_response_model.dart | 323 ++++++++++++ .../PackagesOffersServices.dart | 71 ++- .../PackagesOffersViewModel.dart | 15 +- lib/main.dart | 7 +- lib/pages/landing/home_page.dart | 29 ++ .../ClinicOfferAndPackagesPage.dart | 36 +- .../CreateCustomerDailogPage.dart | 206 ++++++++ .../OfferAndPackagesCartPage.dart | 40 +- .../packages_offers/OfferAndPackagesPage.dart | 201 ++++---- .../PackageOrderCompletedPage.dart | 146 ++++++ lib/routes.dart | 3 + lib/widgets/AnimatedTextFields.dart | 347 +++++++++++++ lib/widgets/LoadingButton.dart | 465 ++++++++++++++++++ lib/widgets/TextFieldInertiaDirection.java | 343 +++++++++++++ lib/widgets/in_app_browser/InAppBrowser.dart | 53 +- .../offers_packages/PackagesCartItemCard.dart | 2 +- .../offers_packages/PackagesOfferCard.dart | 4 +- lib/widgets/others/app_scaffold_widget.dart | 91 ++-- 25 files changed, 2522 insertions(+), 196 deletions(-) create mode 100644 assets/images/svg/success.svg create mode 100644 lib/core/enum/PaymentOptions.dart create mode 100644 lib/core/model/ResponseModel.dart create mode 100644 lib/core/model/packages_offers/responses/order_response_model.dart create mode 100644 lib/pages/packages_offers/CreateCustomerDailogPage.dart create mode 100644 lib/pages/packages_offers/PackageOrderCompletedPage.dart create mode 100644 lib/widgets/AnimatedTextFields.dart create mode 100644 lib/widgets/LoadingButton.dart create mode 100644 lib/widgets/TextFieldInertiaDirection.java diff --git a/assets/images/svg/robort_svg.svg b/assets/images/svg/robort_svg.svg index 07d3d4b4..8b4875a1 100644 --- a/assets/images/svg/robort_svg.svg +++ b/assets/images/svg/robort_svg.svg @@ -1,3 +1,4 @@ + diff --git a/assets/images/svg/success.svg b/assets/images/svg/success.svg new file mode 100644 index 00000000..343589bd --- /dev/null +++ b/assets/images/svg/success.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/config/config.dart b/lib/config/config.dart index a167e1b5..3c7e1e9a 100644 --- a/lib/config/config.dart +++ b/lib/config/config.dart @@ -11,8 +11,9 @@ const MAX_SMALL_SCREEN = 660; const EXA_CART_API_BASE_URL = 'http://10.200.101.75:9000'; const PACKAGES_CATEGORIES = '/api/categories'; const PACKAGES_PRODUCTS = '/api/products'; -const PACKAGES_SHOPPING_CART = '/api/shopping_cart_items'; const PACKAGES_CUSTOMER = '/api/customers'; +const PACKAGES_SHOPPING_CART = '/api/shopping_cart_items'; +const PACKAGES_ORDERS = '/api/orders'; const BASE_URL = 'https://uat.hmgwebservices.com/'; // const BASE_URL = 'https://hmgwebservices.com/'; diff --git a/lib/core/enum/PaymentOptions.dart b/lib/core/enum/PaymentOptions.dart new file mode 100644 index 00000000..bbf43cb2 --- /dev/null +++ b/lib/core/enum/PaymentOptions.dart @@ -0,0 +1,34 @@ +enum PaymentOptions { + VISA, + MASTERCARD, + MADA, + INSTALLMENT, + APPLEPAY +} + +extension PaymentOptions_ on PaymentOptions{ + String value(){ + switch(this){ + case PaymentOptions.VISA: + return "VISA"; + break; + + case PaymentOptions.MASTERCARD: + return "MASTERCARD"; + break; + + case PaymentOptions.MADA: + return "MADA"; + break; + + case PaymentOptions.INSTALLMENT: + return "INSTALLMENT"; + break; + case PaymentOptions.APPLEPAY: + return "APPLEPAY"; + break; + } + + return null; + } +} diff --git a/lib/core/model/ResponseModel.dart b/lib/core/model/ResponseModel.dart new file mode 100644 index 00000000..f34abf7d --- /dev/null +++ b/lib/core/model/ResponseModel.dart @@ -0,0 +1,7 @@ +class ResponseModel{ + final bool status; + final String error; + final T data; + + ResponseModel({this.status, this.data, this.error}); +} \ No newline at end of file diff --git a/lib/core/model/packages_offers/responses/PackagesCartItemsResponseModel.dart b/lib/core/model/packages_offers/responses/PackagesCartItemsResponseModel.dart index 9514976f..99c8a8b4 100644 --- a/lib/core/model/packages_offers/responses/PackagesCartItemsResponseModel.dart +++ b/lib/core/model/packages_offers/responses/PackagesCartItemsResponseModel.dart @@ -1,7 +1,7 @@ import 'package:diplomaticquarterapp/core/model/packages_offers/responses/PackagesResponseModel.dart'; -class CartProductResponseModel { +class PackagesCartItemsResponseModel { int _quantity; set quantity(int value) { @@ -19,7 +19,7 @@ class CartProductResponseModel { PackagesResponseModel get product => _product; int get id => _id; - CartProductResponseModel({ + PackagesCartItemsResponseModel({ int quantity, String shoppingCartType, int productId, @@ -32,7 +32,7 @@ class CartProductResponseModel { _id = id; } - CartProductResponseModel.fromJson(dynamic json) { + PackagesCartItemsResponseModel.fromJson(dynamic json) { _quantity = json["quantity"]; _shoppingCartType = json["shopping_cart_type"]; _productId = json["product_id"]; diff --git a/lib/core/model/packages_offers/responses/PackagesCustomerResponseModel.dart b/lib/core/model/packages_offers/responses/PackagesCustomerResponseModel.dart index 314b0ad3..245973dd 100644 --- a/lib/core/model/packages_offers/responses/PackagesCustomerResponseModel.dart +++ b/lib/core/model/packages_offers/responses/PackagesCustomerResponseModel.dart @@ -1,8 +1,38 @@ +import 'PackagesCartItemsResponseModel.dart'; + +/// shopping_cart_items : [] +/// billing_address : null +/// shipping_address : null +/// addresses : [{"first_name":null,"last_name":null,"email":"a2zzuhaib@gmil.com","company":null,"country_id":null,"country":null,"state_province_id":null,"city":null,"address1":null,"address2":null,"zip_postal_code":null,"phone_number":"0500409598","fax_number":null,"customer_attributes":null,"created_on_utc":"2021-03-11T09:40:23.8091261Z","province":null,"id":0}] +/// customer_guid : "1367e5c7-be3b-43cc-ad81-ff1fc8d3b130" +/// username : null +/// email : "a2zzuhaib@gmil.com" +/// first_name : null +/// last_name : null +/// language_id : null +/// date_of_birth : null +/// gender : null +/// admin_comment : null +/// is_tax_exempt : false +/// has_shopping_cart_items : false +/// active : true +/// deleted : false +/// is_system_account : false +/// system_name : null +/// last_ip_address : null +/// created_on_utc : "2021-03-11T09:40:23.7535859Z" +/// last_login_date_utc : null +/// last_activity_date_utc : "2021-03-11T09:40:23.7535892Z" +/// registered_in_store_id : 0 +/// subscribed_to_newsletter : false +/// role_ids : [] +/// id : 76823 + class PackagesCustomerResponseModel { - List _shoppingCartItems; + List _shoppingCartItems; dynamic _billingAddress; dynamic _shippingAddress; - List _addresses; + List _addresses; String _customerGuid; dynamic _username; String _email; @@ -24,13 +54,13 @@ class PackagesCustomerResponseModel { String _lastActivityDateUtc; int _registeredInStoreId; bool _subscribedToNewsletter; - List _roleIds; + List _roleIds; int _id; List get shoppingCartItems => _shoppingCartItems; dynamic get billingAddress => _billingAddress; dynamic get shippingAddress => _shippingAddress; - List get addresses => _addresses; + List get addresses => _addresses; String get customerGuid => _customerGuid; dynamic get username => _username; String get email => _email; @@ -59,7 +89,7 @@ class PackagesCustomerResponseModel { List shoppingCartItems, dynamic billingAddress, dynamic shippingAddress, - List addresses, + List addresses, String customerGuid, dynamic username, String email, @@ -115,6 +145,12 @@ class PackagesCustomerResponseModel { PackagesCustomerResponseModel.fromJson(dynamic json) { _billingAddress = json["billing_address"]; _shippingAddress = json["shipping_address"]; + if (json["addresses"] != null) { + _addresses = []; + json["addresses"].forEach((v) { + _addresses.add(Addresses.fromJson(v)); + }); + } _customerGuid = json["customer_guid"]; _username = json["username"]; _email = json["email"]; @@ -136,26 +172,22 @@ class PackagesCustomerResponseModel { _lastActivityDateUtc = json["last_activity_date_utc"]; _registeredInStoreId = json["registered_in_store_id"]; _subscribedToNewsletter = json["subscribed_to_newsletter"]; - _id = json["id"]; - // if (json["role_ids"] != null) { - // _roleIds = []; - // json["role_ids"].forEach((v) { - // _roleIds.add(dynamic.fromJson(v)); - // }); - // } - // if (json["addresses"] != null) { - // _addresses = []; - // json["addresses"].forEach((v) { - // _addresses.add(dynamic.fromJson(v)); - // }); - // } - // if (json["shopping_cart_items"] != null) { - // _shoppingCartItems = []; - // json["shopping_cart_items"].forEach((v) { - // _shoppingCartItems.add(dynamic.fromJson(v)); - // }); - // } + if (json["role_ids"] != null) { + _roleIds = []; + json["role_ids"].forEach((v) { + _roleIds.add(v); + }); + } + + if (json["shopping_cart_items"] != null) { + _shoppingCartItems = []; + json["shopping_cart_items"].forEach((v) { + _shoppingCartItems.add(PackagesCartItemsResponseModel.fromJson(v)); + }); + } + + _id = json["id"]; } Map toJson() { @@ -190,10 +222,146 @@ class PackagesCustomerResponseModel { map["registered_in_store_id"] = _registeredInStoreId; map["subscribed_to_newsletter"] = _subscribedToNewsletter; if (_roleIds != null) { - map["role_ids"] = _roleIds.map((v) => v.toJson()).toList(); + map["role_ids"] = _roleIds.map((v) => v).toList(); } map["id"] = _id; return map; } +} + +/// first_name : null +/// last_name : null +/// email : "a2zzuhaib@gmil.com" +/// company : null +/// country_id : null +/// country : null +/// state_province_id : null +/// city : null +/// address1 : null +/// address2 : null +/// zip_postal_code : null +/// phone_number : "0500409598" +/// fax_number : null +/// customer_attributes : null +/// created_on_utc : "2021-03-11T09:40:23.8091261Z" +/// province : null +/// id : 0 + +class Addresses { + dynamic _firstName; + dynamic _lastName; + String _email; + dynamic _company; + dynamic _countryId; + dynamic _country; + dynamic _stateProvinceId; + dynamic _city; + dynamic _address1; + dynamic _address2; + dynamic _zipPostalCode; + String _phoneNumber; + dynamic _faxNumber; + dynamic _customerAttributes; + String _createdOnUtc; + dynamic _province; + int _id; + + dynamic get firstName => _firstName; + dynamic get lastName => _lastName; + String get email => _email; + dynamic get company => _company; + dynamic get countryId => _countryId; + dynamic get country => _country; + dynamic get stateProvinceId => _stateProvinceId; + dynamic get city => _city; + dynamic get address1 => _address1; + dynamic get address2 => _address2; + dynamic get zipPostalCode => _zipPostalCode; + String get phoneNumber => _phoneNumber; + dynamic get faxNumber => _faxNumber; + dynamic get customerAttributes => _customerAttributes; + String get createdOnUtc => _createdOnUtc; + dynamic get province => _province; + int get id => _id; + + Addresses({ + dynamic firstName, + dynamic lastName, + String email, + dynamic company, + dynamic countryId, + dynamic country, + dynamic stateProvinceId, + dynamic city, + dynamic address1, + dynamic address2, + dynamic zipPostalCode, + String phoneNumber, + dynamic faxNumber, + dynamic customerAttributes, + String createdOnUtc, + dynamic province, + int id}){ + _firstName = firstName; + _lastName = lastName; + _email = email; + _company = company; + _countryId = countryId; + _country = country; + _stateProvinceId = stateProvinceId; + _city = city; + _address1 = address1; + _address2 = address2; + _zipPostalCode = zipPostalCode; + _phoneNumber = phoneNumber; + _faxNumber = faxNumber; + _customerAttributes = customerAttributes; + _createdOnUtc = createdOnUtc; + _province = province; + _id = id; +} + + Addresses.fromJson(dynamic json) { + _firstName = json["first_name"]; + _lastName = json["last_name"]; + _email = json["email"]; + _company = json["company"]; + _countryId = json["country_id"]; + _country = json["country"]; + _stateProvinceId = json["state_province_id"]; + _city = json["city"]; + _address1 = json["address1"]; + _address2 = json["address2"]; + _zipPostalCode = json["zip_postal_code"]; + _phoneNumber = json["phone_number"]; + _faxNumber = json["fax_number"]; + _customerAttributes = json["customer_attributes"]; + _createdOnUtc = json["created_on_utc"]; + _province = json["province"]; + _id = json["id"]; + } + + Map toJson() { + var map = {}; + map["first_name"] = _firstName; + map["last_name"] = _lastName; + map["email"] = _email; + map["company"] = _company; + map["country_id"] = _countryId; + map["country"] = _country; + map["state_province_id"] = _stateProvinceId; + map["city"] = _city; + map["address1"] = _address1; + map["address2"] = _address2; + map["zip_postal_code"] = _zipPostalCode; + map["phone_number"] = _phoneNumber; + map["fax_number"] = _faxNumber; + map["customer_attributes"] = _customerAttributes; + map["created_on_utc"] = _createdOnUtc; + map["province"] = _province; + map["id"] = _id; + return map; + } + } \ No newline at end of file diff --git a/lib/core/model/packages_offers/responses/order_response_model.dart b/lib/core/model/packages_offers/responses/order_response_model.dart new file mode 100644 index 00000000..ed53a765 --- /dev/null +++ b/lib/core/model/packages_offers/responses/order_response_model.dart @@ -0,0 +1,323 @@ + + +import 'package:diplomaticquarterapp/core/model/packages_offers/responses/PackagesCustomerResponseModel.dart'; +import 'PackagesCartItemsResponseModel.dart'; + +class PackagesOrderResponseModel { + String _customOrderNumber; + int _storeId; + dynamic _pickUpInStore; + String _paymentMethodSystemName; + String _customerCurrencyCode; + double _currencyRate; + int _customerTaxDisplayTypeId; + dynamic _vatNumber; + double _orderSubtotalInclTax; + double _orderSubtotalExclTax; + double _orderSubTotalDiscountInclTax; + double _orderSubTotalDiscountExclTax; + double _orderShippingInclTax; + double _orderShippingExclTax; + double _paymentMethodAdditionalFeeInclTax; + double _paymentMethodAdditionalFeeExclTax; + String _taxRates; + double _orderTax; + double _orderDiscount; + double _orderTotal; + double _refundedAmount; + dynamic _rewardPointsWereAdded; + String _checkoutAttributeDescription; + int _customerLanguageId; + int _affiliateId; + String _customerIp; + dynamic _authorizationTransactionId; + dynamic _authorizationTransactionCode; + dynamic _authorizationTransactionResult; + dynamic _captureTransactionId; + dynamic _captureTransactionResult; + dynamic _subscriptionTransactionId; + dynamic _paidDateUtc; + dynamic _shippingMethod; + dynamic _shippingRateComputationMethodSystemName; + String _customValuesXml; + dynamic _paymentOption; + bool _deleted; + String _createdOnUtc; + PackagesCustomerResponseModel _customer; + int _customerId; + dynamic _billingAddress; + dynamic _shippingAddress; + List _orderItems; + String _orderStatus; + String _paymentStatus; + String _shippingStatus; + String _customerTaxDisplayType; + int _id; + + String get customOrderNumber => _customOrderNumber; + int get storeId => _storeId; + dynamic get pickUpInStore => _pickUpInStore; + String get paymentMethodSystemName => _paymentMethodSystemName; + String get customerCurrencyCode => _customerCurrencyCode; + double get currencyRate => _currencyRate; + int get customerTaxDisplayTypeId => _customerTaxDisplayTypeId; + dynamic get vatNumber => _vatNumber; + double get orderSubtotalInclTax => _orderSubtotalInclTax; + double get orderSubtotalExclTax => _orderSubtotalExclTax; + double get orderSubTotalDiscountInclTax => _orderSubTotalDiscountInclTax; + double get orderSubTotalDiscountExclTax => _orderSubTotalDiscountExclTax; + double get orderShippingInclTax => _orderShippingInclTax; + double get orderShippingExclTax => _orderShippingExclTax; + double get paymentMethodAdditionalFeeInclTax => _paymentMethodAdditionalFeeInclTax; + double get paymentMethodAdditionalFeeExclTax => _paymentMethodAdditionalFeeExclTax; + String get taxRates => _taxRates; + double get orderTax => _orderTax; + double get orderDiscount => _orderDiscount; + double get orderTotal => _orderTotal; + double get refundedAmount => _refundedAmount; + dynamic get rewardPointsWereAdded => _rewardPointsWereAdded; + String get checkoutAttributeDescription => _checkoutAttributeDescription; + int get customerLanguageId => _customerLanguageId; + int get affiliateId => _affiliateId; + String get customerIp => _customerIp; + dynamic get authorizationTransactionId => _authorizationTransactionId; + dynamic get authorizationTransactionCode => _authorizationTransactionCode; + dynamic get authorizationTransactionResult => _authorizationTransactionResult; + dynamic get captureTransactionId => _captureTransactionId; + dynamic get captureTransactionResult => _captureTransactionResult; + dynamic get subscriptionTransactionId => _subscriptionTransactionId; + dynamic get paidDateUtc => _paidDateUtc; + dynamic get shippingMethod => _shippingMethod; + dynamic get shippingRateComputationMethodSystemName => _shippingRateComputationMethodSystemName; + String get customValuesXml => _customValuesXml; + dynamic get paymentOption => _paymentOption; + bool get deleted => _deleted; + String get createdOnUtc => _createdOnUtc; + PackagesCustomerResponseModel get customer => _customer; + int get customerId => _customerId; + dynamic get billingAddress => _billingAddress; + dynamic get shippingAddress => _shippingAddress; + List get orderItems => _orderItems; + String get orderStatus => _orderStatus; + String get paymentStatus => _paymentStatus; + String get shippingStatus => _shippingStatus; + String get customerTaxDisplayType => _customerTaxDisplayType; + int get id => _id; + + OrderResponseModel({ + String customOrderNumber, + int storeId, + dynamic pickUpInStore, + String paymentMethodSystemName, + String customerCurrencyCode, + double currencyRate, + int customerTaxDisplayTypeId, + dynamic vatNumber, + double orderSubtotalInclTax, + double orderSubtotalExclTax, + double orderSubTotalDiscountInclTax, + double orderSubTotalDiscountExclTax, + double orderShippingInclTax, + double orderShippingExclTax, + double paymentMethodAdditionalFeeInclTax, + double paymentMethodAdditionalFeeExclTax, + String taxRates, + double orderTax, + double orderDiscount, + double orderTotal, + double refundedAmount, + dynamic rewardPointsWereAdded, + String checkoutAttributeDescription, + int customerLanguageId, + int affiliateId, + String customerIp, + dynamic authorizationTransactionId, + dynamic authorizationTransactionCode, + dynamic authorizationTransactionResult, + dynamic captureTransactionId, + dynamic captureTransactionResult, + dynamic subscriptionTransactionId, + dynamic paidDateUtc, + dynamic shippingMethod, + dynamic shippingRateComputationMethodSystemName, + String customValuesXml, + dynamic paymentOption, + bool deleted, + String createdOnUtc, + PackagesCustomerResponseModel customer, + int customerId, + dynamic billingAddress, + dynamic shippingAddress, + List orderItems, + String orderStatus, + String paymentStatus, + String shippingStatus, + String customerTaxDisplayType, + int id}){ + _customOrderNumber = customOrderNumber; + _storeId = storeId; + _pickUpInStore = pickUpInStore; + _paymentMethodSystemName = paymentMethodSystemName; + _customerCurrencyCode = customerCurrencyCode; + _currencyRate = currencyRate; + _customerTaxDisplayTypeId = customerTaxDisplayTypeId; + _vatNumber = vatNumber; + _orderSubtotalInclTax = orderSubtotalInclTax; + _orderSubtotalExclTax = orderSubtotalExclTax; + _orderSubTotalDiscountInclTax = orderSubTotalDiscountInclTax; + _orderSubTotalDiscountExclTax = orderSubTotalDiscountExclTax; + _orderShippingInclTax = orderShippingInclTax; + _orderShippingExclTax = orderShippingExclTax; + _paymentMethodAdditionalFeeInclTax = paymentMethodAdditionalFeeInclTax; + _paymentMethodAdditionalFeeExclTax = paymentMethodAdditionalFeeExclTax; + _taxRates = taxRates; + _orderTax = orderTax; + _orderDiscount = orderDiscount; + _orderTotal = orderTotal; + _refundedAmount = refundedAmount; + _rewardPointsWereAdded = rewardPointsWereAdded; + _checkoutAttributeDescription = checkoutAttributeDescription; + _customerLanguageId = customerLanguageId; + _affiliateId = affiliateId; + _customerIp = customerIp; + _authorizationTransactionId = authorizationTransactionId; + _authorizationTransactionCode = authorizationTransactionCode; + _authorizationTransactionResult = authorizationTransactionResult; + _captureTransactionId = captureTransactionId; + _captureTransactionResult = captureTransactionResult; + _subscriptionTransactionId = subscriptionTransactionId; + _paidDateUtc = paidDateUtc; + _shippingMethod = shippingMethod; + _shippingRateComputationMethodSystemName = shippingRateComputationMethodSystemName; + _customValuesXml = customValuesXml; + _paymentOption = paymentOption; + _deleted = deleted; + _createdOnUtc = createdOnUtc; + _customer = customer; + _customerId = customerId; + _billingAddress = billingAddress; + _shippingAddress = shippingAddress; + _orderItems = orderItems; + _orderStatus = orderStatus; + _paymentStatus = paymentStatus; + _shippingStatus = shippingStatus; + _customerTaxDisplayType = customerTaxDisplayType; + _id = id; +} + + PackagesOrderResponseModel.fromJson(dynamic json) { + _customOrderNumber = json["custom_order_number"]; + _storeId = json["store_id"]; + _pickUpInStore = json["pick_up_in_store"]; + _paymentMethodSystemName = json["payment_method_system_name"]; + _customerCurrencyCode = json["customer_currency_code"]; + _currencyRate = json["currency_rate"]; + _customerTaxDisplayTypeId = json["customer_tax_display_type_id"]; + _vatNumber = json["vat_number"]; + _orderSubtotalInclTax = json["order_subtotal_incl_tax"]; + _orderSubtotalExclTax = json["order_subtotal_excl_tax"]; + _orderSubTotalDiscountInclTax = json["order_sub_total_discount_incl_tax"]; + _orderSubTotalDiscountExclTax = json["order_sub_total_discount_excl_tax"]; + _orderShippingInclTax = json["order_shipping_incl_tax"]; + _orderShippingExclTax = json["order_shipping_excl_tax"]; + _paymentMethodAdditionalFeeInclTax = json["payment_method_additional_fee_incl_tax"]; + _paymentMethodAdditionalFeeExclTax = json["payment_method_additional_fee_excl_tax"]; + _taxRates = json["tax_rates"]; + _orderTax = json["order_tax"]; + _orderDiscount = json["order_discount"]; + _orderTotal = json["order_total"]; + _refundedAmount = json["refunded_amount"]; + _rewardPointsWereAdded = json["reward_points_were_added"]; + _checkoutAttributeDescription = json["checkout_attribute_description"]; + _customerLanguageId = json["customer_language_id"]; + _affiliateId = json["affiliate_id"]; + _customerIp = json["customer_ip"]; + _authorizationTransactionId = json["authorization_transaction_id"]; + _authorizationTransactionCode = json["authorization_transaction_code"]; + _authorizationTransactionResult = json["authorization_transaction_result"]; + _captureTransactionId = json["capture_transaction_id"]; + _captureTransactionResult = json["capture_transaction_result"]; + _subscriptionTransactionId = json["subscription_transaction_id"]; + _paidDateUtc = json["paid_date_utc"]; + _shippingMethod = json["shipping_method"]; + _shippingRateComputationMethodSystemName = json["shipping_rate_computation_method_system_name"]; + _customValuesXml = json["custom_values_xml"]; + _paymentOption = json["payment_option"]; + _deleted = json["deleted"]; + _createdOnUtc = json["created_on_utc"]; + _customer = json["customer"] != null ? PackagesCustomerResponseModel.fromJson(json["customer"]) : null; + _customerId = json["customer_id"]; + _billingAddress = json["billing_address"]; + _shippingAddress = json["shipping_address"]; + if (json["order_items"] != null) { + _orderItems = []; + json["order_items"].forEach((v) { + _orderItems.add(PackagesCartItemsResponseModel.fromJson(v)); + }); + } + _orderStatus = json["order_status"]; + _paymentStatus = json["payment_status"]; + _shippingStatus = json["shipping_status"]; + _customerTaxDisplayType = json["customer_tax_display_type"]; + _id = json["id"]; + } + + Map toJson() { + var map = {}; + map["custom_order_number"] = _customOrderNumber; + map["store_id"] = _storeId; + map["pick_up_in_store"] = _pickUpInStore; + map["payment_method_system_name"] = _paymentMethodSystemName; + map["customer_currency_code"] = _customerCurrencyCode; + map["currency_rate"] = _currencyRate; + map["customer_tax_display_type_id"] = _customerTaxDisplayTypeId; + map["vat_number"] = _vatNumber; + map["order_subtotal_incl_tax"] = _orderSubtotalInclTax; + map["order_subtotal_excl_tax"] = _orderSubtotalExclTax; + map["order_sub_total_discount_incl_tax"] = _orderSubTotalDiscountInclTax; + map["order_sub_total_discount_excl_tax"] = _orderSubTotalDiscountExclTax; + map["order_shipping_incl_tax"] = _orderShippingInclTax; + map["order_shipping_excl_tax"] = _orderShippingExclTax; + map["payment_method_additional_fee_incl_tax"] = _paymentMethodAdditionalFeeInclTax; + map["payment_method_additional_fee_excl_tax"] = _paymentMethodAdditionalFeeExclTax; + map["tax_rates"] = _taxRates; + map["order_tax"] = _orderTax; + map["order_discount"] = _orderDiscount; + map["order_total"] = _orderTotal; + map["refunded_amount"] = _refundedAmount; + map["reward_points_were_added"] = _rewardPointsWereAdded; + map["checkout_attribute_description"] = _checkoutAttributeDescription; + map["customer_language_id"] = _customerLanguageId; + map["affiliate_id"] = _affiliateId; + map["customer_ip"] = _customerIp; + map["authorization_transaction_id"] = _authorizationTransactionId; + map["authorization_transaction_code"] = _authorizationTransactionCode; + map["authorization_transaction_result"] = _authorizationTransactionResult; + map["capture_transaction_id"] = _captureTransactionId; + map["capture_transaction_result"] = _captureTransactionResult; + map["subscription_transaction_id"] = _subscriptionTransactionId; + map["paid_date_utc"] = _paidDateUtc; + map["shipping_method"] = _shippingMethod; + map["shipping_rate_computation_method_system_name"] = _shippingRateComputationMethodSystemName; + map["custom_values_xml"] = _customValuesXml; + map["payment_option"] = _paymentOption; + map["deleted"] = _deleted; + map["created_on_utc"] = _createdOnUtc; + if (_customer != null) { + map["customer"] = _customer.toJson(); + } + map["customer_id"] = _customerId; + map["billing_address"] = _billingAddress; + map["shipping_address"] = _shippingAddress; + if (_orderItems != null) { + map["order_items"] = _orderItems.map((v) => v.toJson()).toList(); + } + map["order_status"] = _orderStatus; + map["payment_status"] = _paymentStatus; + map["shipping_status"] = _shippingStatus; + map["customer_tax_display_type"] = _customerTaxDisplayType; + map["id"] = _id; + return map; + } + +} diff --git a/lib/core/service/packages_offers/PackagesOffersServices.dart b/lib/core/service/packages_offers/PackagesOffersServices.dart index c0c7bc94..15811b82 100644 --- a/lib/core/service/packages_offers/PackagesOffersServices.dart +++ b/lib/core/service/packages_offers/PackagesOffersServices.dart @@ -3,6 +3,8 @@ import 'dart:developer'; import 'dart:ui'; import 'package:diplomaticquarterapp/config/config.dart'; +import 'package:diplomaticquarterapp/core/enum/PaymentOptions.dart'; +import 'package:diplomaticquarterapp/core/model/ResponseModel.dart'; import 'package:diplomaticquarterapp/core/model/packages_offers/requests/AddProductToCartRequestModel.dart'; import 'package:diplomaticquarterapp/core/model/packages_offers/requests/CreateCustomerRequestModel.dart'; import 'package:diplomaticquarterapp/core/model/packages_offers/requests/OffersCategoriesRequestModel.dart'; @@ -11,6 +13,7 @@ import 'package:diplomaticquarterapp/core/model/packages_offers/responses/Packag import 'package:diplomaticquarterapp/core/model/packages_offers/responses/PackagesCategoriesResponseModel.dart'; import 'package:diplomaticquarterapp/core/model/packages_offers/responses/PackagesResponseModel.dart'; import 'package:diplomaticquarterapp/core/model/packages_offers/responses/PackagesCustomerResponseModel.dart'; +import 'package:diplomaticquarterapp/core/model/packages_offers/responses/order_response_model.dart'; import 'package:diplomaticquarterapp/core/service/base_service.dart'; import 'package:diplomaticquarterapp/core/service/client/base_app_client.dart'; import 'package:diplomaticquarterapp/uitl/gif_loader_dialog_utils.dart'; @@ -24,7 +27,8 @@ class OffersAndPackagesServices extends BaseService { List latestOffersList = List(); List bestSellerList = List(); List bannersList = List(); - List cartItemList = List(); + List cartItemList = List(); + String cartItemCount = ""; PackagesCustomerResponseModel customer; @@ -154,15 +158,15 @@ class OffersAndPackagesServices extends BaseService { // -------------------- // Create Customer // -------------------- - Future createCustomer(PackagesCustomerRequestModel request, {@required BuildContext context, bool showLoading = true, Function(bool) completion }) async{ + Future createCustomer(PackagesCustomerRequestModel request, {@required BuildContext context, bool showLoading = true, Function(bool) completion }) async{ if(customer != null) return Future.value(customer); - hasError = false; - var url = EXA_CART_API_BASE_URL + PACKAGES_CUSTOMER; - customer = null; + Future errorThrow; + _showLoading(context, showLoading); + var url = EXA_CART_API_BASE_URL + PACKAGES_CUSTOMER; await baseAppClient.simplePost(url, body: request.json(), onSuccess: (dynamic stringResponse, int statusCode){ _hideLoading(context, showLoading); @@ -172,10 +176,13 @@ class OffersAndPackagesServices extends BaseService { }, onFailure: (String error, int statusCode){ _hideLoading(context, showLoading); + errorThrow = Future.error(error); log(error); }); - return customer; + await Future.delayed(Duration(seconds: 1)); + + return errorThrow ?? customer; } @@ -183,7 +190,7 @@ class OffersAndPackagesServices extends BaseService { // -------------------- // Shopping Cart // -------------------- - Future> cartItems({@required BuildContext context, bool showLoading = true}) async{ + Future> cartItems({@required BuildContext context, bool showLoading = true}) async{ Future errorThrow; cartItemList.clear(); @@ -194,7 +201,7 @@ class OffersAndPackagesServices extends BaseService { var jsonResponse = json.decode(stringResponse); jsonResponse['shopping_carts'].forEach((json) { - cartItemList.add(CartProductResponseModel.fromJson(json)); + cartItemList.add(PackagesCartItemsResponseModel.fromJson(json)); }); }, onFailure: (String error, int statusCode) { @@ -206,8 +213,9 @@ class OffersAndPackagesServices extends BaseService { return errorThrow ?? cartItemList; } - Future addProductToCart(AddProductToCartRequestModel request, {@required BuildContext context, bool showLoading = true}) async{ + Future> addProductToCart(AddProductToCartRequestModel request, {@required BuildContext context, bool showLoading = true}) async{ Future errorThrow; + ResponseModel response; request.customer_id = customer.id; @@ -217,14 +225,16 @@ class OffersAndPackagesServices extends BaseService { _hideLoading(context, showLoading); var jsonResponse = json.decode(stringResponse); + var jsonCartItem = jsonResponse["shopping_carts"][0]; + response = ResponseModel(status: true, data: PackagesCartItemsResponseModel.fromJson(jsonCartItem), error: null); + cartItemCount = response.data.quantity.toString(); }, onFailure: (String error, int statusCode){ _hideLoading(context, showLoading); - log(error); - errorThrow = Future.error(error); + errorThrow = Future.error(ResponseModel(status: true, data: null, error: error)); }); - return errorThrow ?? true; + return errorThrow ?? response; } Future updateProductToCart(int cartItemID, {UpdateProductToCartRequestModel request, @required BuildContext context, bool showLoading = true}) async{ @@ -268,21 +278,29 @@ class OffersAndPackagesServices extends BaseService { // -------------------- // Place Order // -------------------- - Future placeOrder({@required BuildContext context, bool showLoading = true}) async{ + Future placeOrder({@required String paymentOption, @required BuildContext context, bool showLoading = true}) async{ Future errorThrow; var jsonBody = { "order": { - "customer_id" : customer.id + "customer_id" : customer.id, + "billing_address": { + "email": customer.email, + "phone_number": customer.addresses.first.phoneNumber + }, + "payment_method_system_name": "Payments.PayFort", + "payment_option": paymentOption } }; + int order_id; _showLoading(context, showLoading); - var url = EXA_CART_API_BASE_URL + PACKAGES_SHOPPING_CART; + var url = EXA_CART_API_BASE_URL + PACKAGES_ORDERS; await baseAppClient.simplePost(url, body: jsonBody, onSuccess: (dynamic stringResponse, int statusCode){ _hideLoading(context, showLoading); var jsonResponse = json.decode(stringResponse); + order_id = jsonResponse['orders'][0]['id']; }, onFailure: (String error, int statusCode){ _hideLoading(context, showLoading); @@ -290,7 +308,28 @@ class OffersAndPackagesServices extends BaseService { errorThrow = Future.error(error); }); - return errorThrow ?? true; + return errorThrow ?? order_id; + } + + Future> getOrderById(int id, {@required BuildContext context, bool showLoading = true}) async{ + Future errorThrow; + ResponseModel response; + + _showLoading(context, showLoading); + var url = EXA_CART_API_BASE_URL + PACKAGES_ORDERS + '/$id'; + await baseAppClient.simpleGet(url, onSuccess: (dynamic stringResponse, int statusCode) { + _hideLoading(context, showLoading); + + var jsonResponse = json.decode(stringResponse); + var jsonOrder = jsonResponse['orders'][0]; + response = ResponseModel(status: true, data: PackagesOrderResponseModel.fromJson(jsonOrder)); + + }, onFailure: (String error, int statusCode) { + _hideLoading(context, showLoading); + errorThrow = Future.error(ResponseModel(status: false,error: error)); + }, queryParams: null); + + return errorThrow ?? response; } } diff --git a/lib/core/viewModels/packages_offers/PackagesOffersViewModel.dart b/lib/core/viewModels/packages_offers/PackagesOffersViewModel.dart index 83e104dc..1029ed4d 100644 --- a/lib/core/viewModels/packages_offers/PackagesOffersViewModel.dart +++ b/lib/core/viewModels/packages_offers/PackagesOffersViewModel.dart @@ -9,10 +9,11 @@ import 'package:diplomaticquarterapp/core/viewModels/base_view_model.dart'; import 'package:flutter/cupertino.dart'; import 'package:diplomaticquarterapp/locator.dart'; -class OfferCategoriesViewModel extends BaseViewModel { +class OfferCategoriesViewModel extends BaseViewModel{ OffersAndPackagesServices service = locator(); get categoryList => service.categoryList; get productList => service.categoryList; + } class PackagesViewModel extends BaseViewModel { @@ -22,5 +23,15 @@ class PackagesViewModel extends BaseViewModel { List get latestOffersList => service.latestOffersList; List get bestSellerList => service.bestSellerList; List get bannersList => service.bannersList; - List get cartItemList => service.cartItemList; + List get cartItemList => service.cartItemList; + + + String _cartItemCount = ""; + + String get cartItemCount => _cartItemCount; + + set cartItemCount(String value) { + _cartItemCount = value; + notifyListeners(); + } } diff --git a/lib/main.dart b/lib/main.dart index 05615a68..aaffe3c3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -60,7 +60,6 @@ class MyApp extends StatelessWidget { ], child: Consumer( builder: (context, projectProvider, child) => MaterialApp( - showSemanticsDebugger: false, title: 'Diplomatic Quarter App', locale: projectProvider.appLocal, @@ -91,6 +90,7 @@ class MyApp extends StatelessWidget { color: Color(0xffB8382C), ), ), + floatingActionButtonTheme: FloatingActionButtonThemeData(highlightElevation: 2, disabledElevation: 0, elevation: 2), disabledColor: Colors.grey[300], errorColor: Color.fromRGBO(235, 80, 60, 1.0), scaffoldBackgroundColor: Color(0xffE9E9E9), // Colors.grey[100], @@ -115,8 +115,9 @@ class MyApp extends StatelessWidget { ), ), ), - // initialRoute: SPLASH, - initialRoute: PACKAGES_OFFERS, + initialRoute: SPLASH, + // initialRoute: PACKAGES_OFFERS, + // initialRoute: PACKAGES_ORDER_COMPLETED, routes: routes, debugShowCheckedModeBanner: false, ), diff --git a/lib/pages/landing/home_page.dart b/lib/pages/landing/home_page.dart index e3156331..94ebbc06 100644 --- a/lib/pages/landing/home_page.dart +++ b/lib/pages/landing/home_page.dart @@ -10,6 +10,7 @@ import 'package:diplomaticquarterapp/pages/Covid-DriveThru/covid-drivethru-locat import 'package:diplomaticquarterapp/pages/ErService/ErOptions.dart'; import 'package:diplomaticquarterapp/pages/base/base_view.dart'; import 'package:diplomaticquarterapp/pages/livecare/livecare_home.dart'; +import 'package:diplomaticquarterapp/pages/packages_offers/OfferAndPackagesPage.dart'; import 'package:diplomaticquarterapp/pages/paymentService/payment_service.dart'; import 'package:diplomaticquarterapp/pages/pharmacies/screens/pharmacy_module_page.dart'; import 'package:diplomaticquarterapp/uitl/date_uitl.dart'; @@ -410,6 +411,34 @@ class _HomePageState extends State { ), ], ), + + Padding( + padding: const EdgeInsets.only(bottom: 15, right: 15, left: 15), + child: InkWell( + onTap: (){ + Navigator.of(context).push(MaterialPageRoute(builder: (context) => PackagesHomePage())); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Color(0xffB8382C), + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + Text( + TranslationBase.of(context).offerAndPackages, + style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.normal), + ), + Spacer(), + Image.asset("assets/images/offer_icon.png"), + ], + ), + ), + ), + ), + ), if(projectViewModel.havePrivilege(64)||projectViewModel.havePrivilege(65)||projectViewModel.havePrivilege(67)) Container( margin: EdgeInsets.only(left: 15, right: 15), diff --git a/lib/pages/packages_offers/ClinicOfferAndPackagesPage.dart b/lib/pages/packages_offers/ClinicOfferAndPackagesPage.dart index b1e447ef..76688606 100644 --- a/lib/pages/packages_offers/ClinicOfferAndPackagesPage.dart +++ b/lib/pages/packages_offers/ClinicOfferAndPackagesPage.dart @@ -1,4 +1,6 @@ +import 'package:after_layout/after_layout.dart'; import 'package:carousel_slider/carousel_slider.dart'; +import 'package:diplomaticquarterapp/core/model/packages_offers/requests/AddProductToCartRequestModel.dart'; import 'package:diplomaticquarterapp/core/model/packages_offers/requests/OffersCategoriesRequestModel.dart'; import 'package:diplomaticquarterapp/core/model/packages_offers/requests/OffersProductsRequestModel.dart'; import 'package:diplomaticquarterapp/core/model/packages_offers/responses/PackagesCategoriesResponseModel.dart'; @@ -21,6 +23,8 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_material_pickers/flutter_material_pickers.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'CreateCustomerDailogPage.dart'; + dynamic languageID; class ClinicPackagesPage extends StatefulWidget { @@ -33,15 +37,39 @@ class ClinicPackagesPage extends StatefulWidget { } -class _ClinicPackagesPageState extends State { - List get _products => widget.products; +class _ClinicPackagesPageState extends State with AfterLayoutMixin{ + AppScaffold appScaffold; + List get _products => widget.products; PackagesViewModel viewModel; + + + onProductCartClick(PackagesResponseModel product) async { + if(viewModel.service.customer == null) + viewModel.service.customer = await CreateCustomerDialogPage(context: context).show(); + + if(viewModel.service.customer != null) { + var request = AddProductToCartRequestModel(product_id: product.id, customer_id: viewModel.service.customer.id); + await viewModel.service.addProductToCart(request, context: context).then((response){ + appScaffold.appBar.badgeUpdater(viewModel.service.cartItemCount); + }).catchError((error) { + utils.Utils.showErrorToast(error); + }); + } + } + + + @override + void afterFirstLayout(BuildContext context) async{ + appScaffold.appBar.badgeUpdater(viewModel.service.cartItemCount); + } + @override void initState() { super.initState(); } + @override Widget build(BuildContext context) { @@ -50,7 +78,7 @@ class _ClinicPackagesPageState extends State { onModelReady: (model){ viewModel = model; }, - builder: (_, model, wi) => AppScaffold( + builder: (_, model, wi) => appScaffold = AppScaffold( appBarTitle: TranslationBase.of(context).offerAndPackages, isShowAppBar: true, isPharmacy: false, @@ -66,7 +94,7 @@ class _ClinicPackagesPageState extends State { itemCount: _products.length, itemBuilder: (BuildContext context, int index) => new Container( color: Colors.transparent, - child: PackagesItemCard( itemContentPadding: 10,itemModel: _products[index],) + child: PackagesItemCard( itemContentPadding: 10,itemModel: _products[index], onCartClick: onProductCartClick,) ), staggeredTileBuilder: (int index) => StaggeredTile.fit(2), mainAxisSpacing: 20, diff --git a/lib/pages/packages_offers/CreateCustomerDailogPage.dart b/lib/pages/packages_offers/CreateCustomerDailogPage.dart new file mode 100644 index 00000000..4cf5fa5b --- /dev/null +++ b/lib/pages/packages_offers/CreateCustomerDailogPage.dart @@ -0,0 +1,206 @@ +import 'package:after_layout/after_layout.dart'; +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:diplomaticquarterapp/config/size_config.dart'; +import 'package:diplomaticquarterapp/core/model/packages_offers/requests/AddProductToCartRequestModel.dart'; +import 'package:diplomaticquarterapp/core/model/packages_offers/requests/CreateCustomerRequestModel.dart'; +import 'package:diplomaticquarterapp/core/model/packages_offers/requests/OffersCategoriesRequestModel.dart'; +import 'package:diplomaticquarterapp/core/model/packages_offers/requests/OffersProductsRequestModel.dart'; +import 'package:diplomaticquarterapp/core/model/packages_offers/responses/PackagesCustomerResponseModel.dart'; +import 'package:diplomaticquarterapp/core/model/packages_offers/responses/PackagesResponseModel.dart'; +import 'package:diplomaticquarterapp/core/viewModels/packages_offers/PackagesOffersViewModel.dart'; +import 'package:diplomaticquarterapp/core/viewModels/pharmacyModule/order_model_view_model.dart'; +import 'package:diplomaticquarterapp/locator.dart'; +import 'package:diplomaticquarterapp/pages/base/base_view.dart'; +import 'package:diplomaticquarterapp/pages/packages_offers/ClinicOfferAndPackagesPage.dart'; +import 'package:diplomaticquarterapp/pages/packages_offers/OfferAndPackageDetailPage.dart'; +import 'package:diplomaticquarterapp/pages/packages_offers/OfferAndPackagesCartPage.dart'; +import 'package:diplomaticquarterapp/uitl/gif_loader_dialog_utils.dart'; +import 'package:diplomaticquarterapp/uitl/utils.dart' as utils; +import 'package:diplomaticquarterapp/widgets/AnimatedTextFields.dart'; +import 'package:diplomaticquarterapp/widgets/Loader/gif_loader_container.dart'; +import 'package:diplomaticquarterapp/widgets/LoadingButton.dart'; +import 'package:diplomaticquarterapp/widgets/carousel_indicator/carousel_indicator.dart'; +import 'package:diplomaticquarterapp/widgets/loadings/ShimmerLoading.dart'; +import 'package:diplomaticquarterapp/widgets/offers_packages/PackagesOfferCard.dart'; +import 'package:diplomaticquarterapp/widgets/others/app_scaffold_widget.dart'; +import 'package:diplomaticquarterapp/uitl/translations_delegate_base.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:diplomaticquarterapp/config/shared_pref_kay.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_material_pickers/flutter_material_pickers.dart'; + +dynamic languageID; +var emailRegex = RegExp(r'^[^\s@]+@[^\s@]+\.[^\s@]+$'); + +class CreateCustomerDialogPage extends StatefulWidget { + final BuildContext context; + CreateCustomerDialogPage({this.context}); + PackagesViewModel viewModel; + + Future show() async{ + await showDialog(context: context, builder: (context ){ + return AlertDialog(content: this, shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20) + ), elevation: 5, ); + }); + return viewModel.service.customer; + } + + @override + _CreateCustomerDialogPageState createState() => _CreateCustomerDialogPageState(); + +} + +class _CreateCustomerDialogPageState extends State with AfterLayoutMixin, TickerProviderStateMixin{ + AnimationController _loadingController; + AnimationController _submitController; + bool _enableInput = true; + + Interval _nameTextFieldLoadingAnimationInterval = const Interval(0, .85); + + final _phoneFocusNode = FocusNode(); + + @override + void initState() { + _submitController = AnimationController(vsync: this, duration: Duration(milliseconds: 1000),); + super.initState(); + } + + @override + void afterFirstLayout(BuildContext context) async{ + } + + // Controllers + TextEditingController _emailTextController = TextEditingController(); + TextEditingController _phoneTextController = TextEditingController(); + TextEditingController _emailPinTextController = TextEditingController(); + TextEditingController _phonePinTextController = TextEditingController(); + + bool verifyPin = false; + + PackagesViewModel viewModel() => widget.viewModel; + + @override + Widget build(BuildContext context) { + + return BaseView( + allowAny: true, + onModelReady: (model) => widget.viewModel = model, + builder: (_, model, wi) => verifyPin ? verifyPinWidget() : userDetailWidget() + ); + } + + Widget verifyPinWidget(){ + + } + + + Widget userDetailWidget(){ + return + Container( + width: SizeConfig.realScreenWidth * 0.8, + height: 270, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text("Create Guest Customer"), + SizedBox(height: 30,), + AnimatedTextFormField( + enabled: _enableInput, + controller: _emailTextController, + width: 100, + loadingController: _loadingController, + interval: _nameTextFieldLoadingAnimationInterval, + labelText: "Email", + prefixIcon: Icon(Icons.email), + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, + onFieldSubmitted: (value) { + FocusScope.of(context).requestFocus(_phoneFocusNode); + }, + validator: (value){ + return (value.isEmpty || !emailRegex.hasMatch(value)) + ? 'Invalid email!' + : null; + }, + ), + + SizedBox(height: 30,), + AnimatedTextFormField( + enabled: _enableInput, + controller: _phoneTextController, + width: 100, + loadingController: _loadingController, + interval: _nameTextFieldLoadingAnimationInterval, + labelText: "Mobile Number", + prefixIcon: Icon(Icons.phone_android), + keyboardType: TextInputType.phone, + textInputAction: TextInputAction.next, + onFieldSubmitted: (value) { + FocusScope.of(context).requestFocus(_phoneFocusNode); + }, + validator: (value){ + return (value.isEmpty || !emailRegex.hasMatch(value)) + ? 'Invalid email!' + : null; + }, + ), + Spacer(flex: 1,), + + AnimatedButton( + color: Theme.of(context).primaryColor, + loadingColor: Theme.of(context).primaryColor, + controller: _submitController, + text: TranslationBase.of(context).done, + onPressed: (){ + createCustomer(); + }, + ) + + // RaisedButton( + // child: Text( + // TranslationBase.of(context).done, + // style: TextStyle(fontSize: 15, color: Colors.white, fontWeight: FontWeight.bold), + // ), + // padding: EdgeInsets.only(top: 5, bottom: 5, left: 0, right: 0), + // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5), side: BorderSide(color: Theme.of(context).primaryColor, width: 0.5)), + // color: Theme.of(context).primaryColor, + // onPressed: (){ + // + // }, + // ) + , + ], + ), + ); + } + + createCustomer() async{ + setState(() => _enableInput = false); + + loading(true); + var request = PackagesCustomerRequestModel(email: _emailTextController.text, phoneNumber: _phoneTextController.text); + viewModel().service + .createCustomer(request, context: context, showLoading: false) + .then((value) => success()) + .catchError((error) => showError(error)); + + } + + success() async{ + loading(false); + await Future.delayed(Duration(seconds: 2)); + Navigator.of(context).pop(); + } + + showError(String errorMessage) async{ + loading(false); + setState(() => _enableInput = true); + } + + loading(bool can){ + can ? _submitController.forward() : _submitController.reverse(); + } + +} diff --git a/lib/pages/packages_offers/OfferAndPackagesCartPage.dart b/lib/pages/packages_offers/OfferAndPackagesCartPage.dart index 98f237f6..72a82d0a 100644 --- a/lib/pages/packages_offers/OfferAndPackagesCartPage.dart +++ b/lib/pages/packages_offers/OfferAndPackagesCartPage.dart @@ -1,5 +1,7 @@ import 'package:after_layout/after_layout.dart'; import 'package:carousel_slider/carousel_slider.dart'; +import 'package:diplomaticquarterapp/core/enum/PaymentOptions.dart'; +import 'package:diplomaticquarterapp/core/model/ResponseModel.dart'; import 'package:diplomaticquarterapp/core/model/packages_offers/requests/AddProductToCartRequestModel.dart'; import 'package:diplomaticquarterapp/core/model/packages_offers/requests/OffersCategoriesRequestModel.dart'; import 'package:diplomaticquarterapp/core/model/packages_offers/requests/OffersProductsRequestModel.dart'; @@ -9,11 +11,13 @@ import 'package:diplomaticquarterapp/locator.dart'; import 'package:diplomaticquarterapp/pages/base/base_view.dart'; import 'package:diplomaticquarterapp/pages/packages_offers/ClinicOfferAndPackagesPage.dart'; import 'package:diplomaticquarterapp/pages/packages_offers/OfferAndPackageDetailPage.dart'; +import 'package:diplomaticquarterapp/pages/packages_offers/PackageOrderCompletedPage.dart'; import 'package:diplomaticquarterapp/pages/pharmacies/screens/pharmacy-terms-conditions-page.dart'; import 'package:diplomaticquarterapp/uitl/gif_loader_dialog_utils.dart'; import 'package:diplomaticquarterapp/uitl/utils.dart' as utils; import 'package:diplomaticquarterapp/widgets/Loader/gif_loader_container.dart'; import 'package:diplomaticquarterapp/widgets/carousel_indicator/carousel_indicator.dart'; +import 'package:diplomaticquarterapp/widgets/in_app_browser/InAppBrowser.dart'; import 'package:diplomaticquarterapp/widgets/loadings/ShimmerLoading.dart'; import 'package:diplomaticquarterapp/widgets/offers_packages/PackagesCartItemCard.dart'; import 'package:diplomaticquarterapp/widgets/offers_packages/PackagesOfferCard.dart'; @@ -73,7 +77,21 @@ class _PackagesCartPageState extends State with AfterLayoutMix } onPayNowClick() async{ - await viewModel.service.placeOrder(context: context); + await viewModel.service.placeOrder(context: context,paymentOption: _selectedPaymentMethod.toUpperCase()).then((orderId){ + if(orderId.runtimeType == int){ // result == order_id + var browser = MyInAppBrowser( + context: context, + onExitCallback: (data, isDone) => paymentClosed(orderId: orderId, withStatus: isDone, data: data) + ); + browser.openPackagesPaymentBrowser(customer_id: viewModel.service.customer.id, order_id: orderId); + + }else{ + utils.Utils.showErrorToast('Failed to place order, please try again later'); + } + + }).catchError((error){ + utils.Utils.showErrorToast(error); + }); } @override @@ -124,10 +142,10 @@ class _PackagesCartPageState extends State with AfterLayoutMix itemModel: item, shouldStepperChangeApply: (apply,total) async{ var request = AddProductToCartRequestModel(product_id: item.productId, quantity: apply); - bool success = await viewModel.service.addProductToCart(request, context: context, showLoading: false).catchError((error){ + ResponseModel response = await viewModel.service.addProductToCart(request, context: context, showLoading: false).catchError((error){ utils.Utils.showErrorToast(error); }); - return success ?? false; + return response.status ?? false; }, ) ); @@ -180,13 +198,27 @@ class _PackagesCartPageState extends State with AfterLayoutMix await viewModel.service.cartItems(context: context).catchError((error) {}); setState((){}); } + + paymentClosed({@required int orderId, @required bool withStatus, dynamic data}) async{ + viewModel.service.getOrderById(orderId, context: context).then((value){ + var heading = withStatus ? "Success" : "Failed"; + var title = "Your order has been placed successfully"; + var subTitle = "Order# ${value.data.customOrderNumber}"; + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => PackageOrderCompletedPage(heading: heading, title: title, subTitle: subTitle)) + ); + + }).catchError((error){ + debugPrint(error); + }); + } } // /* Payment Footer Widgets */ // --------------------------- String _selectedPaymentMethod; Widget _paymentOptions(BuildContext context, Function(String) onSelected) { - double height = 22; + double height = 30; Widget buttonContent(bool isSelected, String imageName) { return Container( diff --git a/lib/pages/packages_offers/OfferAndPackagesPage.dart b/lib/pages/packages_offers/OfferAndPackagesPage.dart index 0a83c683..c908cba4 100644 --- a/lib/pages/packages_offers/OfferAndPackagesPage.dart +++ b/lib/pages/packages_offers/OfferAndPackagesPage.dart @@ -4,12 +4,14 @@ import 'package:diplomaticquarterapp/core/model/packages_offers/requests/AddProd import 'package:diplomaticquarterapp/core/model/packages_offers/requests/CreateCustomerRequestModel.dart'; import 'package:diplomaticquarterapp/core/model/packages_offers/requests/OffersCategoriesRequestModel.dart'; import 'package:diplomaticquarterapp/core/model/packages_offers/requests/OffersProductsRequestModel.dart'; +import 'package:diplomaticquarterapp/core/model/packages_offers/responses/PackagesCustomerResponseModel.dart'; import 'package:diplomaticquarterapp/core/model/packages_offers/responses/PackagesResponseModel.dart'; import 'package:diplomaticquarterapp/core/viewModels/packages_offers/PackagesOffersViewModel.dart'; import 'package:diplomaticquarterapp/core/viewModels/pharmacyModule/order_model_view_model.dart'; import 'package:diplomaticquarterapp/locator.dart'; import 'package:diplomaticquarterapp/pages/base/base_view.dart'; import 'package:diplomaticquarterapp/pages/packages_offers/ClinicOfferAndPackagesPage.dart'; +import 'package:diplomaticquarterapp/pages/packages_offers/CreateCustomerDailogPage.dart'; import 'package:diplomaticquarterapp/pages/packages_offers/OfferAndPackageDetailPage.dart'; import 'package:diplomaticquarterapp/pages/packages_offers/OfferAndPackagesCartPage.dart'; import 'package:diplomaticquarterapp/uitl/gif_loader_dialog_utils.dart'; @@ -85,120 +87,123 @@ class _PackagesHomePageState extends State with AfterLayoutMix onProductCartClick(PackagesResponseModel product) async { if(viewModel.service.customer == null) - await viewModel.service.createCustomer(PackagesCustomerRequestModel(email: "zikambrani@gmail.com", phoneNumber: "0500409598"), context: context); + viewModel.service.customer = await CreateCustomerDialogPage(context: context).show(); - if(viewModel.service.customer != null){ + if(viewModel.service.customer != null) { var request = AddProductToCartRequestModel(product_id: product.id, customer_id: viewModel.service.customer.id); - await viewModel.service.addProductToCart(request, context: context).catchError((error) { + await viewModel.service.addProductToCart(request, context: context).then((response){ + appScaffold.appBar.badgeUpdater(viewModel.service.cartItemCount); + }).catchError((error) { utils.Utils.showErrorToast(error); }); } } + AppScaffold appScaffold; @override Widget build(BuildContext context) { - return BaseView( allowAny: true, onModelReady: (model) => viewModel = model, builder: (_, model, wi){ return - AppScaffold( - appBarTitle: TranslationBase.of(context).offerAndPackages, - isShowAppBar: true, - isPharmacy: false, - showPharmacyCart: false, - showHomeAppBarIcon: false, - isOfferPackages: true, - showOfferPackagesCart: true, - isShowDecPage: false, - body: ListView( - children: [ - - // Top Banner Carousel - AspectRatio( - aspectRatio: 2.2/1, - child: bannerCarousel() - ), - - Center( - child: CarouselIndicator( - activeColor: Theme.of(context).appBarTheme.color, - color: Colors.grey[300], - cornerRadius: 15, - width: 15, height: 15, - count: _bannerCarousel.itemCount, - index: carouselIndicatorIndex, - onClick: (index){ - debugPrint('onClick at ${index}'); - }, - ), - ), - - SizedBox(height: 10,), - - Padding( - padding: const EdgeInsets.all(15), - child: Column( - children: [ - // Search Textfield - searchTextField(), - - SizedBox(height: 10,), - - // Filter Selection - filterOptionSelection(), - - SizedBox(height: 20,), - - // Horizontal Scrollable Cards - Text( - "Latest offers", - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.black87, - fontSize: 20 - ), - ), - - // Latest Offers Horizontal Scrollable List - AspectRatio( - aspectRatio: 1.3/1, - child: LayoutBuilder(builder: (context, constraints){ - double itemContentPadding = 10; - double itemWidth = (constraints.maxWidth/2) - (itemContentPadding*2); - return latestOfferListView(itemWidth: itemWidth, itemContentPadding: itemContentPadding); - }), + appScaffold = + AppScaffold( + appBarTitle: TranslationBase.of(context).offerAndPackages, + isShowAppBar: true, + isPharmacy: false, + showPharmacyCart: false, + showHomeAppBarIcon: false, + isOfferPackages: true, + showOfferPackagesCart: true, + isShowDecPage: false, + body: ListView( + children: [ + + // Top Banner Carousel + AspectRatio( + aspectRatio: 2.2/1, + child: bannerCarousel() + ), + + Center( + child: CarouselIndicator( + activeColor: Theme.of(context).appBarTheme.color, + color: Colors.grey[300], + cornerRadius: 15, + width: 15, height: 15, + count: _bannerCarousel.itemCount, + index: carouselIndicatorIndex, + onClick: (index){ + debugPrint('onClick at ${index}'); + }, ), - - SizedBox(height: 10,), - - Text( - "Best sellers", - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.black87, - fontSize: 20 - ), - ), - - - // Best Seller Horizontal Scrollable List - AspectRatio( - aspectRatio: 1.3/1, - child: LayoutBuilder(builder: (context, constraints){ - double itemContentPadding = 10; // 10 is content padding in each item - double itemWidth = (constraints.maxWidth/2) - (itemContentPadding*2 /* 2 = LeftRight */); - return bestSellerListView(itemWidth: itemWidth, itemContentPadding: itemContentPadding); - }), - ) - - ],), + ), + + SizedBox(height: 10,), + + Padding( + padding: const EdgeInsets.all(15), + child: Column( + children: [ + // Search Textfield + searchTextField(), + + SizedBox(height: 10,), + + // Filter Selection + filterOptionSelection(), + + SizedBox(height: 20,), + + // Horizontal Scrollable Cards + Text( + "Latest offers", + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black87, + fontSize: 20 + ), + ), + + // Latest Offers Horizontal Scrollable List + AspectRatio( + aspectRatio: 1.3/1, + child: LayoutBuilder(builder: (context, constraints){ + double itemContentPadding = 10; + double itemWidth = (constraints.maxWidth/2) - (itemContentPadding*2); + return latestOfferListView(itemWidth: itemWidth, itemContentPadding: itemContentPadding); + }), + ), + + SizedBox(height: 10,), + + Text( + "Best sellers", + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black87, + fontSize: 20 + ), + ), + + + // Best Seller Horizontal Scrollable List + AspectRatio( + aspectRatio: 1.3/1, + child: LayoutBuilder(builder: (context, constraints){ + double itemContentPadding = 10; // 10 is content padding in each item + double itemWidth = (constraints.maxWidth/2) - (itemContentPadding*2 /* 2 = LeftRight */); + return bestSellerListView(itemWidth: itemWidth, itemContentPadding: itemContentPadding); + }), + ) + + ],), + ), + ], ), - ], - ), - ) - .setOnAppBarCartClick(onCartClick); + ) + .setOnAppBarCartClick(onCartClick); } ); } diff --git a/lib/pages/packages_offers/PackageOrderCompletedPage.dart b/lib/pages/packages_offers/PackageOrderCompletedPage.dart new file mode 100644 index 00000000..c0a7db46 --- /dev/null +++ b/lib/pages/packages_offers/PackageOrderCompletedPage.dart @@ -0,0 +1,146 @@ +import 'package:diplomaticquarterapp/core/viewModels/packages_offers/PackagesOffersViewModel.dart'; +import 'package:diplomaticquarterapp/pages/base/base_view.dart'; +import 'package:diplomaticquarterapp/uitl/translations_delegate_base.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_svg/svg.dart'; + +dynamic languageID; + +class PackageOrderCompletedPage extends StatelessWidget{ + double buttonHeight; + double buttonWidth; + Widget icon; + String heading; + String title; + String subTitle; + String actionTitle; + + PackageOrderCompletedPage({this.buttonWidth, this.buttonHeight, @required this.heading, @required this.title, @required this.subTitle, this.actionTitle }); + + @override + Widget build(BuildContext context) { + assert((heading != null || title != null || subTitle != null), "Data missing in properties"); + + buttonWidth = buttonWidth ?? MediaQuery.of(context).size.width/2; + buttonHeight = buttonHeight ?? 40; + actionTitle = actionTitle ?? TranslationBase.of(context).done; + + return BaseView( + allowAny: true, + onModelReady: (model){}, + builder: (_, model, wi){ + return Container( + color: Colors.white, + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + + AspectRatio( + aspectRatio: 1.2/1, + child: + iconWidget(context), + ), + + headingWidget(context), + + + AspectRatio( + aspectRatio: 1/1, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + titleWidget(context), + SizedBox(height: 20,), + subTitleWidget(context), + SizedBox(height: 50,), + actionWidget(context) + ], + ), + ), + ) + + ], + ), + ), + ); + } + ); + } + + Widget iconWidget(BuildContext context){ + return Padding( + padding: const EdgeInsets.all(50), + child: icon ?? SvgPicture.asset( + "assets/images/svg/success.svg", + semanticsLabel: 'icon' + ), + ); + } + + Widget headingWidget(BuildContext context) => Text( + heading, + textAlign: TextAlign.center, + maxLines: 1, + style: TextStyle( + color: Theme.of(context).primaryColor, + fontSize: 35.0, + fontWeight: FontWeight.bold, + letterSpacing: 0.9 + ) + ); + + Widget titleWidget(BuildContext context) => Text( + title, + textAlign: TextAlign.center, + maxLines: 2, + style: TextStyle( + color: Theme.of(context).primaryColor, + fontSize: 25.0, + fontWeight: FontWeight.w200, + letterSpacing: 0.9 + ) + ); + + Widget subTitleWidget(BuildContext context) => Text( + subTitle, + textAlign: TextAlign.center, + maxLines: 2, + style: TextStyle( + color: Theme.of(context).primaryColor, + fontSize: 15.0, + fontWeight: FontWeight.normal, + letterSpacing: 0.9 + ) + ); + + + Widget actionWidget(BuildContext context) => Container( + height: buttonHeight, + width: buttonWidth, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + + shape:RoundedRectangleBorder( + borderRadius: new BorderRadius.circular(buttonHeight/2), + ) + ), + child: Text( + actionTitle, + style: TextStyle( + color: Colors.white, + fontSize: 18.0, + fontWeight: FontWeight.normal, + ) + ), + onPressed: (){ + Navigator.of(context).pop(); + }, + ), + ); + +} diff --git a/lib/routes.dart b/lib/routes.dart index 57452796..51aaccfe 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -12,6 +12,7 @@ import 'package:diplomaticquarterapp/pages/login/login.dart'; import 'package:diplomaticquarterapp/pages/login/register.dart'; import 'package:diplomaticquarterapp/pages/packages_offers/OfferAndPackagesCartPage.dart'; import 'package:diplomaticquarterapp/pages/packages_offers/OfferAndPackagesPage.dart'; +import 'package:diplomaticquarterapp/pages/packages_offers/PackageOrderCompletedPage.dart'; import 'package:diplomaticquarterapp/pages/settings/settings.dart'; import 'package:diplomaticquarterapp/pages/symptom-checker/info.dart'; import 'package:diplomaticquarterapp/pages/symptom-checker/select-gender.dart'; @@ -39,6 +40,7 @@ const String SELECT_GENDER = 'select-gender'; const String SETTINGS = 'settings'; const String PACKAGES_OFFERS = 'packages-offers'; const String PACKAGES_OFFERS_CART = 'packages-offers-cart'; +const String PACKAGES_ORDER_COMPLETED = 'packages-offers-cart'; var routes = { SPLASH: (_) => SplashScreen(), @@ -60,4 +62,5 @@ var routes = { SETTINGS: (_) => Settings(), PACKAGES_OFFERS: (_) => PackagesHomePage(), PACKAGES_OFFERS_CART: (_) => PackagesCartPage(), + PACKAGES_ORDER_COMPLETED: (_) => PackageOrderCompletedPage(), }; diff --git a/lib/widgets/AnimatedTextFields.dart b/lib/widgets/AnimatedTextFields.dart new file mode 100644 index 00000000..b4ad5f65 --- /dev/null +++ b/lib/widgets/AnimatedTextFields.dart @@ -0,0 +1,347 @@ +import 'dart:math'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +enum TextFieldInertiaDirection { + left, + right, +} + +Interval _getInternalInterval( + double start, + double end, + double externalStart, + double externalEnd, [ + Curve curve = Curves.linear, + ]) { + return Interval( + start + (end - start) * externalStart, + start + (end - start) * externalEnd, + curve: curve, + ); +} + +class AnimatedTextFormField extends StatefulWidget { + AnimatedTextFormField({ + Key key, + this.interval = const Interval(0.0, 1.0), + @required this.width, + this.loadingController, + this.inertiaController, + this.inertiaDirection, + this.enabled = true, + this.labelText, + this.prefixIcon, + this.suffixIcon, + this.keyboardType, + this.textInputAction, + this.obscureText = false, + this.controller, + this.focusNode, + this.validator, + this.onFieldSubmitted, + this.onSaved, + }) : assert((inertiaController == null && inertiaDirection == null) || + (inertiaController != null && inertiaDirection != null)), + super(key: key); + + final Interval interval; + final AnimationController loadingController; + final AnimationController inertiaController; + final double width; + final bool enabled; + final String labelText; + final Widget prefixIcon; + final Widget suffixIcon; + final TextInputType keyboardType; + final TextInputAction textInputAction; + final bool obscureText; + final TextEditingController controller; + final FocusNode focusNode; + final FormFieldValidator validator; + final ValueChanged onFieldSubmitted; + final FormFieldSetter onSaved; + final TextFieldInertiaDirection inertiaDirection; + + @override + _AnimatedTextFormFieldState createState() => _AnimatedTextFormFieldState(); +} + +class _AnimatedTextFormFieldState extends State { + Animation scaleAnimation; + Animation sizeAnimation; + Animation suffixIconOpacityAnimation; + + Animation fieldTranslateAnimation; + Animation iconRotationAnimation; + Animation iconTranslateAnimation; + + @override + void initState() { + super.initState(); + + widget.inertiaController?.addStatusListener(handleAnimationStatus); + + final interval = widget.interval; + final loadingController = widget.loadingController; + + if (loadingController != null) { + scaleAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: loadingController, + curve: _getInternalInterval( + 0, .2, interval.begin, interval.end, Curves.easeOutBack), + )); + suffixIconOpacityAnimation = + Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation( + parent: loadingController, + curve: _getInternalInterval(.65, 1.0, interval.begin, interval.end), + )); + _updateSizeAnimation(); + } + + final inertiaController = widget.inertiaController; + final inertiaDirection = widget.inertiaDirection; + final sign = inertiaDirection == TextFieldInertiaDirection.right ? 1 : -1; + + if (inertiaController != null) { + fieldTranslateAnimation = Tween( + begin: 0.0, + end: sign * 15.0, + ).animate(CurvedAnimation( + parent: inertiaController, + curve: Interval(0, .5, curve: Curves.easeOut), + reverseCurve: Curves.easeIn, + )); + iconRotationAnimation = + Tween(begin: 0.0, end: sign * pi / 12 /* ~15deg */) + .animate(CurvedAnimation( + parent: inertiaController, + curve: Interval(.5, 1.0, curve: Curves.easeOut), + reverseCurve: Curves.easeIn, + )); + iconTranslateAnimation = + Tween(begin: 0.0, end: 8.0).animate(CurvedAnimation( + parent: inertiaController, + curve: Interval(.5, 1.0, curve: Curves.easeOut), + reverseCurve: Curves.easeIn, + )); + } + } + + void _updateSizeAnimation() { + final interval = widget.interval; + final loadingController = widget.loadingController; + + sizeAnimation = Tween( + begin: 48.0, + end: widget.width, + ).animate(CurvedAnimation( + parent: loadingController, + curve: _getInternalInterval( + .2, 1.0, interval.begin, interval.end, Curves.linearToEaseOut), + reverseCurve: Curves.easeInExpo, + )); + } + + @override + void didUpdateWidget(AnimatedTextFormField oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.width != widget.width) { + _updateSizeAnimation(); + } + } + + @override + dispose() { + widget.inertiaController?.removeStatusListener(handleAnimationStatus); + super.dispose(); + } + + void handleAnimationStatus(status) { + if (status == AnimationStatus.completed) { + widget.inertiaController?.reverse(); + } + } + + Widget _buildInertiaAnimation(Widget child) { + if (widget.inertiaController == null) { + return child; + } + + return AnimatedBuilder( + animation: iconTranslateAnimation, + builder: (context, child) => Transform( + alignment: Alignment.center, + transform: Matrix4.identity() + ..translate(iconTranslateAnimation.value) + ..rotateZ(iconRotationAnimation.value), + child: child, + ), + child: child, + ); + } + + InputDecoration _getInputDecoration(ThemeData theme) { + return InputDecoration( + contentPadding: EdgeInsets.fromLTRB(0, 0, 0, 0), + border: OutlineInputBorder( + borderRadius: new BorderRadius.circular(10.0), + borderSide: new BorderSide(), + ), + labelText: widget.labelText, + prefixIcon: _buildInertiaAnimation(widget.prefixIcon), + suffixIcon: _buildInertiaAnimation(widget.loadingController != null + ? FadeTransition( + opacity: suffixIconOpacityAnimation, + child: widget.suffixIcon, + ) + : widget.suffixIcon), + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + Widget textField = TextFormField( + controller: widget.controller, + focusNode: widget.focusNode, + decoration: _getInputDecoration(theme), + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + obscureText: widget.obscureText, + onFieldSubmitted: widget.onFieldSubmitted, + onSaved: widget.onSaved, + validator: widget.validator, + enabled: widget.enabled, + ); + + if (widget.loadingController != null) { + textField = ScaleTransition( + scale: scaleAnimation, + child: AnimatedBuilder( + animation: sizeAnimation, + builder: (context, child) => ConstrainedBox( + constraints: BoxConstraints.tightFor(width: sizeAnimation.value), + child: child, + ), + child: textField, + ), + ); + } + + if (widget.inertiaController != null) { + textField = AnimatedBuilder( + animation: fieldTranslateAnimation, + builder: (context, child) => Transform.translate( + offset: Offset(fieldTranslateAnimation.value, 0), + child: child, + ), + child: textField, + ); + } + + return textField; + } +} + +class AnimatedPasswordTextFormField extends StatefulWidget { + AnimatedPasswordTextFormField({ + Key key, + this.interval = const Interval(0.0, 1.0), + @required this.animatedWidth, + this.loadingController, + this.inertiaController, + this.inertiaDirection, + this.enabled = true, + this.labelText, + this.keyboardType, + this.textInputAction, + this.controller, + this.focusNode, + this.validator, + this.onFieldSubmitted, + this.onSaved, + }) : assert((inertiaController == null && inertiaDirection == null) || + (inertiaController != null && inertiaDirection != null)), + super(key: key); + + final Interval interval; + final AnimationController loadingController; + final AnimationController inertiaController; + final double animatedWidth; + final bool enabled; + final String labelText; + final TextInputType keyboardType; + final TextInputAction textInputAction; + final TextEditingController controller; + final FocusNode focusNode; + final FormFieldValidator validator; + final ValueChanged onFieldSubmitted; + final FormFieldSetter onSaved; + final TextFieldInertiaDirection inertiaDirection; + + @override + _AnimatedPasswordTextFormFieldState createState() => + _AnimatedPasswordTextFormFieldState(); +} + +class _AnimatedPasswordTextFormFieldState + extends State { + var _obscureText = true; + + @override + Widget build(BuildContext context) { + return AnimatedTextFormField( + interval: widget.interval, + loadingController: widget.loadingController, + inertiaController: widget.inertiaController, + width: widget.animatedWidth, + enabled: widget.enabled, + labelText: widget.labelText, + prefixIcon: Icon(Icons.lock, size: 20), + suffixIcon: GestureDetector( + onTap: () => setState(() => _obscureText = !_obscureText), + dragStartBehavior: DragStartBehavior.down, + child: AnimatedCrossFade( + duration: const Duration(milliseconds: 250), + firstCurve: Curves.easeInOutSine, + secondCurve: Curves.easeInOutSine, + alignment: Alignment.center, + layoutBuilder: (Widget topChild, _, Widget bottomChild, __) { + return Stack( + alignment: Alignment.center, + children: [bottomChild, topChild], + ); + }, + firstChild: Icon( + Icons.visibility, + size: 25.0, + semanticLabel: 'show password', + ), + secondChild: Icon( + Icons.visibility_off, + size: 25.0, + semanticLabel: 'hide password', + ), + crossFadeState: _obscureText + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + ), + ), + obscureText: _obscureText, + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + controller: widget.controller, + focusNode: widget.focusNode, + validator: widget.validator, + onFieldSubmitted: widget.onFieldSubmitted, + onSaved: widget.onSaved, + inertiaDirection: widget.inertiaDirection, + ); + } +} \ No newline at end of file diff --git a/lib/widgets/LoadingButton.dart b/lib/widgets/LoadingButton.dart new file mode 100644 index 00000000..8fef5271 --- /dev/null +++ b/lib/widgets/LoadingButton.dart @@ -0,0 +1,465 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'dart:math'; + +class AnimatedButton extends StatefulWidget { + AnimatedButton({ + Key key, + @required this.text, + @required this.onPressed, + @required this.controller, + this.textColor, + this.loadingColor, + this.color, + }) : super(key: key); + + final String text; + final Color color; + final Color textColor; + final Color loadingColor; + final Function onPressed; + final AnimationController controller; + + @override + _AnimatedButtonState createState() => _AnimatedButtonState(); +} + +class _AnimatedButtonState extends State + with SingleTickerProviderStateMixin { + Animation _sizeAnimation; + Animation _textOpacityAnimation; + Animation _buttonOpacityAnimation; + Animation _ringThicknessAnimation; + Animation _ringOpacityAnimation; + Animation _colorAnimation; + var _isLoading = false; + var _hover = false; + var _width = 120.0; + + Color _color; + Color _loadingColor; + + static const _height = 40.0; + static const _loadingCircleRadius = _height / 2; + static const _loadingCircleThickness = 4.0; + + @override + void initState() { + super.initState(); + + _textOpacityAnimation = Tween(begin: 1.0, end: 0.0).animate( + CurvedAnimation( + parent: widget.controller, + curve: Interval(0.0, .25), + ), + ); + + // _colorAnimation + // _width, _sizeAnimation + + _buttonOpacityAnimation = + Tween(begin: 1.0, end: 0.0).animate(CurvedAnimation( + parent: widget.controller, + curve: Threshold(.65), + )); + + _ringThicknessAnimation = + Tween(begin: _loadingCircleRadius, end: _loadingCircleThickness) + .animate(CurvedAnimation( + parent: widget.controller, + curve: Interval(.65, .85), + )); + _ringOpacityAnimation = + Tween(begin: 1.0, end: 0.0).animate(CurvedAnimation( + parent: widget.controller, + curve: Interval(.85, 1.0), + )); + + widget.controller.addStatusListener(handleStatusChanged); + } + + @override + void didChangeDependencies() { + _updateColorAnimation(); + _updateWidth(); + super.didChangeDependencies(); + } + + void _updateColorAnimation() { + final theme = Theme.of(context); + final buttonTheme = theme.floatingActionButtonTheme; + + _color = widget.color ?? buttonTheme.backgroundColor; + _loadingColor = widget.loadingColor ?? theme.accentColor; + + _colorAnimation = ColorTween( + begin: _color, + end: _loadingColor, + ).animate( + CurvedAnimation( + parent: widget.controller, + curve: const Interval(0.0, .65, curve: Curves.fastOutSlowIn), + ), + ); + } + + @override + void didUpdateWidget(AnimatedButton oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.color != widget.color || + oldWidget.loadingColor != widget.loadingColor) { + _updateColorAnimation(); + } + + if (oldWidget.text != widget.text) { + _updateWidth(); + } + } + + @override + void dispose() { + super.dispose(); + widget.controller.removeStatusListener(handleStatusChanged); + } + + void handleStatusChanged(status) { + if (status == AnimationStatus.forward) { + setState(() => _isLoading = true); + } + if (status == AnimationStatus.dismissed) { + setState(() => _isLoading = false); + } + } + + /// sets width and size animation + void _updateWidth() { + final theme = Theme.of(context); + final fontSize = theme.textTheme.button.fontSize; + final renderParagraph = RenderParagraph( + TextSpan( + text: widget.text, + style: TextStyle( + fontSize: fontSize, + fontWeight: theme.textTheme.button.fontWeight, + letterSpacing: theme.textTheme.button.letterSpacing, + ), + ), + textDirection: TextDirection.ltr, + maxLines: 1, + ); + + renderParagraph.layout(BoxConstraints(minWidth: 120.0)); + + // text width based on fontSize, plus 45.0 for padding + var textWidth = + renderParagraph.getMinIntrinsicWidth(fontSize).ceilToDouble() + 45.0; + + // button width is min 120.0 and max 240.0 + _width = textWidth > 120.0 && textWidth < 240.0 + ? textWidth + : textWidth >= 240.0 ? 240.0 : 120.0; + + _sizeAnimation = Tween(begin: 1.0, end: _height / _width) + .animate(CurvedAnimation( + parent: widget.controller, + curve: Interval(0.0, .65, curve: Curves.fastOutSlowIn), + )); + } + + Widget _buildButtonText(ThemeData theme) { + return FadeTransition( + opacity: _textOpacityAnimation, + child: AnimatedText( + text: widget.text, + style: TextStyle(color: widget.textColor ?? Colors.white), + ), + ); + } + + Widget _buildButton(ThemeData theme) { + final buttonTheme = theme.floatingActionButtonTheme; + + return FadeTransition( + opacity: _buttonOpacityAnimation, + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + child: AnimatedBuilder( + animation: _colorAnimation, + builder: (context, child) => Material( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(_height/2) + ), + color: _colorAnimation.value, + child: child, + shadowColor: _color, + elevation: !_isLoading + ? (_hover ? buttonTheme.highlightElevation : buttonTheme.elevation) + : 0, + ), + child: InkWell( + onTap: !_isLoading ? widget.onPressed : null, + splashColor: buttonTheme.splashColor, + customBorder: buttonTheme.shape, + onHighlightChanged: (value) => setState(() => _hover = value), + child: SizeTransition( + sizeFactor: _sizeAnimation, + axis: Axis.horizontal, + child: Container( + width: _width, + height: _height, + alignment: Alignment.center, + child: _buildButtonText(theme), + ), + ), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Stack( + alignment: Alignment.center, + children: [ + FadeTransition( + opacity: _ringOpacityAnimation, + child: AnimatedBuilder( + animation: _ringThicknessAnimation, + builder: (context, child) => Ring( + color: widget.loadingColor, + size: _height, + thickness: _ringThicknessAnimation.value, + ), + ), + ), + if (_isLoading) + SizedBox( + width: _height - _loadingCircleThickness, + height: _height - _loadingCircleThickness, + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(widget.loadingColor), + // backgroundColor: Colors.red, + strokeWidth: _loadingCircleThickness, + ), + ), + _buildButton(theme), + ], + ); + } +} + +class Ring extends StatelessWidget { + Ring({ + Key key, + this.color, + this.size = 40.0, + this.thickness = 2.0, + this.value = 1.0, + }) : assert(size - thickness > 0), + assert(thickness >= 0), + super(key: key); + + final Color color; + final double size; + final double thickness; + final double value; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: size - thickness, + height: size - thickness, + child: thickness == 0 + ? null + : CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(color), + strokeWidth: thickness, + value: value, + ), + ); + } +} + + +enum AnimatedTextRotation { up, down } + +/// https://medium.com/flutter-community/flutter-challenge-3d-bottom-navigation-bar-48952a5fd996 +class AnimatedText extends StatefulWidget { + AnimatedText({ + Key key, + @required this.text, + this.style, + this.textRotation = AnimatedTextRotation.up, + }) : super(key: key); + + final String text; + final TextStyle style; + final AnimatedTextRotation textRotation; + + @override + _AnimatedTextState createState() => _AnimatedTextState(); +} + +class _AnimatedTextState extends State + with SingleTickerProviderStateMixin { + var _newText = ''; + var _oldText = ''; + var _layoutHeight = 0.0; + final _textKey = GlobalKey(); + + Animation _animation; + AnimationController _controller; + + double get radius => _layoutHeight / 2; + + + @override + void initState() { + super.initState(); + + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 500), + ); + + _animation = Tween(begin: 0.0, end: pi / 2).animate(CurvedAnimation( + parent: _controller, + curve: Curves.easeOutBack, + )); + + _oldText = widget.text; + + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() => _layoutHeight = getWidgetSize(_textKey)?.height); + }); + } + + @override + void didUpdateWidget(AnimatedText oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.text != oldWidget.text) { + _oldText = oldWidget.text; + _newText = widget.text; + _controller.forward().then((_) { + setState(() { + final t = _oldText; + _oldText = _newText; + _newText = t; + }); + _controller.reset(); + }); + } + } + + @override + void dispose() { + super.dispose(); + _controller.dispose(); + } + + Matrix4 get _matrix { + // Fix: The text is not centered after applying perspective effect in the web build. Idk why + if (kIsWeb) { + return Matrix4.identity(); + } + return Matrix4.identity()..setEntry(3, 2, .006); + } + + Matrix4 _getFrontSideUp(double value) { + return _matrix + ..translate( + 0.0, + -radius * sin(_animation.value), + -radius * cos(_animation.value), + ) + ..rotateX(-_animation.value); // 0 -> -pi/2 + } + + Matrix4 _getBackSideUp(double value) { + return _matrix + ..translate( + 0.0, + radius * cos(_animation.value), + -radius * sin(_animation.value), + ) + ..rotateX((pi / 2) - _animation.value); // pi/2 -> 0 + } + + Matrix4 _getFrontSideDown(double value) { + return _matrix + ..translate( + 0.0, + radius * sin(_animation.value), + -radius * cos(_animation.value), + ) + ..rotateX(_animation.value); // 0 -> pi/2 + } + + Matrix4 _getBackSideDown(double value) { + return _matrix + ..translate( + 0.0, + -radius * cos(_animation.value), + -radius * sin(_animation.value), + ) + ..rotateX(_animation.value - pi / 2); // -pi/2 -> 0 + } + + @override + Widget build(BuildContext context) { + final rollUp = widget.textRotation == AnimatedTextRotation.up; + final oldText = Text( + _oldText, + key: _textKey, + style: widget.style, + overflow: TextOverflow.visible, + softWrap: false, + ); + final newText = Text( + _newText, + style: widget.style, + overflow: TextOverflow.visible, + softWrap: false, + ); + + return AnimatedBuilder( + animation: _animation, + builder: (context, child) => Stack( + alignment: Alignment.center, + children: [ + if (_animation.value <= toRadian(85)) + Transform( + alignment: Alignment.center, + transform: rollUp + ? _getFrontSideUp(_animation.value) + : _getFrontSideDown(_animation.value), + child: oldText, + ), + if (_animation.value >= toRadian(5)) + Transform( + alignment: Alignment.center, + transform: rollUp + ? _getBackSideUp(_animation.value) + : _getBackSideDown(_animation.value), + child: newText, + ), + ], + ), + ); + } + + +// Helpers + double toRadian(double degree) => degree * pi / 180; + double lerp(double start, double end, double percent) => (start + percent * (end - start)); + Size getWidgetSize(GlobalKey key) { + final RenderBox renderBox = key.currentContext?.findRenderObject(); + return renderBox?.size; + } +} \ No newline at end of file diff --git a/lib/widgets/TextFieldInertiaDirection.java b/lib/widgets/TextFieldInertiaDirection.java new file mode 100644 index 00000000..627bf406 --- /dev/null +++ b/lib/widgets/TextFieldInertiaDirection.java @@ -0,0 +1,343 @@ +import 'dart:math'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +enum TextFieldInertiaDirection { + left, + right, +} + +Interval _getInternalInterval( + double start, + double end, + double externalStart, + double externalEnd, [ + Curve curve = Curves.linear, +]) { + return Interval( + start + (end - start) * externalStart, + start + (end - start) * externalEnd, + curve: curve, + ); +} + +class AnimatedTextFormField extends StatefulWidget { + AnimatedTextFormField({ + Key key, + this.interval = const Interval(0.0, 1.0), + @required this.width, + this.loadingController, + this.inertiaController, + this.inertiaDirection, + this.enabled = true, + this.labelText, + this.prefixIcon, + this.suffixIcon, + this.keyboardType, + this.textInputAction, + this.obscureText = false, + this.controller, + this.focusNode, + this.validator, + this.onFieldSubmitted, + this.onSaved, + }) : assert((inertiaController == null && inertiaDirection == null) || + (inertiaController != null && inertiaDirection != null)), + super(key: key); + + final Interval interval; + final AnimationController loadingController; + final AnimationController inertiaController; + final double width; + final bool enabled; + final String labelText; + final Widget prefixIcon; + final Widget suffixIcon; + final TextInputType keyboardType; + final TextInputAction textInputAction; + final bool obscureText; + final TextEditingController controller; + final FocusNode focusNode; + final FormFieldValidator validator; + final ValueChanged onFieldSubmitted; + final FormFieldSetter onSaved; + final TextFieldInertiaDirection inertiaDirection; + + @override + _AnimatedTextFormFieldState createState() => _AnimatedTextFormFieldState(); +} + +class _AnimatedTextFormFieldState extends State { + Animation scaleAnimation; + Animation sizeAnimation; + Animation suffixIconOpacityAnimation; + + Animation fieldTranslateAnimation; + Animation iconRotationAnimation; + Animation iconTranslateAnimation; + + @override + void initState() { + super.initState(); + + widget.inertiaController?.addStatusListener(handleAnimationStatus); + + final interval = widget.interval; + final loadingController = widget.loadingController; + + if (loadingController != null) { + scaleAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: loadingController, + curve: _getInternalInterval( + 0, .2, interval.begin, interval.end, Curves.easeOutBack), + )); + suffixIconOpacityAnimation = + Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation( + parent: loadingController, + curve: _getInternalInterval(.65, 1.0, interval.begin, interval.end), + )); + _updateSizeAnimation(); + } + + final inertiaController = widget.inertiaController; + final inertiaDirection = widget.inertiaDirection; + final sign = inertiaDirection == TextFieldInertiaDirection.right ? 1 : -1; + + if (inertiaController != null) { + fieldTranslateAnimation = Tween( + begin: 0.0, + end: sign * 15.0, + ).animate(CurvedAnimation( + parent: inertiaController, + curve: Interval(0, .5, curve: Curves.easeOut), + reverseCurve: Curves.easeIn, + )); + iconRotationAnimation = + Tween(begin: 0.0, end: sign * pi / 12 /* ~15deg */) + .animate(CurvedAnimation( + parent: inertiaController, + curve: Interval(.5, 1.0, curve: Curves.easeOut), + reverseCurve: Curves.easeIn, + )); + iconTranslateAnimation = + Tween(begin: 0.0, end: 8.0).animate(CurvedAnimation( + parent: inertiaController, + curve: Interval(.5, 1.0, curve: Curves.easeOut), + reverseCurve: Curves.easeIn, + )); + } + } + + void _updateSizeAnimation() { + final interval = widget.interval; + final loadingController = widget.loadingController; + + sizeAnimation = Tween( + begin: 48.0, + end: widget.width, + ).animate(CurvedAnimation( + parent: loadingController, + curve: _getInternalInterval( + .2, 1.0, interval.begin, interval.end, Curves.linearToEaseOut), + reverseCurve: Curves.easeInExpo, + )); + } + + @override + void didUpdateWidget(AnimatedTextFormField oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.width != widget.width) { + _updateSizeAnimation(); + } + } + + @override + dispose() { + widget.inertiaController?.removeStatusListener(handleAnimationStatus); + super.dispose(); + } + + void handleAnimationStatus(status) { + if (status == AnimationStatus.completed) { + widget.inertiaController?.reverse(); + } + } + + Widget _buildInertiaAnimation(Widget child) { + if (widget.inertiaController == null) { + return child; + } + + return AnimatedBuilder( + animation: iconTranslateAnimation, + builder: (context, child) => Transform( + alignment: Alignment.center, + transform: Matrix4.identity() + ..translate(iconTranslateAnimation.value) + ..rotateZ(iconRotationAnimation.value), + child: child, + ), + child: child, + ); + } + + InputDecoration _getInputDecoration(ThemeData theme) { + return InputDecoration( + labelText: widget.labelText, + prefixIcon: _buildInertiaAnimation(widget.prefixIcon), + suffixIcon: _buildInertiaAnimation(widget.loadingController != null + ? FadeTransition( + opacity: suffixIconOpacityAnimation, + child: widget.suffixIcon, + ) + : widget.suffixIcon), + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + Widget textField = TextFormField( + controller: widget.controller, + focusNode: widget.focusNode, + decoration: _getInputDecoration(theme), + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + obscureText: widget.obscureText, + onFieldSubmitted: widget.onFieldSubmitted, + onSaved: widget.onSaved, + validator: widget.validator, + enabled: widget.enabled, + ); + + if (widget.loadingController != null) { + textField = ScaleTransition( + scale: scaleAnimation, + child: AnimatedBuilder( + animation: sizeAnimation, + builder: (context, child) => ConstrainedBox( + constraints: BoxConstraints.tightFor(width: sizeAnimation.value), + child: child, + ), + child: textField, + ), + ); + } + + if (widget.inertiaController != null) { + textField = AnimatedBuilder( + animation: fieldTranslateAnimation, + builder: (context, child) => Transform.translate( + offset: Offset(fieldTranslateAnimation.value, 0), + child: child, + ), + child: textField, + ); + } + + return textField; + } +} + +class AnimatedPasswordTextFormField extends StatefulWidget { + AnimatedPasswordTextFormField({ + Key key, + this.interval = const Interval(0.0, 1.0), + @required this.animatedWidth, + this.loadingController, + this.inertiaController, + this.inertiaDirection, + this.enabled = true, + this.labelText, + this.keyboardType, + this.textInputAction, + this.controller, + this.focusNode, + this.validator, + this.onFieldSubmitted, + this.onSaved, + }) : assert((inertiaController == null && inertiaDirection == null) || + (inertiaController != null && inertiaDirection != null)), + super(key: key); + + final Interval interval; + final AnimationController loadingController; + final AnimationController inertiaController; + final double animatedWidth; + final bool enabled; + final String labelText; + final TextInputType keyboardType; + final TextInputAction textInputAction; + final TextEditingController controller; + final FocusNode focusNode; + final FormFieldValidator validator; + final ValueChanged onFieldSubmitted; + final FormFieldSetter onSaved; + final TextFieldInertiaDirection inertiaDirection; + + @override + _AnimatedPasswordTextFormFieldState createState() => + _AnimatedPasswordTextFormFieldState(); +} + +class _AnimatedPasswordTextFormFieldState + extends State { + var _obscureText = true; + + @override + Widget build(BuildContext context) { + return AnimatedTextFormField( + interval: widget.interval, + loadingController: widget.loadingController, + inertiaController: widget.inertiaController, + width: widget.animatedWidth, + enabled: widget.enabled, + labelText: widget.labelText, + prefixIcon: Icon(FontAwesomeIcons.lock, size: 20), + suffixIcon: GestureDetector( + onTap: () => setState(() => _obscureText = !_obscureText), + dragStartBehavior: DragStartBehavior.down, + child: AnimatedCrossFade( + duration: const Duration(milliseconds: 250), + firstCurve: Curves.easeInOutSine, + secondCurve: Curves.easeInOutSine, + alignment: Alignment.center, + layoutBuilder: (Widget topChild, _, Widget bottomChild, __) { + return Stack( + alignment: Alignment.center, + children: [bottomChild, topChild], + ); + }, + firstChild: Icon( + Icons.visibility, + size: 25.0, + semanticLabel: 'show password', + ), + secondChild: Icon( + Icons.visibility_off, + size: 25.0, + semanticLabel: 'hide password', + ), + crossFadeState: _obscureText + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + ), + ), + obscureText: _obscureText, + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + controller: widget.controller, + focusNode: widget.focusNode, + validator: widget.validator, + onFieldSubmitted: widget.onFieldSubmitted, + onSaved: widget.onSaved, + inertiaDirection: widget.inertiaDirection, + ); + } +} \ No newline at end of file diff --git a/lib/widgets/in_app_browser/InAppBrowser.dart b/lib/widgets/in_app_browser/InAppBrowser.dart index 603c5108..5654411c 100644 --- a/lib/widgets/in_app_browser/InAppBrowser.dart +++ b/lib/widgets/in_app_browser/InAppBrowser.dart @@ -1,13 +1,26 @@ import 'dart:convert'; +import 'package:diplomaticquarterapp/config/config.dart'; import 'package:diplomaticquarterapp/config/shared_pref_kay.dart'; import 'package:diplomaticquarterapp/models/Appointments/AppoimentAllHistoryResultList.dart'; import 'package:diplomaticquarterapp/models/Authentication/authenticated_user.dart'; import 'package:diplomaticquarterapp/services/authentication/auth_provider.dart'; import 'package:diplomaticquarterapp/uitl/app_shared_preferences.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +enum _PAYMENT_TYPE{PACKAGES, PHARMACY, PATIENT} +var _InAppBrowserOptions = InAppBrowserClassOptions( + inAppWebViewGroupOptions: InAppWebViewGroupOptions(crossPlatform: InAppWebViewOptions(useShouldOverrideUrlLoading: true)), + crossPlatform: InAppBrowserOptions(hideUrlBar: true), + ios: IOSInAppBrowserOptions(toolbarBottom: false,) +); + class MyInAppBrowser extends InAppBrowser { + _PAYMENT_TYPE paymentType; + static String SERVICE_URL = 'https://hmgwebservices.com/PayFortWeb/pages/SendPayFortRequest.aspx'; // Payfort Payment Gateway URL UAT @@ -20,6 +33,11 @@ class MyInAppBrowser extends InAppBrowser { // static String PREAUTH_SERVICE_URL = // 'https://hmgwebservices.com/PayFortWebLive/pages/SendPayFortRequest.aspx'; //Payfort PreAuth Payment Gateway URL Live Store + // Packages + static String PACKAGES_REQUEST_PAYMENT_URL = '$EXA_CART_API_BASE_URL/checkout/OpcCompleteRedirectionPayment1'; + static String PACKAGES_PAYMENT_SUCCESS_URL = '$EXA_CART_API_BASE_URL/Checkout/MobilePaymentSuccess'; + static String PACKAGES_PAYMENT_FAIL_URL = '$EXA_CART_API_BASE_URL/Checkout/MobilePaymentFailed'; + static List successURLS = [ 'success', 'PayFortResponse', @@ -30,6 +48,7 @@ class MyInAppBrowser extends InAppBrowser { final Function onExitCallback; final Function onLoadStartCallback; + final BuildContext context; AppSharedPreferences sharedPref = AppSharedPreferences(); AuthProvider authProvider = new AuthProvider(); @@ -45,7 +64,7 @@ class MyInAppBrowser extends InAppBrowser { static bool isPaymentDone = false; - MyInAppBrowser({this.onExitCallback, this.appo, this.onLoadStartCallback}); + MyInAppBrowser({this.onExitCallback, this.appo, this.onLoadStartCallback, this.context}); Future onBrowserCreated() async { print("\n\nBrowser Created!\n\n"); @@ -53,7 +72,8 @@ class MyInAppBrowser extends InAppBrowser { @override Future onLoadStart(String url) async { - onLoadStartCallback(url); + if(onLoadStartCallback != null) + onLoadStartCallback(url); } @override @@ -74,13 +94,20 @@ class MyInAppBrowser extends InAppBrowser { @override void onExit() { print("\n\nBrowser closed!\n\n"); - onExitCallback(appo, isPaymentDone); + if(onExitCallback != null) + onExitCallback(appo, isPaymentDone); } @override - Future shouldOverrideUrlLoading( - ShouldOverrideUrlLoadingRequest shouldOverrideUrlLoadingRequest) async { - print("\n\n override ${shouldOverrideUrlLoadingRequest.url}\n\n"); + Future shouldOverrideUrlLoading(ShouldOverrideUrlLoadingRequest shouldOverrideUrlLoadingRequest) async { + var url = shouldOverrideUrlLoadingRequest.url; + debugPrint("redirecting/overriding to: $url"); + + if(paymentType == _PAYMENT_TYPE.PACKAGES && [PACKAGES_PAYMENT_SUCCESS_URL,PACKAGES_PAYMENT_FAIL_URL].contains(url)){ + isPaymentDone = (url == PACKAGES_PAYMENT_SUCCESS_URL); + close(); + } + return ShouldOverrideUrlLoadingAction.ALLOW; } @@ -106,6 +133,12 @@ class MyInAppBrowser extends InAppBrowser { } } + openPackagesPaymentBrowser({@required int customer_id, @required int order_id}){ + paymentType = _PAYMENT_TYPE.PACKAGES; + var full_url = '$PACKAGES_REQUEST_PAYMENT_URL?customer_id=$customer_id&order_id=$order_id'; + this.openUrl(url: full_url, options: _InAppBrowserOptions); + } + openPaymentBrowser( double amount, String orderDesc, @@ -142,13 +175,15 @@ class MyInAppBrowser extends InAppBrowser { clinicID, doctorID) .then((value) { - this.browser.openUrl(url: value); + + paymentType = _PAYMENT_TYPE.PATIENT; + this.browser.openUrl(url: value, options: _InAppBrowserOptions); }); } openBrowser(String url) { this.browser = browser; - this.browser.openUrl(url: url); + this.browser.openUrl(url: url, options: _InAppBrowserOptions); } Future generateURL( @@ -311,4 +346,4 @@ class MyChromeSafariBrowser extends ChromeSafariBrowser { void onClosed() { print("ChromeSafari browser closed"); } -} +} \ No newline at end of file diff --git a/lib/widgets/offers_packages/PackagesCartItemCard.dart b/lib/widgets/offers_packages/PackagesCartItemCard.dart index 72ce7475..532c132d 100644 --- a/lib/widgets/offers_packages/PackagesCartItemCard.dart +++ b/lib/widgets/offers_packages/PackagesCartItemCard.dart @@ -12,7 +12,7 @@ import 'package:flutter/material.dart'; bool wide = true; class PackagesCartItemCard extends StatefulWidget { - final CartProductResponseModel itemModel; + final PackagesCartItemsResponseModel itemModel; final StepperCallbackFuture shouldStepperChangeApply ; const PackagesCartItemCard( diff --git a/lib/widgets/offers_packages/PackagesOfferCard.dart b/lib/widgets/offers_packages/PackagesOfferCard.dart index af0341c5..c0a1a22a 100644 --- a/lib/widgets/offers_packages/PackagesOfferCard.dart +++ b/lib/widgets/offers_packages/PackagesOfferCard.dart @@ -30,6 +30,7 @@ class PackagesItemCard extends StatefulWidget { } class PackagesItemCardState extends State { + imageUrl() => widget.itemModel.images.isNotEmpty ? widget.itemModel.images.first.src : "https://wallpaperaccess.com/full/30103.jpg"; @override Widget build(BuildContext context) { @@ -57,8 +58,7 @@ class PackagesItemCardState extends State { child: ClipRRect( borderRadius: BorderRadius.circular(10), child: Utils.loadNetworkImage( - url: - "https://wallpaperaccess.com/full/30103.jpg", + url: imageUrl(), )), )), Text( diff --git a/lib/widgets/others/app_scaffold_widget.dart b/lib/widgets/others/app_scaffold_widget.dart index af61052e..9c9cbcf9 100644 --- a/lib/widgets/others/app_scaffold_widget.dart +++ b/lib/widgets/others/app_scaffold_widget.dart @@ -1,3 +1,4 @@ +import 'package:badges/badges.dart'; import 'package:diplomaticquarterapp/config/config.dart'; import 'package:diplomaticquarterapp/config/size_config.dart'; import 'package:diplomaticquarterapp/config/size_config.dart'; @@ -56,6 +57,7 @@ class AppScaffold extends StatelessWidget { AuthenticatedUserObject authenticatedUserObject = locator(); + AppBarWidget appBar; AppScaffold( {@required this.body, this.appBarTitle = '', @@ -92,7 +94,7 @@ class AppScaffold extends StatelessWidget { backgroundColor: backgroundColor ?? Theme.of(context).scaffoldBackgroundColor, appBar: isShowAppBar - ? AppBarWidget( + ? appBar = AppBarWidget( appBarTitle: appBarTitle, appBarIcons: appBarIcons, showHomeAppBarIcon: showHomeAppBarIcon, @@ -104,20 +106,23 @@ class AppScaffold extends StatelessWidget { ) : null, bottomSheet: bottomSheet, - body: (!Provider.of(context, listen: false).isLogin && - isShowDecPage) - ? NotAutPage( - title: title ?? appBarTitle, - description: description, - infoList: infoList, - imagesInfo: imagesInfo, - ) - : baseViewModel != null - ? NetworkBaseView( - child: body, - baseViewModel: baseViewModel, - ) - : body, + body: SafeArea( + top: true, bottom: true, + child: (!Provider.of(context, listen: false).isLogin && + isShowDecPage) + ? NotAutPage( + title: title ?? appBarTitle, + description: description, + infoList: infoList, + imagesInfo: imagesInfo, + ) + : baseViewModel != null + ? NetworkBaseView( + child: body, + baseViewModel: baseViewModel, + ) + : body, + ), floatingActionButton: floatingActionButton, ); } @@ -127,7 +132,7 @@ class AppScaffold extends StatelessWidget { } } -class AppBarWidget extends StatelessWidget with PreferredSizeWidget { +class AppBarWidget extends StatefulWidget with PreferredSizeWidget { final AuthenticatedUserObject authenticatedUserObject = locator(); @@ -140,6 +145,8 @@ class AppBarWidget extends StatelessWidget with PreferredSizeWidget { final bool showOfferPackagesCart; final bool isShowDecPage; + Function(String) badgeUpdater; + AppBarWidget( {this.appBarTitle, this.showHomeAppBarIcon, @@ -150,23 +157,40 @@ class AppBarWidget extends StatelessWidget with PreferredSizeWidget { this.showOfferPackagesCart = false, this.isShowDecPage = true}); + @override + State createState() => AppBarWidgetState(); + + @override + Size get preferredSize => Size(double.maxFinite, 60); +} + +class AppBarWidgetState extends State{ + + String badgeText = "0"; @override Widget build(BuildContext context) { + widget.badgeUpdater = badgeUpdateBlock; return buildAppBar(context); } + badgeUpdateBlock(String value){ + setState(() { + badgeText = value; + }); + } + Widget buildAppBar(BuildContext context) { ProjectViewModel projectViewModel = Provider.of(context); return AppBar( elevation: 0, backgroundColor: - isPharmacy ? Colors.green : Theme.of(context).appBarTheme.color, + widget.isPharmacy ? Colors.green : Theme.of(context).appBarTheme.color, textTheme: TextTheme( headline6: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), title: Text( - authenticatedUserObject.isLogin || !isShowDecPage - ? appBarTitle.toUpperCase() + widget.authenticatedUserObject.isLogin || !widget.isShowDecPage + ? widget.appBarTitle.toUpperCase() : TranslationBase.of(context).serviceInformationTitle, style: TextStyle( fontWeight: FontWeight.bold, @@ -179,18 +203,31 @@ class AppBarWidget extends StatelessWidget with PreferredSizeWidget { ), centerTitle: true, actions: [ - (isPharmacy && showPharmacyCart) + (widget.isPharmacy && widget.showPharmacyCart) ? IconButton( - icon: Icon(Icons.shopping_cart), + icon: Badge( + badgeContent: Text( + badgeText + ), + child: Icon(Icons.shopping_cart) + ), color: Colors.white, onPressed: () { Navigator.of(context).popUntil(ModalRoute.withName('/')); }) : Container(), - (isOfferPackages && showOfferPackagesCart) + (widget.isOfferPackages && widget.showOfferPackagesCart) ? IconButton( - icon: Icon(Icons.shopping_cart), + icon: Badge( + + position: BadgePosition.topStart(top: -15,start: -10), + badgeContent: Text( + badgeText, + style: TextStyle(fontSize: 9,color: Colors.white, fontWeight: FontWeight.normal), + ), + child: Icon(Icons.shopping_cart) + ), color: Colors.white, onPressed: () { // Cart Click Event @@ -200,7 +237,7 @@ class AppBarWidget extends StatelessWidget with PreferredSizeWidget { }) : Container(), - if (showHomeAppBarIcon) + if (widget.showHomeAppBarIcon) IconButton( icon: Icon(FontAwesomeIcons.home), color: Colors.white, @@ -208,18 +245,16 @@ class AppBarWidget extends StatelessWidget with PreferredSizeWidget { Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (context) => LandingPage()), - (Route r) => false); + (Route r) => false); // Cart Click Event if(_onCartClick != null) _onCartClick(); }, ), - if (appBarIcons != null) ...appBarIcons + if (widget.appBarIcons != null) ...widget.appBarIcons ], ); } - @override - Size get preferredSize => Size(double.maxFinite, 60); }