|
| 1 | +import 'package:flutter/material.dart'; |
| 2 | + |
| 3 | +import '../../services/cakepay/cakepay_service.dart'; |
| 4 | +import '../../services/cakepay/src/models/order.dart'; |
| 5 | +import '../../themes/stack_colors.dart'; |
| 6 | +import '../../utilities/text_styles.dart'; |
| 7 | +import '../../utilities/util.dart'; |
| 8 | +import '../../widgets/background.dart'; |
| 9 | +import '../../widgets/conditional_parent.dart'; |
| 10 | +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; |
| 11 | +import '../../widgets/desktop/desktop_dialog.dart'; |
| 12 | +import '../../widgets/desktop/desktop_dialog_close_button.dart'; |
| 13 | +import '../../widgets/rounded_white_container.dart'; |
| 14 | +import 'cakepay_order_view.dart'; |
| 15 | + |
| 16 | +class CakePayOrdersView extends StatefulWidget { |
| 17 | + const CakePayOrdersView({super.key}); |
| 18 | + |
| 19 | + static const String routeName = "/cakePayOrders"; |
| 20 | + |
| 21 | + @override |
| 22 | + State<CakePayOrdersView> createState() => _CakePayOrdersViewState(); |
| 23 | +} |
| 24 | + |
| 25 | +class _CakePayOrdersViewState extends State<CakePayOrdersView> { |
| 26 | + List<CakePayOrder> _orders = []; |
| 27 | + bool _syncing = false; |
| 28 | + |
| 29 | + @override |
| 30 | + void initState() { |
| 31 | + super.initState(); |
| 32 | + _syncFromApi(); |
| 33 | + } |
| 34 | + |
| 35 | + /// Fetch each locally-tracked order ID individually via getOrder() |
| 36 | + /// (which works with the seller API key, unlike getMyOrders()). |
| 37 | + /// Mirrors ShopInBit's _syncFromApi() pattern. |
| 38 | + Future<void> _syncFromApi() async { |
| 39 | + setState(() => _syncing = true); |
| 40 | + try { |
| 41 | + final orderIds = CakePayService.instance.getOrderIds(); |
| 42 | + final results = <CakePayOrder>[]; |
| 43 | + |
| 44 | + for (final id in orderIds) { |
| 45 | + final resp = await CakePayService.instance.client.getOrder(id); |
| 46 | + if (!resp.hasError && resp.value != null) { |
| 47 | + var order = resp.value!; |
| 48 | + final override = |
| 49 | + CakePayService.devStatusOverrides[order.orderId]; |
| 50 | + if (override != null) { |
| 51 | + order = order.copyWith(status: override); |
| 52 | + } |
| 53 | + results.add(order); |
| 54 | + } |
| 55 | + } |
| 56 | + |
| 57 | + if (mounted) { |
| 58 | + setState(() { |
| 59 | + _orders = results; |
| 60 | + }); |
| 61 | + } |
| 62 | + } catch (_) { |
| 63 | + // Fall back to empty list — no local cache to fall back on |
| 64 | + } finally { |
| 65 | + if (mounted) { |
| 66 | + setState(() => _syncing = false); |
| 67 | + } |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + String _statusLabel(CakePayOrderStatus status) { |
| 72 | + switch (status) { |
| 73 | + case CakePayOrderStatus.new_: |
| 74 | + return "New"; |
| 75 | + case CakePayOrderStatus.expiredButStillPending: |
| 76 | + return "Expired (pending)"; |
| 77 | + case CakePayOrderStatus.expired: |
| 78 | + return "Expired"; |
| 79 | + case CakePayOrderStatus.failed: |
| 80 | + return "Failed"; |
| 81 | + case CakePayOrderStatus.paid: |
| 82 | + return "Paid"; |
| 83 | + case CakePayOrderStatus.paidPartial: |
| 84 | + return "Partially paid"; |
| 85 | + case CakePayOrderStatus.pendingPurchase: |
| 86 | + return "Pending purchase"; |
| 87 | + case CakePayOrderStatus.purchaseProcessing: |
| 88 | + return "Processing"; |
| 89 | + case CakePayOrderStatus.purchased: |
| 90 | + return "Purchased"; |
| 91 | + case CakePayOrderStatus.pendingEmail: |
| 92 | + return "Pending email"; |
| 93 | + case CakePayOrderStatus.complete: |
| 94 | + return "Complete"; |
| 95 | + case CakePayOrderStatus.pendingRefund: |
| 96 | + return "Pending refund"; |
| 97 | + case CakePayOrderStatus.refunded: |
| 98 | + return "Refunded"; |
| 99 | + } |
| 100 | + } |
| 101 | + |
| 102 | + Color _statusColor(BuildContext context, CakePayOrderStatus status) { |
| 103 | + final colors = Theme.of(context).extension<StackColors>()!; |
| 104 | + switch (status) { |
| 105 | + case CakePayOrderStatus.complete: |
| 106 | + case CakePayOrderStatus.purchased: |
| 107 | + return colors.accentColorGreen; |
| 108 | + case CakePayOrderStatus.new_: |
| 109 | + case CakePayOrderStatus.paid: |
| 110 | + case CakePayOrderStatus.paidPartial: |
| 111 | + return colors.accentColorBlue; |
| 112 | + case CakePayOrderStatus.pendingPurchase: |
| 113 | + case CakePayOrderStatus.purchaseProcessing: |
| 114 | + case CakePayOrderStatus.pendingEmail: |
| 115 | + case CakePayOrderStatus.expiredButStillPending: |
| 116 | + return colors.accentColorYellow; |
| 117 | + case CakePayOrderStatus.expired: |
| 118 | + case CakePayOrderStatus.failed: |
| 119 | + case CakePayOrderStatus.pendingRefund: |
| 120 | + case CakePayOrderStatus.refunded: |
| 121 | + return colors.textSubtitle1; |
| 122 | + } |
| 123 | + } |
| 124 | + |
| 125 | + @override |
| 126 | + Widget build(BuildContext context) { |
| 127 | + final isDesktop = Util.isDesktop; |
| 128 | + |
| 129 | + final list = _orders.isEmpty |
| 130 | + ? Center( |
| 131 | + child: Text( |
| 132 | + _syncing ? "Loading orders..." : "No orders yet", |
| 133 | + style: isDesktop |
| 134 | + ? STextStyles.desktopTextSmall(context) |
| 135 | + : STextStyles.itemSubtitle(context), |
| 136 | + ), |
| 137 | + ) |
| 138 | + : ListView.separated( |
| 139 | + shrinkWrap: isDesktop, |
| 140 | + primary: isDesktop ? false : null, |
| 141 | + itemCount: _orders.length, |
| 142 | + separatorBuilder: (_, __) => SizedBox(height: isDesktop ? 16 : 12), |
| 143 | + itemBuilder: (context, index) { |
| 144 | + final order = _orders[index]; |
| 145 | + return GestureDetector( |
| 146 | + onTap: () { |
| 147 | + if (isDesktop) { |
| 148 | + Navigator.of(context, rootNavigator: true).pop(); |
| 149 | + showDialog<void>( |
| 150 | + context: context, |
| 151 | + builder: (_) => CakePayOrderView(orderId: order.orderId), |
| 152 | + ); |
| 153 | + } else { |
| 154 | + Navigator.of(context).pushNamed( |
| 155 | + CakePayOrderView.routeName, |
| 156 | + arguments: order.orderId, |
| 157 | + ); |
| 158 | + } |
| 159 | + }, |
| 160 | + child: RoundedWhiteContainer( |
| 161 | + child: Row( |
| 162 | + children: [ |
| 163 | + Expanded( |
| 164 | + child: Column( |
| 165 | + crossAxisAlignment: CrossAxisAlignment.start, |
| 166 | + children: [ |
| 167 | + Row( |
| 168 | + mainAxisAlignment: MainAxisAlignment.spaceBetween, |
| 169 | + children: [ |
| 170 | + Text( |
| 171 | + order.orderId.length > 8 |
| 172 | + ? "${order.orderId.substring(0, 8)}..." |
| 173 | + : order.orderId, |
| 174 | + style: isDesktop |
| 175 | + ? STextStyles.desktopTextSmall(context) |
| 176 | + : STextStyles.titleBold12(context), |
| 177 | + ), |
| 178 | + Container( |
| 179 | + padding: const EdgeInsets.symmetric( |
| 180 | + horizontal: 8, |
| 181 | + vertical: 2, |
| 182 | + ), |
| 183 | + decoration: BoxDecoration( |
| 184 | + borderRadius: BorderRadius.circular(8), |
| 185 | + color: _statusColor( |
| 186 | + context, |
| 187 | + order.status, |
| 188 | + ).withValues(alpha: 0.2), |
| 189 | + ), |
| 190 | + child: Text( |
| 191 | + _statusLabel(order.status), |
| 192 | + style: |
| 193 | + (isDesktop |
| 194 | + ? STextStyles.desktopTextExtraExtraSmall( |
| 195 | + context, |
| 196 | + ) |
| 197 | + : STextStyles.itemSubtitle12( |
| 198 | + context, |
| 199 | + )) |
| 200 | + .copyWith( |
| 201 | + color: _statusColor( |
| 202 | + context, |
| 203 | + order.status, |
| 204 | + ), |
| 205 | + ), |
| 206 | + ), |
| 207 | + ), |
| 208 | + ], |
| 209 | + ), |
| 210 | + if (order.amountUsd != null) ...[ |
| 211 | + const SizedBox(height: 4), |
| 212 | + Text( |
| 213 | + "\$${order.amountUsd} USD", |
| 214 | + style: isDesktop |
| 215 | + ? STextStyles.desktopTextExtraExtraSmall( |
| 216 | + context, |
| 217 | + ) |
| 218 | + : STextStyles.itemSubtitle12( |
| 219 | + context, |
| 220 | + ).copyWith( |
| 221 | + color: Theme.of(context) |
| 222 | + .extension<StackColors>()! |
| 223 | + .textSubtitle1, |
| 224 | + ), |
| 225 | + ), |
| 226 | + ], |
| 227 | + ], |
| 228 | + ), |
| 229 | + ), |
| 230 | + SizedBox(width: isDesktop ? 16 : 8), |
| 231 | + Icon( |
| 232 | + Icons.chevron_right, |
| 233 | + color: Theme.of( |
| 234 | + context, |
| 235 | + ).extension<StackColors>()!.textSubtitle1, |
| 236 | + ), |
| 237 | + ], |
| 238 | + ), |
| 239 | + ), |
| 240 | + ); |
| 241 | + }, |
| 242 | + ); |
| 243 | + |
| 244 | + final content = Stack( |
| 245 | + children: [ |
| 246 | + list, |
| 247 | + if (_syncing) |
| 248 | + const Center( |
| 249 | + child: SizedBox( |
| 250 | + width: 24, |
| 251 | + height: 24, |
| 252 | + child: CircularProgressIndicator(strokeWidth: 2), |
| 253 | + ), |
| 254 | + ), |
| 255 | + ], |
| 256 | + ); |
| 257 | + |
| 258 | + return ConditionalParent( |
| 259 | + condition: isDesktop, |
| 260 | + builder: (child) => DesktopDialog( |
| 261 | + maxWidth: 580, |
| 262 | + maxHeight: 550, |
| 263 | + child: Column( |
| 264 | + children: [ |
| 265 | + Row( |
| 266 | + mainAxisAlignment: MainAxisAlignment.spaceBetween, |
| 267 | + children: [ |
| 268 | + Padding( |
| 269 | + padding: const EdgeInsets.only(left: 32), |
| 270 | + child: Text( |
| 271 | + "My Orders", |
| 272 | + style: STextStyles.desktopH3(context), |
| 273 | + ), |
| 274 | + ), |
| 275 | + const DesktopDialogCloseButton(), |
| 276 | + ], |
| 277 | + ), |
| 278 | + Expanded( |
| 279 | + child: Padding( |
| 280 | + padding: const EdgeInsets.symmetric( |
| 281 | + horizontal: 32, |
| 282 | + vertical: 16, |
| 283 | + ), |
| 284 | + child: child, |
| 285 | + ), |
| 286 | + ), |
| 287 | + ], |
| 288 | + ), |
| 289 | + ), |
| 290 | + child: ConditionalParent( |
| 291 | + condition: !isDesktop, |
| 292 | + builder: (child) => Background( |
| 293 | + child: Scaffold( |
| 294 | + backgroundColor: Theme.of( |
| 295 | + context, |
| 296 | + ).extension<StackColors>()!.background, |
| 297 | + appBar: AppBar( |
| 298 | + leading: AppBarBackButton( |
| 299 | + onPressed: () => Navigator.of(context).pop(), |
| 300 | + ), |
| 301 | + title: Text("My Orders", style: STextStyles.navBarTitle(context)), |
| 302 | + ), |
| 303 | + body: SafeArea( |
| 304 | + child: Padding(padding: const EdgeInsets.all(16), child: child), |
| 305 | + ), |
| 306 | + ), |
| 307 | + ), |
| 308 | + child: content, |
| 309 | + ), |
| 310 | + ); |
| 311 | + } |
| 312 | +} |
0 commit comments