diff --git a/lib/widgets/payment/payment_overlay.dart b/lib/widgets/payment/payment_overlay.dart index dfb9b539..1acf2c12 100644 --- a/lib/widgets/payment/payment_overlay.dart +++ b/lib/widgets/payment/payment_overlay.dart @@ -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() { diff --git a/lib/widgets/post/post_award_sheet.dart b/lib/widgets/post/post_award_sheet.dart new file mode 100644 index 00000000..06b187e6 --- /dev/null +++ b/lib/widgets/post/post_award_sheet.dart @@ -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(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( + segments: [ + ButtonSegment( + value: 0, + label: Text('awardAttitudePositive'.tr()), + icon: const Icon(Symbols.thumb_up), + ), + ButtonSegment( + value: 2, + label: Text('awardAttitudeNegative'.tr()), + icon: const Icon(Symbols.thumb_down), + ), + ], + selected: {selectedAttitude.value}, + onSelectionChanged: (Set 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 _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); + } + } + } +}