✨ Post award
This commit is contained in:
		| @@ -270,6 +270,8 @@ class _PaymentContentState extends ConsumerState<_PaymentContent> { | |||||||
|           } |           } | ||||||
|         } else if (err.response?.statusCode == 400) { |         } else if (err.response?.statusCode == 400) { | ||||||
|           errorMessage = err.response?.data?['error'] ?? errorMessage; |           errorMessage = err.response?.data?['error'] ?? errorMessage; | ||||||
|  |         } else { | ||||||
|  |           rethrow; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       throw errorMessage; |       throw errorMessage; | ||||||
| @@ -419,42 +421,48 @@ class _PaymentContentState extends ConsumerState<_PaymentContent> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget _buildBiometricAuth() { |   Widget _buildBiometricAuth() { | ||||||
|     return Column( |     return SingleChildScrollView( | ||||||
|       mainAxisAlignment: MainAxisAlignment.center, |       child: | ||||||
|       crossAxisAlignment: CrossAxisAlignment.center, |           Column( | ||||||
|       children: [ |             mainAxisAlignment: MainAxisAlignment.center, | ||||||
|         Icon(Symbols.fingerprint, size: 48), |             crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|         const Gap(16), |             children: [ | ||||||
|         Text( |               Icon(Symbols.fingerprint, size: 48), | ||||||
|           'useBiometricToConfirm'.tr(), |               const Gap(16), | ||||||
|           style: Theme.of( |               Text( | ||||||
|             context, |                 'useBiometricToConfirm'.tr(), | ||||||
|           ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), |                 style: Theme.of( | ||||||
|           textAlign: TextAlign.center, |                   context, | ||||||
|         ), |                 ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), | ||||||
|         Text( |                 textAlign: TextAlign.center, | ||||||
|           'The biometric data will only be processed on your device', |               ), | ||||||
|           style: Theme.of(context).textTheme.bodyMedium?.copyWith( |               Text( | ||||||
|             color: Theme.of(context).colorScheme.onSurfaceVariant, |                 'The biometric data will only be processed on your device', | ||||||
|             fontSize: 11, |                 style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||||
|           ), |                   color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||||
|           textAlign: TextAlign.center, |                   fontSize: 11, | ||||||
|         ).opacity(0.75), |                 ), | ||||||
|         const Gap(28), |                 textAlign: TextAlign.center, | ||||||
|         ElevatedButton.icon( |               ).opacity(0.75), | ||||||
|           onPressed: _authenticateWithBiometric, |               const Gap(28), | ||||||
|           icon: const Icon(Symbols.fingerprint), |               ElevatedButton.icon( | ||||||
|           label: Text('authenticateNow'.tr()), |                 onPressed: _authenticateWithBiometric, | ||||||
|           style: ElevatedButton.styleFrom( |                 icon: const Icon(Symbols.fingerprint), | ||||||
|             padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), |                 label: Text('authenticateNow'.tr()), | ||||||
|           ), |                 style: ElevatedButton.styleFrom( | ||||||
|         ), |                   padding: const EdgeInsets.symmetric( | ||||||
|         TextButton( |                     horizontal: 24, | ||||||
|           onPressed: () => _fallbackToPinMode(null), |                     vertical: 12, | ||||||
|           child: Text('usePinInstead'.tr()), |                   ), | ||||||
|         ), |                 ), | ||||||
|       ], |               ), | ||||||
|     ).center(); |               TextButton( | ||||||
|  |                 onPressed: () => _fallbackToPinMode(null), | ||||||
|  |                 child: Text('usePinInstead'.tr()), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ).center(), | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget _buildActionButtons() { |   Widget _buildActionButtons() { | ||||||
|   | |||||||
							
								
								
									
										287
									
								
								lib/widgets/post/post_award_sheet.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								lib/widgets/post/post_award_sheet.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,287 @@ | |||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
|  | import 'package:gap/gap.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:island/models/post.dart'; | ||||||
|  | import 'package:island/models/wallet.dart'; | ||||||
|  | import 'package:island/pods/network.dart'; | ||||||
|  | import 'package:island/widgets/alert.dart'; | ||||||
|  | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
|  | import 'package:island/widgets/content/sheet.dart'; | ||||||
|  | import 'package:island/widgets/payment/payment_overlay.dart'; | ||||||
|  | import 'package:material_symbols_icons/symbols.dart'; | ||||||
|  |  | ||||||
|  | class PostAwardSheet extends HookConsumerWidget { | ||||||
|  |   final SnPost post; | ||||||
|  |   const PostAwardSheet({super.key, required this.post}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final messageController = useTextEditingController(); | ||||||
|  |     final amountController = useTextEditingController(); | ||||||
|  |     final selectedAttitude = useState<int>(0); // 0 for positive, 2 for negative | ||||||
|  |  | ||||||
|  |     return SheetScaffold( | ||||||
|  |       titleText: 'awardPost'.tr(), | ||||||
|  |       child: SingleChildScrollView( | ||||||
|  |         padding: const EdgeInsets.all(16.0), | ||||||
|  |         child: Column( | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |           children: [ | ||||||
|  |             // Post Preview Section | ||||||
|  |             _buildPostPreview(context), | ||||||
|  |             const Gap(20), | ||||||
|  |  | ||||||
|  |             // Award Result Explanation | ||||||
|  |             _buildAwardResultExplanation(context), | ||||||
|  |             const Gap(20), | ||||||
|  |  | ||||||
|  |             Text( | ||||||
|  |               'awardMessage'.tr(), | ||||||
|  |               style: Theme.of(context).textTheme.titleMedium, | ||||||
|  |             ), | ||||||
|  |             const Gap(8), | ||||||
|  |             TextField( | ||||||
|  |               controller: messageController, | ||||||
|  |               maxLines: 3, | ||||||
|  |               decoration: InputDecoration( | ||||||
|  |                 hintText: 'awardMessageHint'.tr(), | ||||||
|  |                 border: const OutlineInputBorder( | ||||||
|  |                   borderRadius: BorderRadius.all(Radius.circular(16)), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |             const Gap(16), | ||||||
|  |             Text( | ||||||
|  |               'awardAttitude'.tr(), | ||||||
|  |               style: Theme.of(context).textTheme.titleMedium, | ||||||
|  |             ), | ||||||
|  |             const Gap(8), | ||||||
|  |             SegmentedButton<int>( | ||||||
|  |               segments: [ | ||||||
|  |                 ButtonSegment<int>( | ||||||
|  |                   value: 0, | ||||||
|  |                   label: Text('awardAttitudePositive'.tr()), | ||||||
|  |                   icon: const Icon(Symbols.thumb_up), | ||||||
|  |                 ), | ||||||
|  |                 ButtonSegment<int>( | ||||||
|  |                   value: 2, | ||||||
|  |                   label: Text('awardAttitudeNegative'.tr()), | ||||||
|  |                   icon: const Icon(Symbols.thumb_down), | ||||||
|  |                 ), | ||||||
|  |               ], | ||||||
|  |               selected: {selectedAttitude.value}, | ||||||
|  |               onSelectionChanged: (Set<int> selection) { | ||||||
|  |                 selectedAttitude.value = selection.first; | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|  |             const Gap(16), | ||||||
|  |             Text( | ||||||
|  |               'awardAmount'.tr(), | ||||||
|  |               style: Theme.of(context).textTheme.titleMedium, | ||||||
|  |             ), | ||||||
|  |             const Gap(8), | ||||||
|  |             TextField( | ||||||
|  |               controller: amountController, | ||||||
|  |               keyboardType: TextInputType.number, | ||||||
|  |               decoration: InputDecoration( | ||||||
|  |                 hintText: 'awardAmountHint'.tr(), | ||||||
|  |                 border: const OutlineInputBorder( | ||||||
|  |                   borderRadius: BorderRadius.all(Radius.circular(16)), | ||||||
|  |                 ), | ||||||
|  |                 suffixText: 'NSP', | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |             const Gap(24), | ||||||
|  |             SizedBox( | ||||||
|  |               width: double.infinity, | ||||||
|  |               child: FilledButton.icon( | ||||||
|  |                 onPressed: | ||||||
|  |                     () => _submitAward( | ||||||
|  |                       context, | ||||||
|  |                       ref, | ||||||
|  |                       messageController, | ||||||
|  |                       amountController, | ||||||
|  |                       selectedAttitude.value, | ||||||
|  |                     ), | ||||||
|  |                 icon: const Icon(Symbols.star), | ||||||
|  |                 label: Text('awardSubmit'.tr()), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildPostPreview(BuildContext context) { | ||||||
|  |     return Container( | ||||||
|  |       padding: const EdgeInsets.all(12), | ||||||
|  |       decoration: BoxDecoration( | ||||||
|  |         color: Theme.of(context).colorScheme.surfaceContainerHighest, | ||||||
|  |         borderRadius: BorderRadius.circular(12), | ||||||
|  |         border: Border.all( | ||||||
|  |           color: Theme.of(context).colorScheme.outline.withOpacity(0.2), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       child: Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |         children: [ | ||||||
|  |           Row( | ||||||
|  |             children: [ | ||||||
|  |               Icon( | ||||||
|  |                 Symbols.article, | ||||||
|  |                 size: 20, | ||||||
|  |                 color: Theme.of(context).colorScheme.primary, | ||||||
|  |               ), | ||||||
|  |               const Gap(8), | ||||||
|  |               Text( | ||||||
|  |                 'awardPostPreview'.tr(), | ||||||
|  |                 style: Theme.of( | ||||||
|  |                   context, | ||||||
|  |                 ).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |           const Gap(8), | ||||||
|  |           Text( | ||||||
|  |             post.content ?? 'awardNoContent'.tr(), | ||||||
|  |             maxLines: 2, | ||||||
|  |             overflow: TextOverflow.ellipsis, | ||||||
|  |             style: Theme.of(context).textTheme.bodyMedium, | ||||||
|  |           ), | ||||||
|  |           ...[ | ||||||
|  |             const Gap(4), | ||||||
|  |             Row( | ||||||
|  |               spacing: 6, | ||||||
|  |               children: [ | ||||||
|  |                 Text( | ||||||
|  |                   'awardByPublisher'.tr(args: ['@${post.publisher.name}']), | ||||||
|  |                   style: Theme.of(context).textTheme.bodySmall?.copyWith( | ||||||
|  |                     color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |                 ProfilePictureWidget(file: post.publisher.picture, radius: 8), | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildAwardResultExplanation(BuildContext context) { | ||||||
|  |     return Container( | ||||||
|  |       padding: const EdgeInsets.all(12), | ||||||
|  |       decoration: BoxDecoration( | ||||||
|  |         color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3), | ||||||
|  |         borderRadius: BorderRadius.circular(12), | ||||||
|  |         border: Border.all( | ||||||
|  |           color: Theme.of(context).colorScheme.primary.withOpacity(0.2), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       child: Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |         children: [ | ||||||
|  |           Row( | ||||||
|  |             children: [ | ||||||
|  |               Icon( | ||||||
|  |                 Symbols.info, | ||||||
|  |                 size: 20, | ||||||
|  |                 color: Theme.of(context).colorScheme.primary, | ||||||
|  |               ), | ||||||
|  |               const Gap(8), | ||||||
|  |               Text( | ||||||
|  |                 'awardBenefits'.tr(), | ||||||
|  |                 style: Theme.of(context).textTheme.titleSmall?.copyWith( | ||||||
|  |                   fontWeight: FontWeight.w600, | ||||||
|  |                   color: Theme.of(context).colorScheme.primary, | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |           const Gap(8), | ||||||
|  |           Text( | ||||||
|  |             'awardBenefitsDescription'.tr(), | ||||||
|  |             style: Theme.of(context).textTheme.bodySmall?.copyWith( | ||||||
|  |               color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> _submitAward( | ||||||
|  |     BuildContext context, | ||||||
|  |     WidgetRef ref, | ||||||
|  |     TextEditingController messageController, | ||||||
|  |     TextEditingController amountController, | ||||||
|  |     int selectedAttitude, | ||||||
|  |   ) async { | ||||||
|  |     // Get values from controllers | ||||||
|  |     final message = messageController.text.trim(); | ||||||
|  |     final amountText = amountController.text.trim(); | ||||||
|  |  | ||||||
|  |     // Validate inputs | ||||||
|  |     if (amountText.isEmpty) { | ||||||
|  |       showSnackBar('awardAmountRequired'.tr()); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     final amount = double.tryParse(amountText); | ||||||
|  |     if (amount == null || amount <= 0) { | ||||||
|  |       showSnackBar('awardAmountInvalid'.tr()); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (message.length > 4096) { | ||||||
|  |       showSnackBar('awardMessageTooLong'.tr()); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       showLoadingModal(context); | ||||||
|  |  | ||||||
|  |       final client = ref.read(apiClientProvider); | ||||||
|  |  | ||||||
|  |       // Send award request | ||||||
|  |       final awardResponse = await client.post( | ||||||
|  |         '/sphere/posts/${post.id}/awards', | ||||||
|  |         data: { | ||||||
|  |           'amount': amount, | ||||||
|  |           'attitude': selectedAttitude, | ||||||
|  |           if (message.isNotEmpty) 'message': message, | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       final orderId = awardResponse.data['order_id'] as String; | ||||||
|  |  | ||||||
|  |       // Fetch order details | ||||||
|  |       final orderResponse = await client.get('/id/orders/$orderId'); | ||||||
|  |       final order = SnWalletOrder.fromJson(orderResponse.data); | ||||||
|  |  | ||||||
|  |       if (context.mounted) { | ||||||
|  |         hideLoadingModal(context); | ||||||
|  |  | ||||||
|  |         // Show payment overlay | ||||||
|  |         final paidOrder = await PaymentOverlay.show( | ||||||
|  |           context: context, | ||||||
|  |           order: order, | ||||||
|  |           enableBiometric: true, | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         if (paidOrder != null && context.mounted) { | ||||||
|  |           showSnackBar('awardSuccess'.tr()); | ||||||
|  |           Navigator.of(context).pop(); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } catch (err) { | ||||||
|  |       if (context.mounted) { | ||||||
|  |         hideLoadingModal(context); | ||||||
|  |         showErrorAlert(err); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user