v5.7.0 - updates

This commit is contained in:
Anthony 2022-01-29 12:52:05 +00:00
parent 82b3653c6d
commit 157c1a6e96
49 changed files with 2072 additions and 451 deletions

View File

@ -1,3 +1,13 @@
## [5.7.0] - 2022-01-29
* Refactor product detail page
* View HTML in description on the product detail page
* Allow upsell products to be viewed on the Product detail page (if enabled)
* Allow related products to be viewed on the Product detail page (if enabled)
* Allow product reviews to be view on the product page (if enabled)
* Flutter format in /resources
* Pubspec.yaml dependency updates
## [5.6.2] - 2022-01-07
* Fix null return in CheckoutShippingTypeWidget

View File

@ -4,7 +4,7 @@
# WooCommerce App: Label StoreMax
### Label StoreMax - v5.6.2
### Label StoreMax - v5.7.0
[Official WooSignal WooCommerce App](https://woosignal.com)

View File

@ -210,5 +210,14 @@
"This product has been removed from your wishlist": "Dieses Produkt wurde von Ihrer Wunschliste entfernt",
"This product has been added to your wishlist": "Dieses Produkt wurde Ihrer Wunschliste hinzugefügt",
"Spend a minimum of minimumAmount to redeem": "Verbringen Sie mindestens {{minimumAmount}} einlösen",
"Spend less than maximumAmount to redeem": "Geben Sie weniger aus als {{maximumAmount}} einlösen"
"Spend less than maximumAmount to redeem": "Geben Sie weniger aus als {{maximumAmount}} einlösen",
"Related products": "Verwandte Produkte",
"Reviews": "Bewertungen",
"There are no reviews yet.": "Es gibt noch keine Bewertungen.",
"More": "Mehr",
"You may also like": "Sie können auch mögen",
"Leave a review": "Hinterlassen Sie eine Bewertung",
"How would you rate": "Wie beurteilen Sie",
"Submit": "einreichen",
"Your review has been submitted": "Ihre Bewertung wurde übermittelt"
}

View File

@ -210,5 +210,14 @@
"Wishlist": "Wishlist",
"No items found": "No items found",
"This product has been removed from your wishlist": "This product has been removed from your wishlist",
"This product has been added to your wishlist": "This product has been added to your wishlist"
"This product has been added to your wishlist": "This product has been added to your wishlist",
"Related products": "Related products",
"Reviews": "Reviews",
"There are no reviews yet.": "There are no reviews yet.",
"More": "More",
"You may also like": "You may also like",
"Leave a review": "Leave a review",
"How would you rate": "How would you rate",
"Submit": "Submit",
"Your review has been submitted": "Your review has been submitted"
}

View File

@ -210,5 +210,14 @@
"This product has been removed from your wishlist": "Este producto ha sido eliminado de tu lista de deseos.",
"This product has been added to your wishlist": "Este producto ha sido añadido a su lista de deseos",
"Spend a minimum of minimumAmount to redeem": "Gasta un mínimo de {{minimumAmount}} para redimir",
"Spend less than maximumAmount to redeem": "Gasta menos de {{maximumAmount}} para redimir"
"Spend less than maximumAmount to redeem": "Gasta menos de {{maximumAmount}} para redimir",
"Related products": "Productos relacionados",
"Reviews": "Reseñas",
"There are no reviews yet.": "Aún no hay reseñas.",
"More": "Más",
"You may also like": "También te puede interesar",
"Leave a review": "Dejar un comentario",
"How would you rate": "Cómo calificarías",
"Submit": "Entregar",
"Your review has been submitted": "Tu reseña ha sido enviada"
}

View File

@ -210,5 +210,14 @@
"This product has been removed from your wishlist": "Ce produit a été supprimé de votre liste de souhaits",
"This product has been added to your wishlist": "Ce produit a été ajouté à votre liste de souhaits",
"Spend a minimum of minimumAmount to redeem": "Dépensez un minimum de {{minimumAmount}} de racheter",
"Spend less than maximumAmount to redeem": "Dépensez moins de {{maximumAmount}} de racheter"
"Spend less than maximumAmount to redeem": "Dépensez moins de {{maximumAmount}} de racheter",
"Related products": "Produits connexes",
"Reviews": "Commentaires",
"There are no reviews yet.": "Il n'y a pas encore de critiques.",
"More": "Suite",
"You may also like": "Tu pourrais aussi aimer",
"Leave a review": "Laisser un commentaire",
"How would you rate": "Comment évalueriez-vous",
"Submit": "Soumettre",
"Your review has been submitted": "Votre avis a été soumis"
}

View File

@ -210,5 +210,14 @@
"This product has been removed from your wishlist": "yah utpaad aapakee ichchha soochee se hata diya gaya hai",
"This product has been added to your wishlist": "is utpaad ko aapakee vish - list mein jod diya gaya hai",
"Spend a minimum of minimumAmount to redeem": "kam se kam kharch karen {{minimumAmount}} ke evaj mein lena",
"Spend less than maximumAmount to redeem": "se kam kharch karen {{maximumAmount}} ke evaj mein lena"
"Spend less than maximumAmount to redeem": "se kam kharch karen {{maximumAmount}} ke evaj mein lena",
"Related products": "sambandhit utpaad",
"Reviews": "sameeksha",
"There are no reviews yet.": "abhee tak koee sameeksha nahin.",
"More": "adhik",
"You may also like": "aapako yah bhee pasand aa sakata hain",
"Leave a review": "sameeksha likhen",
"How would you rate": "aap ise kya ret karate hain",
"Submit": "prastut karana",
"Your review has been submitted": "aapakee sameeksha jama ho chukee hai"
}

View File

@ -210,5 +210,14 @@
"This product has been removed from your wishlist": "Questo prodotto è stato rimosso dalla tua lista dei desideri",
"This product has been added to your wishlist": "Questo prodotto è stato aggiunto alla tua lista dei desideri",
"Spend a minimum of minimumAmount to redeem": "Spendi un minimo di {{minimumAmount}} riscattare",
"Spend less than maximumAmount to redeem": "Spendi meno di {{maximumAmount}} riscattare"
"Spend less than maximumAmount to redeem": "Spendi meno di {{maximumAmount}} riscattare",
"Related products": "Prodotti correlati",
"Reviews": "Recensioni",
"There are no reviews yet.": "Non ci sono ancora recensioni.",
"More": "Di più",
"You may also like": "Potrebbe piacerti anche",
"Leave a review": "Lascia una recensione",
"How would you rate": "Come valuteresti",
"Submit": "Invia",
"Your review has been submitted": "La tua recensione è stata inviata"
}

View File

@ -210,5 +210,14 @@
"This product has been removed from your wishlist": "Dit product is van je verlanglijst verwijderd",
"This product has been added to your wishlist": "Dit product is toegevoegd aan je verlanglijst",
"Spend a minimum of minimumAmount to redeem": "Besteed minimaal € {{minimumAmount}} verlossen",
"Spend less than maximumAmount to redeem": "Minder uitgeven dan {{maximumAmount}} verlossen"
"Spend less than maximumAmount to redeem": "Minder uitgeven dan {{maximumAmount}} verlossen",
"Related products": "Gerelateerde producten",
"Reviews": "Beoordelingen",
"There are no reviews yet.": "Er zijn nog geen beoordelingen.",
"More": "Meer",
"You may also like": "Dit vind je misschien ook leuk",
"Leave a review": "Laat je mening achter",
"How would you rate": "Hoe zou jij beoordelen",
"Submit": "Indienen",
"Your review has been submitted": "Uw beoordeling is verzonden"
}

View File

@ -210,5 +210,14 @@
"This product has been removed from your wishlist": "Este produto foi removido da sua lista de desejos",
"This product has been added to your wishlist": "Este produto foi adicionado à sua lista de desejos",
"Spend a minimum of minimumAmount to redeem": "Gaste um mínimo de {{minimumAmount}} redimir",
"Spend less than maximumAmount to redeem": "Gaste menos que {{maximumAmount}} redimir"
"Spend less than maximumAmount to redeem": "Gaste menos que {{maximumAmount}} redimir",
"Related products": "Produtos relacionados",
"Reviews": "Avaliações",
"There are no reviews yet.": "Não há comentários ainda.",
"More": "Mais",
"You may also like": "você pode gostar",
"Leave a review": "Deixe um comentário",
"How would you rate": "Como você avaliaria",
"Submit": "Enviar",
"Your review has been submitted": "Sua avaliação foi enviada"
}

View File

@ -210,5 +210,14 @@
"This product has been removed from your wishlist": "Bu ürün istek listenizden kaldırıldı",
"This product has been added to your wishlist": "Bu ürün istek listenize eklendi",
"Spend a minimum of minimumAmount to redeem": "Minimum harcamak {{minimumAmount}} rehinden kurtarmak",
"Spend less than maximumAmount to redeem": "Şundan daha az harcayın: {{maximumAmount}} rehinden kurtarmak"
"Spend less than maximumAmount to redeem": "Şundan daha az harcayın: {{maximumAmount}} rehinden kurtarmak",
"Related products": "İlgili ürünler",
"Reviews": "incelemeler",
"There are no reviews yet.": "Henüz yorum yok.",
"More": "Daha",
"You may also like": "Şunlar da hoşunuza gidebilir",
"Leave a review": "İnceleme bırak",
"How would you rate": "Nasıl değerlendirirsiniz",
"Submit": "Göndermek",
"Your review has been submitted": "İncelemeniz gönderildi"
}

View File

@ -210,5 +210,14 @@
"This product has been removed from your wishlist": "该产品已从您的愿望清单中删除",
"This product has been added to your wishlist": "本产品已经被加入你的心愿单",
"Spend a minimum of minimumAmount to redeem": "至少花费 {{minimumAmount}} 赎回",
"Spend less than maximumAmount to redeem": "花费少于 {{maximumAmount}} 赎回"
"Spend less than maximumAmount to redeem": "花费少于 {{maximumAmount}} 赎回",
"Related products": "相关产品",
"Reviews": "评论",
"There are no reviews yet.": "还没有评论。",
"More": "更多的",
"You may also like": "你可能也会喜欢",
"Leave a review": "发表评论",
"How would you rate": "你如何评价",
"Submit": "提交",
"Your review has been submitted": "您的评论已提交"
}

View File

@ -0,0 +1,21 @@
// Label StoreMax
//
// Created by Anthony Gordon.
// 2022, WooSignal Ltd. All rights reserved.
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import 'controller.dart';
import 'package:flutter/widgets.dart';
class LeaveReviewController extends Controller {
@override
construct(BuildContext context) {
super.construct(context);
}
}

View File

@ -8,27 +8,110 @@
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import 'package:flutter/material.dart';
import 'package:flutter_app/app/models/cart.dart';
import 'package:flutter_app/app/models/cart_line_item.dart';
import 'package:flutter_app/bootstrap/enums/wishlist_action_enums.dart';
import 'package:flutter_app/bootstrap/helpers.dart';
import 'package:nylo_framework/nylo_framework.dart';
import 'package:woosignal/models/response/products.dart';
import 'package:woosignal/models/response/product_variation.dart'
as ws_product_variation;
import 'controller.dart';
import 'package:flutter/widgets.dart';
class ProductDetailController extends Controller {
int quantity = 1;
Product product;
@override
construct(BuildContext context) {
super.construct(context);
product = data() as Product;
}
viewExternalProduct(Product product) {
viewExternalProduct() {
if (product.externalUrl != null && product.externalUrl.isNotEmpty) {
openBrowserTab(url: product.externalUrl);
}
}
viewProductImages(int i, Product product) =>
Navigator.pushNamed(context, "/product-images", arguments: {
"index": i,
"images": product.images.map((f) => f.src).toList()
});
}
itemAddToCart({@required CartLineItem cartLineItem, @required Function onSuccess}) async {
await Cart.getInstance.addToCart(cartLineItem: cartLineItem);
showStatusAlert(context,
title: trans("Success"),
subtitle: trans("Added to cart"),
duration: 1,
icon: Icons.add_shopping_cart,
);
onSuccess();
}
addQuantityTapped({@required Function onSuccess}) {
if (product.manageStock != null && product.manageStock == true) {
if (quantity >= product.stockQuantity) {
showToastNotification(context,
title: trans("Maximum quantity reached"),
description:
"${trans("Sorry, only")} ${product.stockQuantity} ${trans("left")}",
style: ToastNotificationStyleType.INFO);
return;
}
}
if (quantity != 0) {
quantity++;
onSuccess();
}
}
removeQuantityTapped({@required Function onSuccess}) {
if ((quantity - 1) >= 1) {
quantity--;
onSuccess();
}
}
toggleWishList({@required Function onSuccess, @required WishlistAction wishlistAction}) async {
String subtitleMsg;
if (wishlistAction == WishlistAction.remove) {
await removeWishlistProduct(product: product);
subtitleMsg = trans("This product has been removed from your wishlist");
} else {
await saveWishlistProduct(product: product);
subtitleMsg = trans("This product has been added to your wishlist");
}
showStatusAlert(context,
title: trans("Success"),
subtitle: subtitleMsg,
icon: Icons.favorite,
duration: 1,
);
onSuccess();
}
ws_product_variation.ProductVariation findProductVariation(
{@required Map<int, dynamic> tmpAttributeObj,
@required List<ws_product_variation.ProductVariation> productVariations}) {
ws_product_variation.ProductVariation tmpProductVariation;
Map<String, dynamic> tmpSelectedObj = {};
for (var attributeObj in tmpAttributeObj.values) {
tmpSelectedObj[attributeObj["name"]] = attributeObj["value"];
}
for (var productVariation in productVariations) {
Map<String, dynamic> tmpVariations = {};
for (var attr in productVariation.attributes) {
tmpVariations[attr.name] = attr.option;
}
if (tmpVariations.toString() == tmpSelectedObj.toString()) {
tmpProductVariation = productVariation;
}
}
return tmpProductVariation;
}
}

View File

@ -18,6 +18,7 @@ class ProductLoaderController extends WooSignalApiLoaderController<Product> {
Future<void> loadProducts({
@required bool Function(bool hasProducts) hasResults,
@required void Function() didFinish,
List<int> productIds = const []
}) async {
await load(
hasResults: hasResults,
@ -25,6 +26,7 @@ class ProductLoaderController extends WooSignalApiLoaderController<Product> {
apiQuery: (api) => api.getProducts(
perPage: 50,
page: page,
include: productIds,
status: "publish",
stockStatus: "instock",
));

View File

@ -0,0 +1,22 @@
// Label StoreMax
//
// Created by Anthony Gordon.
// 2022, WooSignal Ltd. All rights reserved.
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import 'controller.dart';
import 'package:flutter/widgets.dart';
class ProductReviewsController extends Controller {
@override
construct(BuildContext context) {
super.construct(context);
}
}

View File

@ -0,0 +1,34 @@
// Label StoreMax
//
// Created by Anthony Gordon.
// 2022, WooSignal Ltd. All rights reserved.
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import 'package:flutter/cupertino.dart';
import 'package:flutter_app/app/controllers/woosignal_api_loader_controller.dart';
import 'package:woosignal/models/response/product_review.dart';
import 'package:woosignal/models/response/products.dart';
class ProductReviewsLoaderController extends WooSignalApiLoaderController<ProductReview> {
ProductReviewsLoaderController();
Future<void> loadProductReviews({
@required Product product,
@required bool Function(bool hasProducts) hasResults,
@required void Function() didFinish,
}) async {
await load(
hasResults: hasResults,
didFinish: didFinish,
apiQuery: (api) => api.getProductReviews(
product: [product.id],
perPage: 50,
page: page,
status: "approved",
));
}
}

View File

@ -0,0 +1,15 @@
// Label StoreMax
//
// Created by Anthony Gordon.
// 2022, WooSignal Ltd. All rights reserved.
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
enum WishlistAction {
add,
remove
}

View File

@ -99,7 +99,7 @@ List<PaymentType> getPaymentTypes() {
}
dynamic envVal(String envVal, {dynamic defaultValue}) =>
(getEnv(envVal) == null ? defaultValue : getEnv(envVal));
(getEnv(envVal) ?? defaultValue);
PaymentType addPayment(
{@required int id,
@ -372,7 +372,7 @@ navigatorPush(BuildContext context,
if (forgetAll) {
Navigator.of(context).pushNamedAndRemoveUntil(
routeName, (Route<dynamic> route) => false,
arguments: arguments ?? null);
arguments: arguments);
}
if (forgetLast != null) {
int count = 0;
@ -380,7 +380,7 @@ navigatorPush(BuildContext context,
return count++ == forgetLast;
});
}
Navigator.of(context).pushNamed(routeName, arguments: arguments ?? null);
Navigator.of(context).pushNamed(routeName, arguments: arguments);
}
PlatformDialogAction dialogAction(BuildContext context,
@ -554,6 +554,16 @@ Future<List<dynamic>> getWishlistProducts() async {
return favouriteProducts;
}
hasAddedWishlistProduct(int productId) async {
List<dynamic> favouriteProducts = await getWishlistProducts();
List<int> productIds =
favouriteProducts.map((e) => e['id']).cast<int>().toList();
if (productIds.isEmpty) {
return false;
}
return productIds.contains(productId);
}
saveWishlistProduct({@required Product product}) async {
List<dynamic> products = await getWishlistProducts();
if (products.any((wishListProduct) => wishListProduct['id'] == product.id) ==

View File

@ -17,7 +17,6 @@ import 'package:flutter_app/bootstrap/shared_pref/sp_auth.dart';
import 'package:flutter_app/resources/widgets/app_loader_widget.dart';
import 'package:flutter_app/resources/widgets/safearea_widget.dart';
import 'package:flutter_app/resources/widgets/woosignal_ui.dart';
import 'package:hexcolor/hexcolor.dart';
import 'package:nylo_framework/nylo_framework.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:woosignal/models/response/order.dart';
@ -343,7 +342,7 @@ class _AccountDetailPageState extends State<AccountDetailPage>
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: HexColor("#fcfcfc"),
color: Color(0xFFFCFCFC),
width: 1,
),
),

View File

@ -14,7 +14,6 @@ import 'package:flutter_app/bootstrap/helpers.dart';
import 'package:flutter_app/resources/widgets/app_loader_widget.dart';
import 'package:flutter_app/resources/widgets/safearea_widget.dart';
import 'package:flutter_app/resources/widgets/woosignal_ui.dart';
import 'package:hexcolor/hexcolor.dart';
import 'package:nylo_support/helpers/helper.dart';
import 'package:nylo_support/widgets/ny_state.dart';
import 'package:nylo_support/widgets/ny_stateful_widget.dart';
@ -63,12 +62,15 @@ class _AccountOrderDetailPageState extends NyState<AccountOrderDetailPage> {
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
"${trans("Date Ordered").capitalize()}: " +
dateFormatted(
date: _order.dateCreated,
formatType: formatForDateTime(FormatType.date),
),
Padding(
padding: EdgeInsets.only(top: 8),
child: Text(
"${trans("Date Ordered").capitalize()}: " +
dateFormatted(
date: _order.dateCreated,
formatType: formatForDateTime(FormatType.date),
),
),
),
Container(
margin: EdgeInsets.only(top: 10, bottom: 10),
@ -116,6 +118,7 @@ class _AccountOrderDetailPageState extends NyState<AccountOrderDetailPage> {
Expanded(
child: ListView.builder(
itemBuilder: (cxt, i) {
LineItems lineItem = _order.lineItems[i];
return Card(
child: ListTile(
contentPadding: EdgeInsets.only(
@ -124,7 +127,7 @@ class _AccountOrderDetailPageState extends NyState<AccountOrderDetailPage> {
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: HexColor("#fcfcfc"), width: 1),
color: Color(0xFFFCFCFC), width: 1),
),
),
child: Row(
@ -134,14 +137,13 @@ class _AccountOrderDetailPageState extends NyState<AccountOrderDetailPage> {
children: <Widget>[
Flexible(
child: Text(
_order.lineItems[i].name,
lineItem.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Text(
formatStringCurrency(
total: _order.lineItems[i].price)
formatStringCurrency(total: lineItem.price)
.capitalize(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
@ -163,7 +165,7 @@ class _AccountOrderDetailPageState extends NyState<AccountOrderDetailPage> {
children: <Widget>[
Text(
formatStringCurrency(
total: _order.lineItems[i].total,
total: lineItem.total,
),
style: Theme.of(context)
.textTheme
@ -174,7 +176,7 @@ class _AccountOrderDetailPageState extends NyState<AccountOrderDetailPage> {
textAlign: TextAlign.left,
),
Text(
"x${_order.lineItems[i].quantity.toString()}",
"x${lineItem.quantity.toString()}",
style: Theme.of(context)
.textTheme
.bodyText1,

View File

@ -131,7 +131,7 @@ class _CustomerCountriesPageState extends State<CustomerCountriesPage> {
}
_handleCountryTapped(DefaultShipping defaultShipping) {
if (defaultShipping.states.length > 0) {
if (defaultShipping.states.isNotEmpty) {
_handleStates(defaultShipping);
return;
}

View File

@ -0,0 +1,193 @@
// Label StoreMax
//
// Created by Anthony Gordon.
// 2022, WooSignal Ltd. All rights reserved.
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import 'package:flutter/material.dart';
import 'package:flutter_app/bootstrap/helpers.dart';
import 'package:flutter_app/bootstrap/shared_pref/sp_auth.dart';
import 'package:flutter_app/resources/widgets/app_loader_widget.dart';
import 'package:flutter_app/resources/widgets/buttons.dart';
import 'package:flutter_app/resources/widgets/safearea_widget.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:nylo_framework/nylo_framework.dart';
import 'package:woosignal/models/response/order.dart';
import 'package:woosignal/models/response/product_review.dart';
import 'package:wp_json_api/models/responses/wc_customer_info_response.dart'
as wc_customer_info;
import 'package:wp_json_api/wp_json_api.dart';
import '../../app/controllers/leave_review_controller.dart';
class LeaveReviewPage extends NyStatefulWidget {
final LeaveReviewController controller = LeaveReviewController();
LeaveReviewPage({Key key}) : super(key: key);
@override
_LeaveReviewPageState createState() => _LeaveReviewPageState();
}
class _LeaveReviewPageState extends NyState<LeaveReviewPage> {
LineItems _lineItem;
Order _order;
TextEditingController _textEditingController;
int _rating;
bool _isLoading = false;
@override
widgetDidLoad() async {
_lineItem = widget.controller.data()['line_item'] as LineItems;
_order = widget.controller.data()['order'] as Order;
_textEditingController = TextEditingController();
_rating = 5;
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(trans("Leave a review")),
centerTitle: true,
),
body: SafeAreaWidget(
child: _isLoading
? AppLoaderWidget()
: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(top: 16),
),
Text(
trans("How would you rate"),
style: Theme.of(context).textTheme.bodyText1,
),
Text(_lineItem.name),
Flexible(
child: Container(
child: TextField(
controller: _textEditingController,
style: Theme.of(context).textTheme.subtitle1,
keyboardType: TextInputType.text,
autocorrect: false,
autofocus: true,
obscureText: false,
textCapitalization: TextCapitalization.sentences,
),
),
),
Padding(
padding: EdgeInsets.only(bottom: 16),
),
RatingBar.builder(
initialRating: _rating.toDouble(),
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: false,
itemCount: 5,
itemPadding: EdgeInsets.symmetric(horizontal: 4.0),
itemBuilder: (context, _) =>
Icon(Icons.star, color: Colors.amber),
onRatingUpdate: (rating) {
_rating = rating.toInt();
},
),
Padding(
padding: EdgeInsets.only(bottom: 16),
),
PrimaryButton(title: trans("Submit"), action: _leaveReview),
],
)),
);
}
_leaveReview() async {
if (_isLoading == true) {
return;
}
setState(() {
_isLoading = true;
});
String review = _textEditingController.text;
wc_customer_info.Data wcCustomerInfo = await _fetchWpUserData();
if (wcCustomerInfo == null) {
showToastNotification(
context,
title: trans("Oops!"),
description: trans("Something went wrong"),
style: ToastNotificationStyleType.DANGER,
);
setState(() {
_isLoading = false;
});
return;
}
try {
validator(rules: {"review": "min:5"}, data: {"review": review});
ProductReview productReview =
await appWooSignal((api) => api.createProductReview(
productId: _lineItem.productId,
verified: true,
review: review,
status: "approved",
reviewer: [_order.billing.firstName, _order.billing.lastName]
.join(" "),
rating: _rating,
reviewerEmail: _order.billing.email,
));
if (productReview == null) {
showToastNotification(context,
title: trans("Oops"),
description: trans("Something went wrong"),
style: ToastNotificationStyleType.INFO);
return;
}
showToastNotification(context,
title: trans("Success"),
description: trans("Your review has been submitted"),
style: ToastNotificationStyleType.SUCCESS);
pop(result: _lineItem);
} on ValidationException catch (e) {
NyLogger.error(e.toString());
} finally {
setState(() {
_isLoading = false;
});
}
}
Future<wc_customer_info.Data> _fetchWpUserData() async {
String userToken = await readAuthToken();
wc_customer_info.WCCustomerInfoResponse wcCustomerInfoResponse;
try {
wcCustomerInfoResponse = await WPJsonAPI.instance
.api((request) => request.wcCustomerInfo(userToken));
if (wcCustomerInfoResponse == null) {
return null;
}
if (wcCustomerInfoResponse.status != 200) {
return null;
}
return wcCustomerInfoResponse.data;
} on Exception catch (_) {
return null;
}
}
}

View File

@ -10,20 +10,21 @@
import 'package:flutter/material.dart';
import 'package:flutter_app/app/controllers/product_detail_controller.dart';
import 'package:flutter_app/app/models/cart.dart';
import 'package:flutter_app/app/models/cart_line_item.dart';
import 'package:flutter_app/bootstrap/app_helper.dart';
import 'package:flutter_app/bootstrap/enums/wishlist_action_enums.dart';
import 'package:flutter_app/bootstrap/helpers.dart';
import 'package:flutter_app/resources/widgets/app_loader_widget.dart';
import 'package:flutter_app/resources/widgets/buttons.dart';
import 'package:flutter_app/resources/widgets/cached_image_widget.dart';
import 'package:flutter_app/resources/widgets/cart_icon_widget.dart';
import 'package:flutter_app/resources/widgets/future_build_widget.dart';
import 'package:flutter_app/resources/widgets/product_detail_body_widget.dart';
import 'package:flutter_app/resources/widgets/product_detail_footer_actions_widget.dart';
import 'package:flutter_app/resources/widgets/woosignal_ui.dart';
import 'package:nylo_framework/nylo_framework.dart';
import 'package:woosignal/models/response/product_variation.dart'
as ws_product_variation;
import 'package:woosignal/models/response/products.dart' as ws_product;
import 'package:flutter_swiper/flutter_swiper.dart';
import 'package:woosignal/models/response/woosignal_app.dart';
class ProductDetailPage extends NyStatefulWidget {
@ -36,10 +37,9 @@ class ProductDetailPage extends NyStatefulWidget {
}
class _ProductDetailState extends NyState<ProductDetailPage> {
bool _isLoading = false;
bool _isLoading = true;
ws_product.Product _product;
bool isInFavourites = false;
int _quantityIndicator = 1;
List<ws_product_variation.ProductVariation> _productVariations = [];
final Map<int, dynamic> _tmpAttributeObj = {};
final WooSignalApp _wooSignalApp = AppHelper.instance.appConfig;
@ -47,11 +47,13 @@ class _ProductDetailState extends NyState<ProductDetailPage> {
@override
widgetDidLoad() async {
_product = widget.controller.data();
if (_product.type == "variable") {
_isLoading = true;
await _fetchProductVariations();
return;
}
setState(() {
_isLoading = false;
});
}
_fetchProductVariations() async {
@ -80,29 +82,6 @@ class _ProductDetailState extends NyState<ProductDetailPage> {
});
}
ws_product_variation.ProductVariation findProductVariation() {
ws_product_variation.ProductVariation tmpProductVariation;
Map<String, dynamic> tmpSelectedObj = {};
for (var attributeObj in _tmpAttributeObj.values) {
tmpSelectedObj[attributeObj["name"]] = attributeObj["value"];
}
for (var productVariation in _productVariations) {
Map<String, dynamic> tmpVariations = {};
for (var attr in productVariation.attributes) {
tmpVariations[attr.name] = attr.option;
}
if (tmpVariations.toString() == tmpSelectedObj.toString()) {
tmpProductVariation = productVariation;
}
}
return tmpProductVariation;
}
_modalBottomSheetOptionsForAttribute(int attributeIndex) {
wsModalBottom(
context,
@ -137,19 +116,11 @@ class _ProductDetailState extends NyState<ProductDetailPage> {
);
}
_itemAddToCart({CartLineItem cartLineItem}) async {
await Cart.getInstance.addToCart(cartLineItem: cartLineItem);
showStatusAlert(context,
title: trans("Success"),
subtitle: trans("Added to cart"),
duration: 1,
icon: Icons.add_shopping_cart);
setState(() {});
}
_modalBottomSheetAttributes() {
ws_product_variation.ProductVariation productVariation =
findProductVariation();
ws_product_variation.ProductVariation productVariation = widget.controller
.findProductVariation(
tmpAttributeObj: _tmpAttributeObj,
productVariations: _productVariations);
wsModalBottom(
context,
title: trans("Options"),
@ -203,7 +174,7 @@ class _ProductDetailState extends NyState<ProductDetailPage> {
),
PrimaryButton(
title: trans("Add to cart"),
action: () {
action: () async {
if (_product.attributes.length !=
_tmpAttributeObj.values.length) {
showToastNotification(context,
@ -235,12 +206,16 @@ class _ProductDetailState extends NyState<ProductDetailPage> {
});
CartLineItem cartLineItem = CartLineItem.fromProductVariation(
quantityAmount: _quantityIndicator,
options: options,
product: _product,
productVariation: productVariation);
quantityAmount: widget.controller.quantity,
options: options,
product: _product,
productVariation: productVariation,
);
_itemAddToCart(cartLineItem: cartLineItem);
await widget.controller.itemAddToCart(
cartLineItem: cartLineItem,
onSuccess: () => setState(() => {}));
setState(() {});
Navigator.of(context).pop();
}),
],
@ -250,30 +225,28 @@ class _ProductDetailState extends NyState<ProductDetailPage> {
);
}
_modalBottomSheetMenu() {
wsModalBottom(
context,
title: trans("Description"),
bodyWidget: SingleChildScrollView(
child: Text(
parseHtmlString(_product.description),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: <Widget>[
if (_wooSignalApp.wishlistEnabled)
IconButton(
onPressed: _toggleWishList,
icon: isInFavourites
? Icon(Icons.favorite, color: Colors.red)
: Icon(Icons.favorite_border, color: Colors.black54),
),
FutureBuildWidget(
asyncFuture: hasAddedWishlistProduct(_product.id),
onValue: (isInFavourites) {
return isInFavourites
? IconButton(
onPressed: () => widget.controller.toggleWishList(
onSuccess: () => setState(() {}),
wishlistAction: WishlistAction.remove),
icon: Icon(Icons.favorite, color: Colors.red))
: IconButton(
onPressed: () => widget.controller.toggleWishList(
onSuccess: () => setState(() {}),
wishlistAction: WishlistAction.add),
icon: Icon(Icons.favorite_border,
color: Colors.black54));
}),
CartIconWidget(),
],
title: StoreLogo(
@ -289,248 +262,30 @@ class _ProductDetailState extends NyState<ProductDetailPage> {
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: ListView(
children: <Widget>[
SizedBox(
height: MediaQuery.of(context).size.height * 0.40,
child: SizedBox(
child: Swiper(
itemBuilder: (BuildContext context, int index) =>
CachedImageWidget(
image: _product.images.isNotEmpty
? _product.images[index].src
: getEnv("PRODUCT_PLACEHOLDER_IMAGE"),
),
itemCount: _product.images.isEmpty
? 1
: _product.images.length,
viewportFraction: 0.85,
scale: 0.9,
onTap: (int i) => widget.controller
.viewProductImages(i, _product),
),
),
),
Container(
height: 100,
padding: EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: Text(
_product.name,
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(fontSize: 20),
textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
flex: 4,
),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
formatStringCurrency(
total: _product.price),
style: Theme.of(context)
.textTheme
.headline4
.copyWith(
fontSize: 20,
),
textAlign: TextAlign.right,
),
(_product.onSale == true &&
_product.type != "variable"
? Text(
formatStringCurrency(
total: _product.regularPrice),
style: TextStyle(
color: Colors.grey,
decoration:
TextDecoration.lineThrough,
),
)
: null)
].where((t) => t != null).toList(),
),
flex: 2,
)
],
),
),
Container(
decoration: BoxDecoration(
color: ThemeColor.get(context).background,
borderRadius: BorderRadius.circular(4),
),
padding:
EdgeInsets.symmetric(vertical: 4, horizontal: 16),
height: 180,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
trans("Description"),
style: Theme.of(context)
.textTheme
.caption
.copyWith(fontSize: 18),
textAlign: TextAlign.left,
),
MaterialButton(
child: Text(
trans("Full description"),
style: Theme.of(context)
.textTheme
.bodyText2
.copyWith(fontSize: 14),
textAlign: TextAlign.right,
overflow: TextOverflow.ellipsis,
),
height: 50,
minWidth: 60,
onPressed: _modalBottomSheetMenu,
),
],
),
Flexible(
child: Text(
(_product.shortDescription != null &&
_product.shortDescription != ""
? parseHtmlString(
_product.shortDescription)
: parseHtmlString(_product.description)),
maxLines: 5,
overflow: TextOverflow.ellipsis,
),
flex: 3,
),
],
),
),
],
child: ProductDetailBodyWidget(
wooSignalApp: _wooSignalApp,
product: _product,
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: ThemeColor.get(context).background,
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 15.0,
spreadRadius: -17,
offset: Offset(
0,
-10,
),
)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
(_product.type != "external"
? Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
trans("Quantity"),
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(color: Colors.grey),
),
Row(
children: <Widget>[
IconButton(
icon: Icon(
Icons.remove_circle_outline,
size: 28,
),
onPressed: _removeQuantityTapped,
),
Text(
_quantityIndicator.toString(),
style: Theme.of(context)
.textTheme
.bodyText1,
),
IconButton(
icon: Icon(
Icons.add_circle_outline,
size: 28,
),
onPressed: _addQuantityTapped,
),
],
)
],
)
: null),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: Align(
child: Text(
formatStringCurrency(
total: (parseWcPrice(_product.price) *
_quantityIndicator)
.toString()),
style: Theme.of(context).textTheme.headline4,
textAlign: TextAlign.center,
),
alignment: Alignment.centerLeft,
)),
_product.type == "external"
? Flexible(
child: PrimaryButton(
title: trans("Buy Product"),
action: () => widget.controller
.viewExternalProduct(_product),
),
)
: Flexible(
child: PrimaryButton(
title: trans("Add to cart"),
action: () => _addItemToCart(),
),
),
],
),
].where((e) => e != null).toList(),
),
height: 140,
),
// </Product body>
ProductDetailFooterActionsWidget(
onAddToCart: _addItemToCart,
onViewExternalProduct:
widget.controller.viewExternalProduct,
onAddQuantity: () => widget.controller
.addQuantityTapped(onSuccess: () => setState(() {})),
onRemoveQuantity: () => widget.controller
.removeQuantityTapped(onSuccess: () => setState(() {})),
product: _product,
quantity: widget.controller.quantity,
)
],
),
),
);
}
_addItemToCart() {
_addItemToCart() async {
if (_product.type != "simple") {
_modalBottomSheetAttributes();
return;
@ -543,53 +298,10 @@ class _ProductDetailState extends NyState<ProductDetailPage> {
icon: Icons.local_shipping);
return;
}
_itemAddToCart(
await widget.controller.itemAddToCart(
cartLineItem: CartLineItem.fromProduct(
quantityAmount: _quantityIndicator, product: _product));
}
_addQuantityTapped() {
if (_product.manageStock != null && _product.manageStock == true) {
if (_quantityIndicator >= _product.stockQuantity) {
showToastNotification(context,
title: trans("Maximum quantity reached"),
description:
"${trans("Sorry, only")} ${_product.stockQuantity} ${trans("left")}",
style: ToastNotificationStyleType.INFO);
return;
}
}
if (_quantityIndicator != 0) {
setState(() {
_quantityIndicator++;
});
}
}
_removeQuantityTapped() {
if ((_quantityIndicator - 1) >= 1) {
setState(() {
_quantityIndicator--;
});
}
}
_toggleWishList() async {
String subtitleMsg;
if (isInFavourites) {
await removeWishlistProduct(product: _product);
subtitleMsg = trans("This product has been removed from your wishlist");
} else {
await saveWishlistProduct(product: _product);
subtitleMsg = trans("This product has been added to your wishlist");
}
showStatusAlert(context,
title: trans("Success"),
subtitle: subtitleMsg,
icon: Icons.favorite,
duration: 1);
isInFavourites = !isInFavourites;
setState(() {});
quantityAmount: widget.controller.quantity, product: _product),
onSuccess: () => setState(() {}));
}
}

View File

@ -0,0 +1,213 @@
// Label StoreMax
//
// Created by Anthony Gordon.
// 2022, WooSignal Ltd. All rights reserved.
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app/app/controllers/product_reviews_loader_controller.dart';
import 'package:flutter_app/resources/widgets/app_loader_widget.dart';
import 'package:flutter_app/resources/widgets/no_results_for_products_widget.dart';
import 'package:flutter_app/resources/widgets/product_review_item_container_widget.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:nylo_framework/nylo_framework.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:woosignal/models/response/product_review.dart';
import 'package:woosignal/models/response/products.dart';
import '../../app/controllers/product_reviews_controller.dart';
class ProductReviewsPage extends NyStatefulWidget {
final ProductReviewsController controller = ProductReviewsController();
ProductReviewsPage({Key key}) : super(key: key);
@override
_ProductReviewsPageState createState() => _ProductReviewsPageState();
}
class _ProductReviewsPageState extends NyState<ProductReviewsPage> {
final RefreshController _refreshController =
RefreshController(initialRefresh: false);
Product _product;
bool _shouldStopRequests = false, _isLoading = true;
final ProductReviewsLoaderController _productReviewsLoaderController =
ProductReviewsLoaderController();
@override
widgetDidLoad() async {
_product = widget.data() as Product;
await fetchProductReviews();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
List<ProductReview> productReviews =
_productReviewsLoaderController.getResults();
return Scaffold(
appBar: AppBar(
title: Text(trans('Reviews')),
centerTitle: true,
),
body: _isLoading
? AppLoaderWidget()
: SafeArea(
child: Column(
// shrinkWrap: true,
children: [
Container(
height: mediaQuery.size.height / 5,
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
margin: EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
border:
Border(bottom: BorderSide(color: Colors.black12))),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
child: Text(
_product.name,
style: Theme.of(context).textTheme.headline6,
),
),
Container(
padding: EdgeInsets.symmetric(vertical: 8),
child: Text(
_product.ratingCount.toString() + " Reviews",
style: Theme.of(context).textTheme.bodyText2,
),
),
Row(
children: [
Container(
margin: EdgeInsets.only(right: 8),
child: Text(
_product.averageRating + " Stars",
style: Theme.of(context).textTheme.bodyText2,
),
),
RatingBarIndicator(
rating: double.parse(_product.averageRating),
itemBuilder: (context, index) => Icon(
Icons.star,
color: Colors.amber,
),
itemCount: 5,
itemSize: 20.0,
direction: Axis.horizontal,
),
],
),
],
),
),
Expanded(
child: SmartRefresher(
enablePullDown: true,
enablePullUp: true,
footer: CustomFooter(
builder: (BuildContext context, LoadStatus mode) {
Widget body;
if (mode == LoadStatus.idle) {
body = Text(trans("pull up load"));
} else if (mode == LoadStatus.loading) {
body = CupertinoActivityIndicator();
} else if (mode == LoadStatus.failed) {
body = Text(trans("Load Failed! Click retry!"));
} else if (mode == LoadStatus.canLoading) {
body = Text(trans("release to load more"));
} else {
return SizedBox.shrink();
}
return Container(
height: 55.0,
child: Center(child: body),
);
},
),
controller: _refreshController,
onRefresh: _onRefresh,
onLoading: _onLoading,
child: (productReviews.length != null &&
productReviews.isNotEmpty
? StaggeredGridView.countBuilder(
crossAxisCount: 2,
scrollDirection: Axis.vertical,
itemCount: productReviews.length,
itemBuilder: (BuildContext context, int index) {
ProductReview productReview = productReviews[index];
return Container(
padding: EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
margin: EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
border: Border(
bottom:
BorderSide(color: Colors.black12))),
child: ProductReviewItemContainerWidget(
productReview: productReview),
);
},
staggeredTileBuilder: (int index) {
return StaggeredTile.fit(2);
},
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
)
: NoResultsForProductsWidget()),
))
],
)),
);
}
_onRefresh() async {
_productReviewsLoaderController.clear();
_shouldStopRequests = false;
await fetchProductReviews();
_refreshController.refreshCompleted();
}
_onLoading() async {
await fetchProductReviews();
if (mounted) {
setState(() {});
if (_shouldStopRequests) {
_refreshController.loadNoData();
} else {
_refreshController.loadComplete();
}
}
}
Future fetchProductReviews() async {
await _productReviewsLoaderController.loadProductReviews(
product: _product,
hasResults: (result) {
if (result == false) {
setState(() {
_shouldStopRequests = true;
});
return false;
}
return true;
},
didFinish: () => setState(() {
_isLoading = false;
}));
}
}

View File

@ -10,7 +10,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:hexcolor/hexcolor.dart';
class AppLoaderWidget extends StatelessWidget {
const AppLoaderWidget({Key key}) : super(key: key);
@ -19,6 +18,6 @@ class AppLoaderWidget extends StatelessWidget {
Widget build(BuildContext context) {
bool isDark = (Theme.of(context).brightness == Brightness.dark);
return SpinKitDoubleBounce(
color: HexColor(!isDark ? "#424242" : "#c7c7c7"));
color: Color(!isDark ? 0xFF424242 : 0xFFC7C7C7));
}
}

View File

@ -11,7 +11,6 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app/bootstrap/helpers.dart';
import 'package:hexcolor/hexcolor.dart';
class PrimaryButton extends StatelessWidget {
const PrimaryButton({
@ -54,7 +53,7 @@ class SecondaryButton extends StatelessWidget {
textStyle: Theme.of(context).textTheme.bodyText1.copyWith(
color: Colors.black87,
),
bgColor: HexColor("#f6f6f9"),
bgColor: Color(0xFFF6F6F9),
);
}

View File

@ -25,7 +25,7 @@ class CheckoutCouponAmountWidget extends StatelessWidget {
return Text("");
} else {
if (checkoutSession.coupon == null) {
return Container();
return SizedBox.shrink();
}
return Padding(
child: CheckoutMetaLine(

View File

@ -23,7 +23,7 @@ class CheckoutShippingTypeWidget extends StatelessWidget {
Widget build(BuildContext context) {
bool hasDisableShipping = wooSignalApp.disableShipping == 1;
if (hasDisableShipping == true) {
return Container();
return SizedBox.shrink();
}
bool hasSelectedShippingType = checkoutSession.shippingType != null;
return CheckoutRowLine(
@ -42,10 +42,12 @@ class CheckoutShippingTypeWidget extends StatelessWidget {
CustomerAddress shippingAddress =
checkoutSession.billingDetails.shippingAddress;
if (shippingAddress == null || shippingAddress.customerCountry == null) {
showToastNotification(context,
title: trans("Oops"),
description: trans("Add your shipping details first"),
icon: Icons.local_shipping);
showToastNotification(
context,
title: trans("Oops"),
description: trans("Add your shipping details first"),
icon: Icons.local_shipping,
);
return;
}
Navigator.pushNamed(context, "/checkout-shipping-type")

View File

@ -1,3 +1,4 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app/bootstrap/helpers.dart';
import 'package:flutter_app/resources/widgets/app_loader_widget.dart';
@ -29,16 +30,18 @@ class _CompoHomeWidgetState extends State<CompoHomeWidget> {
_loadHome() async {
categories = await appWooSignal((api) =>
api.getProductCategories(parent: 0, perPage: 20, hideEmpty: true));
api.getProductCategories(parent: 0, perPage: 50, hideEmpty: true));
categories.sort((category1, category2) =>
category1.menuOrder.compareTo(category2.menuOrder));
for (var category in categories) {
List<Product> products = await appWooSignal((api) => api.getProducts(
perPage: 10,
category: category.id.toString(),
status: "publish",
stockStatus: "instock"));
List<Product> products = await appWooSignal(
(api) => api.getProducts(
perPage: 10,
category: category.id.toString(),
status: "publish",
stockStatus: "instock"),
);
if (products.isNotEmpty) {
categoryAndProducts.addAll({category: products});
setState(() {});
@ -106,7 +109,8 @@ class _CompoHomeWidgetState extends State<CompoHomeWidget> {
width: MediaQuery.of(context).size.width,
fit: BoxFit.cover,
),
onTap: () => _showCategory(catProds.key))
onTap: () => _showCategory(catProds.key),
)
: null,
ConstrainedBox(
constraints: BoxConstraints(
@ -123,7 +127,7 @@ class _CompoHomeWidgetState extends State<CompoHomeWidget> {
MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
child: AutoSizeText(
catProds.key.name,
style: Theme.of(context)
.textTheme
@ -131,6 +135,7 @@ class _CompoHomeWidgetState extends State<CompoHomeWidget> {
.copyWith(
fontWeight: FontWeight.bold,
fontSize: 22),
maxLines: 1,
),
),
Flexible(

View File

@ -0,0 +1,43 @@
// Label StoreMax
//
// Created by Anthony Gordon.
// 2022, WooSignal Ltd. All rights reserved.
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import 'package:flutter/material.dart';
class FutureBuildWidget<T> extends StatelessWidget {
const FutureBuildWidget(
{Key key,
@required this.asyncFuture,
@required this.onValue,
this.onLoading})
: super(key: key);
final Widget Function(T value) onValue;
final Widget onLoading;
final Future asyncFuture;
@override
Widget build(BuildContext context) {
return FutureBuilder<T>(
future: asyncFuture,
builder: (BuildContext context, AsyncSnapshot<T> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return onLoading ?? Container();
default:
if (snapshot.hasError) {
return SizedBox.shrink();
} else {
return onValue(snapshot.data);
}
}
},
);
}
}

View File

@ -57,6 +57,8 @@ class _HomeDrawerWidgetState extends State<HomeDrawerWidget> {
),
if (["compo"].contains(_themeType) == false)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
child: Text(

View File

@ -161,7 +161,7 @@ class _NoticHomeWidgetState extends State<NoticHomeWidget> {
} else if (mode == LoadStatus.canLoading) {
body = Text(trans("release to load more"));
} else {
return Container();
return SizedBox.shrink();
}
return Container(
height: 55.0,

View File

@ -60,21 +60,22 @@ class _NoticThemeWidgetState extends State<NoticThemeWidget> {
body: activeWidget,
resizeToAvoidBottomInset: false,
bottomNavigationBar: allNavWidgets == null
? AppLoaderWidget() : BottomNavigationBar(
onTap: (currentIndex) =>
_changeMainWidget(currentIndex, allNavWidgets),
currentIndex: _currentIndex,
unselectedItemColor: Colors.black54,
fixedColor: Colors.black87,
selectedLabelStyle: TextStyle(color: Colors.black),
unselectedLabelStyle: TextStyle(
color: Colors.black87,
),
showSelectedLabels: false,
showUnselectedLabels: false,
items:
allNavWidgets.map((e) => e.bottomNavigationBarItem).toList(),
),
? AppLoaderWidget()
: BottomNavigationBar(
onTap: (currentIndex) =>
_changeMainWidget(currentIndex, allNavWidgets),
currentIndex: _currentIndex,
unselectedItemColor: Colors.black54,
fixedColor: Colors.black87,
selectedLabelStyle: TextStyle(color: Colors.black),
unselectedLabelStyle: TextStyle(
color: Colors.black87,
),
showSelectedLabels: false,
showUnselectedLabels: false,
items:
allNavWidgets.map((e) => e.bottomNavigationBarItem).toList(),
),
);
}
@ -122,12 +123,12 @@ class _NoticThemeWidgetState extends State<NoticThemeWidget> {
items.add(BottomNavItem(
id: 5,
bottomNavigationBarItem:
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Account'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Account'),
tabWidget: (await authCheck())
? AccountDetailPage(showLeadingBackButton: false)
: AccountLandingPage(
showBackButton: false,
),
showBackButton: false,
),
));
}
return items;

View File

@ -0,0 +1,65 @@
// Label StoreMax
//
// Created by Anthony Gordon.
// 2022, WooSignal Ltd. All rights reserved.
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import 'package:flutter/material.dart';
import 'package:flutter_app/resources/widgets/product_detail_description_widget.dart';
import 'package:flutter_app/resources/widgets/product_detail_header_widget.dart';
import 'package:flutter_app/resources/widgets/product_detail_image_swiper_widget.dart';
import 'package:flutter_app/resources/widgets/product_detail_related_products_widget.dart';
import 'package:flutter_app/resources/widgets/product_detail_reviews_widget.dart';
import 'package:flutter_app/resources/widgets/product_detail_upsell_widget.dart';
import 'package:woosignal/models/response/products.dart';
import 'package:woosignal/models/response/woosignal_app.dart';
class ProductDetailBodyWidget extends StatelessWidget {
const ProductDetailBodyWidget(
{Key key, @required this.product, @required this.wooSignalApp})
: super(key: key);
final Product product;
final WooSignalApp wooSignalApp;
@override
Widget build(BuildContext context) {
return ListView(
shrinkWrap: true,
children: <Widget>[
ProductDetailImageSwiperWidget(
product: product,
onTapImage: (i) => _viewProductImages(context, i)),
// </Image Swiper>
ProductDetailHeaderWidget(product: product),
// </Header title + price>
ProductDetailDescriptionWidget(product: product),
// </Description body>
ProductDetailReviewsWidget(
product: product, wooSignalApp: wooSignalApp),
// </Product reviews>
ProductDetailUpsellWidget(
productIds: product.upsellIds, wooSignalApp: wooSignalApp),
// </You may also like>
ProductDetailRelatedProductsWidget(
product: product, wooSignalApp: wooSignalApp)
// </Related products>
],
);
}
_viewProductImages(BuildContext context, int i) =>
Navigator.pushNamed(context, "/product-images", arguments: {
"index": i,
"images": product.images.map((f) => f.src).toList()
});
}

View File

@ -0,0 +1,90 @@
// Label StoreMax
//
// Created by Anthony Gordon.
// 2022, WooSignal Ltd. All rights reserved.
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import 'package:flutter/material.dart';
import 'package:flutter_app/resources/widgets/woosignal_ui.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:nylo_framework/nylo_framework.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:woosignal/models/response/products.dart';
class ProductDetailDescriptionWidget extends StatelessWidget {
const ProductDetailDescriptionWidget({Key key, @required this.product})
: super(key: key);
final Product product;
@override
Widget build(BuildContext context) {
if (product.shortDescription.isEmpty && product.description.isEmpty) {
return SizedBox.shrink();
}
return ListView(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
children: [
Container(
height: 50,
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
trans("Description"),
style:
Theme.of(context).textTheme.caption.copyWith(fontSize: 18),
textAlign: TextAlign.left,
),
if (product.shortDescription.isNotEmpty &&
product.description.isNotEmpty)
MaterialButton(
child: Text(
trans("Full description"),
style: Theme.of(context)
.textTheme
.bodyText2
.copyWith(fontSize: 14),
textAlign: TextAlign.right,
overflow: TextOverflow.ellipsis,
),
height: 50,
minWidth: 60,
onPressed: () => _modalBottomSheetMenu(context),
),
],
),
),
Container(
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 16),
child: HtmlWidget(
product.shortDescription.isNotEmpty
? product.shortDescription
: product.description,
renderMode: RenderMode.column, onTapUrl: (String url) async {
await launch(url);
return true;
}, textStyle: Theme.of(context).textTheme.bodyText2),
),
],
);
}
_modalBottomSheetMenu(BuildContext context) {
wsModalBottom(
context,
title: trans("Description"),
bodyWidget: SingleChildScrollView(
child: HtmlWidget(product.description),
),
);
}
}

View File

@ -0,0 +1,128 @@
// Label StoreMax
//
// Created by Anthony Gordon.
// 2022, WooSignal Ltd. All rights reserved.
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import 'package:flutter/material.dart';
import 'package:flutter_app/bootstrap/helpers.dart';
import 'package:flutter_app/resources/widgets/buttons.dart';
import 'package:nylo_framework/nylo_framework.dart';
import 'package:woosignal/models/response/products.dart';
class ProductDetailFooterActionsWidget extends StatelessWidget {
const ProductDetailFooterActionsWidget(
{Key key,
@required this.product,
@required this.quantity,
@required this.onAddToCart,
@required this.onViewExternalProduct,
@required this.onAddQuantity,
@required this.onRemoveQuantity})
: super(key: key);
final Product product;
final Function onViewExternalProduct;
final Function onAddToCart;
final Function onAddQuantity;
final Function onRemoveQuantity;
final int quantity;
@override
Widget build(BuildContext context) {
return Container(
height: 140,
padding: EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: ThemeColor.get(context).background,
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 15.0,
spreadRadius: -17,
offset: Offset(
0,
-10,
),
)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
(product.type != "external"
? Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
trans("Quantity"),
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(color: Colors.grey),
),
Row(
children: <Widget>[
IconButton(
icon: Icon(
Icons.remove_circle_outline,
size: 28,
),
onPressed: onRemoveQuantity,
),
Text(
quantity.toString(),
style: Theme.of(context).textTheme.bodyText1,
),
IconButton(
icon: Icon(
Icons.add_circle_outline,
size: 28,
),
onPressed: onAddQuantity,
),
],
)
],
)
: null),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: Align(
child: Text(
formatStringCurrency(
total:
(parseWcPrice(product.price) * quantity).toString()),
style: Theme.of(context).textTheme.headline4,
textAlign: TextAlign.center,
),
alignment: Alignment.centerLeft,
)),
product.type == "external"
? Flexible(
child: PrimaryButton(
title: trans("Buy Product"),
action: () => onViewExternalProduct,
),
)
: Flexible(
child: PrimaryButton(
title: trans("Add to cart"),
action: onAddToCart,
),
),
],
),
].where((e) => e != null).toList(),
),
);
}
}

View File

@ -0,0 +1,73 @@
// Label StoreMax
//
// Created by Anthony Gordon.
// 2022, WooSignal Ltd. All rights reserved.
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import 'package:flutter/material.dart';
import 'package:flutter_app/bootstrap/helpers.dart';
import 'package:woosignal/models/response/products.dart';
class ProductDetailHeaderWidget extends StatelessWidget {
const ProductDetailHeaderWidget({Key key, @required this.product})
: super(key: key);
final Product product;
@override
Widget build(BuildContext context) {
return Container(
height: 100,
padding: EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: Text(
product.name,
style:
Theme.of(context).textTheme.bodyText1.copyWith(fontSize: 20),
textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
flex: 4,
),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
formatStringCurrency(total: product.price),
style: Theme.of(context).textTheme.headline4.copyWith(
fontSize: 20,
),
textAlign: TextAlign.right,
),
(product.onSale == true && product.type != "variable"
? Text(
formatStringCurrency(total: product.regularPrice),
style: TextStyle(
color: Colors.grey,
decoration: TextDecoration.lineThrough,
),
)
: null)
].where((t) => t != null).toList(),
),
flex: 2,
)
],
),
);
}
}

View File

@ -0,0 +1,44 @@
// Label StoreMax
//
// Created by Anthony Gordon.
// 2022, WooSignal Ltd. All rights reserved.
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import 'package:flutter/material.dart';
import 'package:flutter_app/resources/widgets/cached_image_widget.dart';
import 'package:flutter_swiper/flutter_swiper.dart';
import 'package:nylo_framework/nylo_framework.dart';
import 'package:woosignal/models/response/products.dart';
class ProductDetailImageSwiperWidget extends StatelessWidget {
const ProductDetailImageSwiperWidget(
{Key key, @required this.product, @required this.onTapImage})
: super(key: key);
final Product product;
final void Function(int i) onTapImage;
@override
Widget build(BuildContext context) {
return SizedBox(
height: MediaQuery.of(context).size.height * 0.40,
child: SizedBox(
child: Swiper(
itemBuilder: (BuildContext context, int index) => CachedImageWidget(
image: product.images.isNotEmpty
? product.images[index].src
: getEnv("PRODUCT_PLACEHOLDER_IMAGE"),
),
itemCount: product.images.isEmpty ? 1 : product.images.length,
viewportFraction: 0.85,
scale: 0.9,
onTap: onTapImage,
),
),
);
}
}

View File

@ -0,0 +1,80 @@
// Label StoreMax
//
// Created by Anthony Gordon.
// 2022, WooSignal Ltd. All rights reserved.
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import 'package:flutter/material.dart';
import 'package:flutter_app/bootstrap/helpers.dart';
import 'package:flutter_app/resources/widgets/app_loader_widget.dart';
import 'package:flutter_app/resources/widgets/future_build_widget.dart';
import 'package:flutter_app/resources/widgets/woosignal_ui.dart';
import 'package:nylo_framework/nylo_framework.dart';
import 'package:woosignal/models/response/products.dart';
import 'package:woosignal/models/response/woosignal_app.dart';
class ProductDetailRelatedProductsWidget extends StatelessWidget {
const ProductDetailRelatedProductsWidget(
{Key key, @required this.product, @required this.wooSignalApp})
: super(key: key);
final Product product;
final WooSignalApp wooSignalApp;
@override
Widget build(BuildContext context) {
if (wooSignalApp.showRelatedProducts == false) {
return SizedBox.shrink();
}
return ListView(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
children: [
Container(
height: 50,
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
trans("Related products"),
style:
Theme.of(context).textTheme.caption.copyWith(fontSize: 18),
textAlign: TextAlign.left,
),
],
),
),
Container(
height: 200,
child: FutureBuildWidget<List<Product>>(
asyncFuture: fetchRelated(),
onValue: (relatedProducts) {
return ListView(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
children: relatedProducts
.map((e) => Container(
width: MediaQuery.of(context).size.width / 2.2,
child: ProductItemContainer(product: e)))
.toList(),
);
},
onLoading: AppLoaderWidget(),
),
),
],
);
}
Future<List<Product>> fetchRelated() async {
return await appWooSignal(
(api) => api.getProducts(perPage: 100, include: product.relatedIds),
);
}
}

View File

@ -0,0 +1,90 @@
// Label StoreMax
//
// Created by Anthony Gordon.
// 2022, WooSignal Ltd. All rights reserved.
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import 'package:flutter/material.dart';
import 'package:flutter_app/bootstrap/helpers.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:nylo_framework/nylo_framework.dart';
import 'package:woosignal/models/response/product_review.dart';
class ProductDetailReviewTileWidget extends StatefulWidget {
ProductDetailReviewTileWidget({Key key, @required this.productReview});
final ProductReview productReview;
@override
_ProductDetailReviewTileWidgetState createState() =>
_ProductDetailReviewTileWidgetState();
}
class _ProductDetailReviewTileWidgetState
extends State<ProductDetailReviewTileWidget> {
int _maxLines = 3;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(top: 5, left: 16, right: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
RatingBarIndicator(
rating: widget.productReview.rating.toDouble(),
itemBuilder: (context, index) => Icon(
Icons.star,
color: Colors.amber,
),
itemCount: 5,
itemSize: 20.0,
direction: Axis.horizontal,
),
Text(widget.productReview.reviewer),
Text(
formatDateTime("MMM d, yyyy").format(
parseDateTime(widget.productReview.dateCreated),
),
),
],
),
ListTile(
title: Container(
margin: EdgeInsets.only(top: 10),
child: Text(parseHtmlString(widget.productReview.review),
maxLines: _maxLines,
overflow: _maxLines != null
? TextOverflow.ellipsis
: TextOverflow.visible),
),
contentPadding: EdgeInsets.all(0),
minVerticalPadding: 0),
if (_maxLines != null && widget.productReview.review.length > 115)
InkWell(
child: Text(trans("More"),
style: Theme.of(context)
.textTheme
.bodyText2
.copyWith(fontWeight: FontWeight.bold)),
onTap: () => setState(() {
_maxLines = null;
}),
)
],
),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Colors.black12, width: 1),
)),
);
}
}

View File

@ -0,0 +1,152 @@
// Label StoreMax
//
// Created by Anthony Gordon.
// 2022, WooSignal Ltd. All rights reserved.
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app/bootstrap/helpers.dart';
import 'package:flutter_app/resources/widgets/future_build_widget.dart';
import 'package:flutter_app/resources/widgets/product_detail_review_tile_widget.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:nylo_framework/nylo_framework.dart';
import 'package:woosignal/models/response/product_review.dart';
import 'package:woosignal/models/response/products.dart';
import 'package:woosignal/models/response/woosignal_app.dart';
class ProductDetailReviewsWidget extends StatefulWidget {
ProductDetailReviewsWidget(
{@required this.product, @required this.wooSignalApp});
final Product product;
final WooSignalApp wooSignalApp;
@override
_ProductDetailReviewsWidgetState createState() =>
_ProductDetailReviewsWidgetState();
}
class _ProductDetailReviewsWidgetState
extends State<ProductDetailReviewsWidget> {
bool _ratingExpanded = false;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
if (widget.product.reviewsAllowed == false ||
widget.wooSignalApp.showProductReviews == false) {
return SizedBox.shrink();
}
return Row(
children: <Widget>[
Expanded(
child: ExpansionTile(
textColor: ThemeColor.get(context).primaryAccent,
iconColor: ThemeColor.get(context).primaryAccent,
tilePadding: EdgeInsets.symmetric(horizontal: 16),
childrenPadding: EdgeInsets.all(0),
title: Text("${trans("Reviews")} (${widget.product.ratingCount})"),
onExpansionChanged: (value) {
setState(() {
_ratingExpanded = value;
});
},
trailing: Container(
width: MediaQuery.of(context).size.width / 1.8,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.end,
children: [
RatingBarIndicator(
rating: double.parse(widget.product.averageRating),
itemBuilder: (context, index) => Icon(
Icons.star,
color: Colors.amber,
),
itemCount: 5,
itemSize: 25.0,
direction: Axis.horizontal,
),
Icon(
_ratingExpanded
? Icons.keyboard_arrow_down_rounded
: Icons.keyboard_arrow_up_rounded,
size: 30)
],
),
),
initiallyExpanded: false,
children: [
_ratingExpanded == true
? FutureBuildWidget<List<ProductReview>>(
asyncFuture: fetchReviews(),
onValue: (reviews) {
int reviewsCount = reviews.length;
List<Widget> childrenWidgets = [];
List<ProductDetailReviewTileWidget> children = reviews
.map((review) => ProductDetailReviewTileWidget(
productReview: review))
.toList();
childrenWidgets.addAll(children);
if (reviewsCount >= 5) {
childrenWidgets.add(
Container(
child: ListTile(
contentPadding:
EdgeInsets.symmetric(horizontal: 16),
title: Text(
trans('See More Reviews'),
),
onTap: () => Navigator.pushNamed(
context, "/product-reviews",
arguments: widget.product),
),
),
);
}
return ListView(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(0),
children: reviews.isEmpty
? [
Container(
child: ListTile(
title: Text(
trans('There are no reviews yet.'),
),
),
)
]
: childrenWidgets,
);
},
onLoading: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: CupertinoActivityIndicator(),
),
)
: null,
].where((element) => element != null).toList(),
)),
],
);
}
Future<List<ProductReview>> fetchReviews() async {
return await appWooSignal(
(api) => api.getProductReviews(
perPage: 5, product: [widget.product.id], status: "approved"),
);
}
}

View File

@ -0,0 +1,170 @@
// Label StoreMax
//
// Created by Anthony Gordon.
// 2022, WooSignal Ltd. All rights reserved.
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app/app/controllers/product_loader_controller.dart';
import 'package:flutter_app/resources/widgets/app_loader_widget.dart';
import 'package:flutter_app/resources/widgets/no_results_for_products_widget.dart';
import 'package:flutter_app/resources/widgets/woosignal_ui.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:nylo_framework/nylo_framework.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:woosignal/models/response/products.dart';
import 'package:woosignal/models/response/woosignal_app.dart';
class ProductDetailUpsellWidget extends StatefulWidget {
ProductDetailUpsellWidget(
{@required this.productIds, @required this.wooSignalApp});
final List<int> productIds;
final WooSignalApp wooSignalApp;
@override
_ProductDetailUpsellWidgetState createState() =>
_ProductDetailUpsellWidgetState();
}
class _ProductDetailUpsellWidgetState extends State<ProductDetailUpsellWidget> {
final RefreshController _refreshControllerUpsell =
RefreshController(initialRefresh: false);
final ProductLoaderController _productLoaderController =
ProductLoaderController();
bool _shouldStopRequests = false, _isLoading = true;
@override
void initState() {
super.initState();
fetchProducts();
}
@override
Widget build(BuildContext context) {
List<Product> products = _productLoaderController.getResults();
if (widget.productIds.isEmpty ||
products.isEmpty ||
widget.wooSignalApp.showUpsellProducts == false) {
return SizedBox.shrink();
}
return ListView(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
children: [
Container(
height: 50,
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
trans("${trans('You may also like')}"),
style:
Theme.of(context).textTheme.caption.copyWith(fontSize: 18),
textAlign: TextAlign.left,
),
],
),
),
_isLoading == true
? AppLoaderWidget()
: Container(
height: 200,
child: SmartRefresher(
enablePullDown: true,
enablePullUp: true,
footer: CustomFooter(
builder: (BuildContext context, LoadStatus mode) {
Widget body;
if (mode == LoadStatus.idle) {
body = Text(trans("pull up load"));
} else if (mode == LoadStatus.loading) {
body = CupertinoActivityIndicator();
} else if (mode == LoadStatus.failed) {
body = Text(trans("Load Failed! Click retry!"));
} else if (mode == LoadStatus.canLoading) {
body = Text(trans("release to load more"));
} else {
return SizedBox.shrink();
}
return Container(
height: 55.0,
child: Center(child: body),
);
},
),
controller: _refreshControllerUpsell,
onRefresh: _onRefresh,
onLoading: _onLoading,
child: (products.length != null && products.isNotEmpty
? StaggeredGridView.countBuilder(
crossAxisCount: 2,
scrollDirection: Axis.horizontal,
itemCount: products.length,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 250,
child: ProductItemContainer(
product: products[index],
),
);
},
staggeredTileBuilder: (int index) {
return StaggeredTile.fit(2);
},
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
)
: NoResultsForProductsWidget()),
),
),
],
);
}
_onRefresh() async {
_productLoaderController.clear();
_shouldStopRequests = false;
await fetchProducts();
_refreshControllerUpsell.refreshCompleted();
}
_onLoading() async {
await fetchProducts();
if (mounted) {
setState(() {});
if (_shouldStopRequests) {
_refreshControllerUpsell.loadNoData();
} else {
_refreshControllerUpsell.loadComplete();
}
}
}
Future fetchProducts() async {
await _productLoaderController.loadProducts(
hasResults: (result) {
if (result == false) {
setState(() {
_shouldStopRequests = true;
});
return false;
}
return true;
},
didFinish: () => setState(() {
_isLoading = false;
}),
productIds: widget.productIds);
}
}

View File

@ -0,0 +1,65 @@
// Label StoreMax
//
// Created by Anthony Gordon.
// 2022, WooSignal Ltd. All rights reserved.
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import 'package:flutter/material.dart';
import 'package:flutter_app/bootstrap/helpers.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:woosignal/models/response/product_review.dart';
class ProductReviewItemContainerWidget extends StatelessWidget {
const ProductReviewItemContainerWidget(
{Key key, @required this.productReview})
: super(key: key);
final ProductReview productReview;
@override
Widget build(BuildContext context) {
return Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
RatingBarIndicator(
rating: productReview.rating.toDouble(),
itemBuilder: (context, index) => Icon(
Icons.star,
color: Colors.amber,
),
itemCount: 5,
itemSize: 20.0,
direction: Axis.horizontal,
),
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(parseHtmlString(productReview.review)),
),
Row(
children: [
Text(productReview.reviewer),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Icon(
Icons.circle_rounded,
size: 3,
),
),
Text(
formatDateTime("MMM d, yyyy").format(
parseDateTime(productReview.dateCreated),
),
),
],
)
],
),
);
}
}

View File

@ -22,7 +22,6 @@ import 'package:flutter_app/resources/widgets/no_results_for_products_widget.dar
import 'package:flutter_app/resources/widgets/top_nav_widget.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:flutter_swiper/flutter_swiper.dart';
import 'package:hexcolor/hexcolor.dart';
import 'package:nylo_support/helpers/helper.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:woosignal/models/response/products.dart';
@ -303,7 +302,7 @@ class CheckoutMetaLine extends StatelessWidget {
List<BoxShadow> wsBoxShadow({double blurRadius}) => [
BoxShadow(
color: HexColor("#e8e8e8"),
color: Color(0xFFE8E8E8),
blurRadius: blurRadius ?? 15.0,
spreadRadius: 0,
offset: Offset(
@ -452,7 +451,10 @@ class ProductItemContainer extends StatelessWidget {
].where((e) => e != null).toList(),
),
),
onTap: () => onTap(product),
onTap: () => onTap != null
? onTap(product)
: Navigator.pushNamed(context, "/product-detail",
arguments: product),
),
);
}

View File

@ -17,9 +17,11 @@ import 'package:flutter_app/resources/pages/coupon_page.dart';
import 'package:flutter_app/resources/pages/customer_countries.dart';
import 'package:flutter_app/resources/pages/home.dart';
import 'package:flutter_app/resources/pages/home_search.dart';
import 'package:flutter_app/resources/pages/leave_review_page.dart';
import 'package:flutter_app/resources/pages/no_connection_page.dart';
import 'package:flutter_app/resources/pages/product_detail.dart';
import 'package:flutter_app/resources/pages/product_image_viewer_page.dart';
import 'package:flutter_app/resources/pages/product_reviews_page.dart';
import 'package:flutter_app/resources/pages/wishlist_page_widget.dart';
import 'package:flutter_app/resources/widgets/checkout_paypal.dart';
import 'package:nylo_support/router/router.dart';
@ -47,6 +49,12 @@ appRouter() => nyRoutes((router) {
router.route("/product-detail", (context) => ProductDetailPage(),
transition: PageTransitionType.rightToLeftWithFade);
router.route("/product-reviews", (context) => ProductReviewsPage(),
transition: PageTransitionType.rightToLeftWithFade);
router.route("/product-leave-review", (context) => LeaveReviewPage(),
transition: PageTransitionType.rightToLeftWithFade);
router.route("/product-images", (context) => ProductImageViewerPage(),
transition: PageTransitionType.fade);

View File

@ -21,7 +21,7 @@ packages:
name: animate_do
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.1.0"
archive:
dependency: transitive
description:
@ -43,6 +43,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.8.2"
audio_session:
dependency: transitive
description:
name: audio_session
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.6+1"
auto_size_text:
dependency: "direct main"
description:
@ -99,6 +106,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
chewie:
dependency: transitive
description:
name: chewie
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.2"
cli_util:
dependency: transitive
description:
@ -249,6 +263,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.3"
flutter_rating_bar:
dependency: "direct main"
description:
name: flutter_rating_bar
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
flutter_secure_storage:
dependency: transitive
description:
@ -311,7 +332,7 @@ packages:
name: flutter_stripe
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.1.1"
flutter_styled_toast:
dependency: "direct main"
description:
@ -319,6 +340,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
flutter_svg:
dependency: transitive
description:
name: flutter_svg
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
flutter_swiper:
dependency: "direct main"
description:
@ -343,6 +371,20 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_widget_from_html:
dependency: "direct main"
description:
name: flutter_widget_from_html
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.4"
flutter_widget_from_html_core:
dependency: transitive
description:
name: flutter_widget_from_html_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.4"
freezed_annotation:
dependency: transitive
description:
@ -350,6 +392,62 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
fwfh_cached_network_image:
dependency: transitive
description:
name: fwfh_cached_network_image
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.0+2"
fwfh_chewie:
dependency: transitive
description:
name: fwfh_chewie
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.0+1"
fwfh_just_audio:
dependency: transitive
description:
name: fwfh_just_audio
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.2+2"
fwfh_selectable_text:
dependency: transitive
description:
name: fwfh_selectable_text
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.3+1"
fwfh_svg:
dependency: transitive
description:
name: fwfh_svg
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.2"
fwfh_text_style:
dependency: transitive
description:
name: fwfh_text_style
url: "https://pub.dartlang.org"
source: hosted
version: "2.7.2"
fwfh_url_launcher:
dependency: transitive
description:
name: fwfh_url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.1+3"
fwfh_webview:
dependency: transitive
description:
name: fwfh_webview
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.2+1"
glob:
dependency: transitive
description:
@ -364,13 +462,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
hexcolor:
dependency: "direct main"
description:
name: hexcolor
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
html:
dependency: "direct main"
description:
@ -420,6 +511,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.4.0"
just_audio:
dependency: transitive
description:
name: just_audio
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.18"
just_audio_platform_interface:
dependency: transitive
description:
name: just_audio_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
just_audio_web:
dependency: transitive
description:
name: just_audio_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.2"
lints:
dependency: "direct dev"
description:
@ -462,6 +574,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.3"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
nylo_framework:
dependency: "direct main"
description:
@ -503,7 +622,7 @@ packages:
name: page_transition
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
version: "2.0.5"
path:
dependency: transitive
description:
@ -511,6 +630,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
path_drawing:
dependency: transitive
description:
name: path_drawing
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
path_parsing:
dependency: transitive
description:
name: path_parsing
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
path_provider:
dependency: transitive
description:
@ -588,6 +721,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.2.3"
provider:
dependency: transitive
description:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.0"
pub_semver:
dependency: transitive
description:
@ -622,7 +762,7 @@ packages:
name: shared_preferences
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.11"
version: "2.0.12"
shared_preferences_android:
dependency: transitive
description:
@ -732,21 +872,21 @@ packages:
name: stripe_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.1.1"
stripe_ios:
dependency: transitive
description:
name: stripe_ios
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.1.1"
stripe_platform_interface:
dependency: transitive
description:
name: stripe_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.1.1"
synchronized:
dependency: transitive
description:
@ -795,7 +935,7 @@ packages:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.17"
version: "6.0.18"
url_launcher_android:
dependency: transitive
description:
@ -866,6 +1006,62 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
video_player:
dependency: transitive
description:
name: video_player
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.13"
video_player_platform_interface:
dependency: transitive
description:
name: video_player_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.0"
video_player_web:
dependency: transitive
description:
name: video_player_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
wakelock:
dependency: transitive
description:
name: wakelock
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.6"
wakelock_macos:
dependency: transitive
description:
name: wakelock_macos
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
wakelock_platform_interface:
dependency: transitive
description:
name: wakelock_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
wakelock_web:
dependency: transitive
description:
name: wakelock_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
wakelock_windows:
dependency: transitive
description:
name: wakelock_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
watcher:
dependency: transitive
description:
@ -914,7 +1110,7 @@ packages:
name: woosignal
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.3"
version: "3.0.5"
wp_json_api:
dependency: "direct main"
description:
@ -945,4 +1141,4 @@ packages:
version: "3.1.0"
sdks:
dart: ">=2.15.0 <3.0.0"
flutter: ">=2.5.0"
flutter: ">=2.8.0"

View File

@ -1,7 +1,7 @@
# Official WooSignal App Template for WooCommerce
# Label StoreMax
# Version: 5.6.2
# Version: 5.7.0
# Author: Anthony Gordon
# Homepage: https://woosignal.com
# Documentation: https://woosignal.com/docs/app/label-storemax
@ -28,10 +28,10 @@ dependencies:
google_fonts: ^2.2.0
analyzer: ^1.5.0
intl: ^0.17.0
page_transition: ^2.0.4
page_transition: ^2.0.5
nylo_framework: ^2.1.4
woosignal: ^3.0.3
flutter_stripe: ^2.1.0
woosignal: ^3.0.5
flutter_stripe: ^2.1.1
wp_json_api: ^3.1.3
cached_network_image: ^3.2.0
package_info: ^2.0.2
@ -41,17 +41,18 @@ dependencies:
webview_flutter: ^2.3.1
pull_to_refresh: 2.0.0
flutter_swiper: ^1.1.6
url_launcher: ^6.0.17
url_launcher: ^6.0.18
flutter_styled_toast: ^2.0.0
animate_do: ^2.0.0
animate_do: ^2.1.0
bubble_tab_indicator: ^0.1.5
status_alert: ^0.1.3
math_expressions: ^2.3.0
validated: ^2.0.0
hexcolor: ^2.0.5
flutter_spinkit: ^5.1.0
auto_size_text: ^3.0.0
html: ^0.15.0
flutter_widget_from_html: ^0.8.4
flutter_rating_bar: ^4.0.0
flutter_staggered_grid_view: ^0.4.1
# firebase_messaging: ^11.2.3
# firebase_core: ^1.10.5

View File

@ -4,7 +4,7 @@
# WooCommerce App: Label StoreMax
### Label StoreMax - v5.6.2
### Label StoreMax - v5.7.0
[Official WooSignal WooCommerce App](https://woosignal.com)