Line data Source code
1 : // Copyright 2023 The terCAD team. All rights reserved.
2 : // Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.
3 :
4 : import 'package:app_finance/_classes/controller/purchase_controller.dart';
5 : import 'package:app_finance/_classes/herald/app_design.dart';
6 : import 'package:app_finance/_classes/herald/app_locale.dart';
7 : import 'package:app_finance/_configs/custom_text_theme.dart';
8 : import 'package:app_finance/_configs/theme_helper.dart';
9 : import 'package:app_finance/_ext/build_context_ext.dart';
10 : import 'package:app_finance/design/generic/loading_widget.dart';
11 : import 'package:app_finance/design/generic/notification_bar.dart';
12 : import 'package:app_finance/design/wrapper/row_widget.dart';
13 : import 'package:app_finance/design/wrapper/text_wrapper.dart';
14 : import 'package:material_ui/material_ui.dart';
15 : import 'package:flutter_currency_picker/flutter_currency_picker.dart';
16 : import 'package:flutter_grid_layout/flutter_grid_layout.dart';
17 : import 'package:in_app_purchase/in_app_purchase.dart';
18 :
19 : class PurchaseWidget extends StatefulWidget {
20 : final String path;
21 : final ProductDetails? product;
22 : final List<PurchaseDetails>? purchase;
23 : final double size;
24 : final String title;
25 :
26 0 : const PurchaseWidget(this.path, {super.key, required this.title, this.product, this.purchase, this.size = 150});
27 :
28 0 : @override
29 0 : PurchaseWidgetState createState() => PurchaseWidgetState();
30 : }
31 :
32 : class PurchaseWidgetState extends State<PurchaseWidget> {
33 : bool isLoading = false;
34 :
35 0 : Future<void> _purchase() async {
36 0 : if (widget.product == null) {
37 0 : NotificationBar.showSnackBar(context, AppLocale.labels.subscriptionInactive, true);
38 : } else {
39 0 : setState(() => isLoading = true);
40 0 : final didStartPurchase = await PurchaseController.buy(widget.product!);
41 0 : setState(() => isLoading = false);
42 0 : if (!didStartPurchase && mounted) {
43 0 : NotificationBar.showSnackBar(context, AppLocale.labels.subscriptionInactive, true);
44 : }
45 : }
46 : }
47 :
48 0 : @override
49 : Widget build(BuildContext context) {
50 0 : final indent = ThemeHelper.getIndent(0.5);
51 0 : final cost = widget.product?.rawPrice.toCurrency(code: widget.product?.currencyCode, withPattern: false) ?? '---';
52 0 : final costWidth = ThemeHelper.getTextWidth(Text(cost));
53 0 : return InkWell(
54 0 : onTap: _purchase,
55 0 : child: Container(
56 0 : width: widget.size,
57 0 : height: widget.size,
58 0 : margin: EdgeInsets.all(indent),
59 0 : decoration: BoxDecoration(
60 0 : color: context.colorScheme.surface,
61 0 : borderRadius: BorderRadius.circular(3),
62 0 : boxShadow: [
63 0 : BoxShadow(
64 0 : color: context.colorScheme.secondary.withValues(alpha: 0.1),
65 0 : spreadRadius: indent / 2,
66 : blurRadius: indent,
67 0 : offset: Offset(0, indent / 2),
68 : ),
69 : ],
70 : ),
71 0 : child: isLoading
72 0 : ? LoadingWidget(isLoading: isLoading, size: Size(widget.size, widget.size))
73 0 : : GridContainer(
74 0 : alignment: AppDesign.getAlignment<MainAxisAlignment>(),
75 : rows: const [40, null, 40],
76 : columns: const [40, null, 34],
77 0 : children: [
78 0 : GridItem(
79 : start: const Size(0, 0),
80 : end: const Size(4, 4),
81 0 : child: SizedBox(
82 0 : child: ClipPath(
83 0 : clipper: _TopRightClipper(),
84 0 : child: Image.asset(widget.path),
85 : ),
86 : ),
87 : ),
88 0 : GridItem(
89 : start: const Size(2, 0),
90 : end: const Size(3, 1),
91 0 : child: Icon(
92 0 : switch (widget.purchase?.last.status) {
93 0 : PurchaseStatus.purchased => Icons.check,
94 0 : PurchaseStatus.pending => Icons.pending,
95 : _ => Icons.add
96 : },
97 0 : color: context.colorScheme.secondary.withValues(alpha: 0.3),
98 : ),
99 : ),
100 0 : GridItem(
101 : start: const Size(0, 2),
102 : end: const Size(3, 3),
103 : order: 2,
104 0 : child: Container(
105 0 : color: context.colorScheme.surface.withValues(alpha: 0.9),
106 0 : padding: EdgeInsets.all(indent),
107 0 : child: RowWidget(
108 0 : chunk: [null, costWidth],
109 : indent: indent,
110 : alignment: MainAxisAlignment.spaceBetween,
111 0 : maxWidth: widget.size - indent * 2,
112 0 : children: [
113 0 : [
114 0 : Align(
115 : alignment: Alignment.centerLeft,
116 0 : child: TextWrapper(widget.title, maxLines: 2, style: context.textTheme.bodySmall)),
117 : ],
118 0 : [
119 0 : Align(
120 : alignment: Alignment.centerRight,
121 0 : child: TextWrapper(cost, style: context.textTheme.numberMedium),
122 : ),
123 : ],
124 : ],
125 : )),
126 : ),
127 : ],
128 : ),
129 : ),
130 : );
131 : }
132 : }
133 :
134 : class _TopRightClipper extends CustomClipper<Path> {
135 0 : @override
136 : Path getClip(Size size) {
137 0 : final path = Path()
138 0 : ..moveTo(size.width / 3, 0)
139 0 : ..quadraticBezierTo(size.width * 3 / 4, 0, size.width, size.height / 2.5)
140 0 : ..lineTo(size.width, size.height)
141 0 : ..lineTo(0, size.height)
142 0 : ..lineTo(0, 0);
143 :
144 : return path;
145 : }
146 :
147 0 : @override
148 : bool shouldReclip(CustomClipper<Path> oldClipper) => false;
149 : }
|