✨ Post award
This commit is contained in:
		| @@ -270,6 +270,8 @@ class _PaymentContentState extends ConsumerState<_PaymentContent> { | ||||
|           } | ||||
|         } else if (err.response?.statusCode == 400) { | ||||
|           errorMessage = err.response?.data?['error'] ?? errorMessage; | ||||
|         } else { | ||||
|           rethrow; | ||||
|         } | ||||
|       } | ||||
|       throw errorMessage; | ||||
| @@ -419,42 +421,48 @@ class _PaymentContentState extends ConsumerState<_PaymentContent> { | ||||
|   } | ||||
|  | ||||
|   Widget _buildBiometricAuth() { | ||||
|     return Column( | ||||
|       mainAxisAlignment: MainAxisAlignment.center, | ||||
|       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|       children: [ | ||||
|         Icon(Symbols.fingerprint, size: 48), | ||||
|         const Gap(16), | ||||
|         Text( | ||||
|           'useBiometricToConfirm'.tr(), | ||||
|           style: Theme.of( | ||||
|             context, | ||||
|           ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), | ||||
|           textAlign: TextAlign.center, | ||||
|         ), | ||||
|         Text( | ||||
|           'The biometric data will only be processed on your device', | ||||
|           style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||
|             color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|             fontSize: 11, | ||||
|           ), | ||||
|           textAlign: TextAlign.center, | ||||
|         ).opacity(0.75), | ||||
|         const Gap(28), | ||||
|         ElevatedButton.icon( | ||||
|           onPressed: _authenticateWithBiometric, | ||||
|           icon: const Icon(Symbols.fingerprint), | ||||
|           label: Text('authenticateNow'.tr()), | ||||
|           style: ElevatedButton.styleFrom( | ||||
|             padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), | ||||
|           ), | ||||
|         ), | ||||
|         TextButton( | ||||
|           onPressed: () => _fallbackToPinMode(null), | ||||
|           child: Text('usePinInstead'.tr()), | ||||
|         ), | ||||
|       ], | ||||
|     ).center(); | ||||
|     return SingleChildScrollView( | ||||
|       child: | ||||
|           Column( | ||||
|             mainAxisAlignment: MainAxisAlignment.center, | ||||
|             crossAxisAlignment: CrossAxisAlignment.center, | ||||
|             children: [ | ||||
|               Icon(Symbols.fingerprint, size: 48), | ||||
|               const Gap(16), | ||||
|               Text( | ||||
|                 'useBiometricToConfirm'.tr(), | ||||
|                 style: Theme.of( | ||||
|                   context, | ||||
|                 ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), | ||||
|                 textAlign: TextAlign.center, | ||||
|               ), | ||||
|               Text( | ||||
|                 'The biometric data will only be processed on your device', | ||||
|                 style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||
|                   color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|                   fontSize: 11, | ||||
|                 ), | ||||
|                 textAlign: TextAlign.center, | ||||
|               ).opacity(0.75), | ||||
|               const Gap(28), | ||||
|               ElevatedButton.icon( | ||||
|                 onPressed: _authenticateWithBiometric, | ||||
|                 icon: const Icon(Symbols.fingerprint), | ||||
|                 label: Text('authenticateNow'.tr()), | ||||
|                 style: ElevatedButton.styleFrom( | ||||
|                   padding: const EdgeInsets.symmetric( | ||||
|                     horizontal: 24, | ||||
|                     vertical: 12, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               TextButton( | ||||
|                 onPressed: () => _fallbackToPinMode(null), | ||||
|                 child: Text('usePinInstead'.tr()), | ||||
|               ), | ||||
|             ], | ||||
|           ).center(), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   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