Compare commits

..

2 Commits

Author SHA1 Message Date
44dbfc36d9 💄 Optimized wallet screen 2025-10-04 20:38:42 +08:00
5dbe7371cb Transaction details 2025-10-04 20:33:34 +08:00
6 changed files with 253 additions and 60 deletions

View File

@@ -481,6 +481,7 @@
"pinCode": "PIN Code", "pinCode": "PIN Code",
"biometric": "Biometric", "biometric": "Biometric",
"enterPinToConfirm": "Enter your 6-digit PIN to confirm payment", "enterPinToConfirm": "Enter your 6-digit PIN to confirm payment",
"enterPin": "Enter your PIN code",
"clearPin": "Clear PIN", "clearPin": "Clear PIN",
"useBiometricToConfirm": "Use biometric authentication to confirm payment", "useBiometricToConfirm": "Use biometric authentication to confirm payment",
"touchSensorToAuthenticate": "Touch the sensor to authenticate", "touchSensorToAuthenticate": "Touch the sensor to authenticate",
@@ -1176,5 +1177,14 @@
"noRecipientsSelected": "No recipients selected", "noRecipientsSelected": "No recipients selected",
"selectRecipientsToSendFund": "Select recipients to send the fund to", "selectRecipientsToSendFund": "Select recipients to send the fund to",
"addRecipient": "Add Recipient", "addRecipient": "Add Recipient",
"addMoreRecipients": "Add More Recipients" "addMoreRecipients": "Add More Recipients",
"transactionDetails": "Transaction Details",
"remarks": "Remarks",
"payer": "Payer",
"payee": "Payee",
"transactionType": "Transaction Type",
"transfer": "Transfer",
"payment": "Payment",
"systemWallet": "System Wallet",
"date": "Date"
} }

View File

@@ -1075,5 +1075,6 @@
"deleteRecycledFiles": "删除被回收的文件", "deleteRecycledFiles": "删除被回收的文件",
"recycledFilesDeleted": "被回收文件成功删除", "recycledFilesDeleted": "被回收文件成功删除",
"failedToDeleteRecycledFiles": "删除被回收文件失败", "failedToDeleteRecycledFiles": "删除被回收文件失败",
"upload": "上传" "upload": "上传",
"systemWallet": "中央统筹"
} }

View File

@@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/account.dart'; import 'package:island/models/account.dart';
import 'package:island/models/wallet.dart'; import 'package:island/models/wallet.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/account/account_picker.dart'; import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
@@ -284,37 +285,6 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
), ),
); );
}), }),
if (selectedRecipients.length < 10)
OutlinedButton.icon(
onPressed: () async {
final recipient =
await showModalBottomSheet<SnAccount>(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder:
(context) =>
const AccountPickerSheet(),
);
if (recipient != null &&
!selectedRecipients.contains(
recipient,
)) {
setState(
() =>
selectedRecipients.add(recipient),
);
}
},
icon: const Icon(Icons.person_add),
label: Text('addRecipient'.tr()),
style: OutlinedButton.styleFrom(
minimumSize: const Size(
double.infinity,
48,
),
),
).padding(all: 16),
], ],
) )
: Column( : Column(
@@ -469,7 +439,7 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
bottom: MediaQuery.of(context).viewInsets.bottom, bottom: MediaQuery.of(context).viewInsets.bottom,
), ),
child: SheetScaffold( child: SheetScaffold(
titleText: 'enterPinToConfirm'.tr(), titleText: 'enterPin'.tr(),
heightFactor: 0.5, heightFactor: 0.5,
child: Padding( child: Padding(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
@@ -480,7 +450,7 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
'enterPinToConfirm'.tr(), 'enterPinToConfirmPayment'.tr(),
style: Theme.of(context).textTheme.titleMedium style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w500), ?.copyWith(fontWeight: FontWeight.w500),
textAlign: TextAlign.center, textAlign: TextAlign.center,
@@ -659,6 +629,173 @@ Future<SnWalletFund> walletFund(Ref ref, String fundId) async {
return SnWalletFund.fromJson(resp.data); return SnWalletFund.fromJson(resp.data);
} }
class TransactionDetailSheet extends StatelessWidget {
final SnTransaction transaction;
const TransactionDetailSheet({super.key, required this.transaction});
@override
Widget build(BuildContext context) {
final isIncome =
transaction.payeeWalletId == null ||
transaction.payeeWallet?.accountId == null;
return SheetScaffold(
titleText: 'transactionDetails'.tr(),
heightFactor: 0.75,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Amount
Text(
'amount'.tr(),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.primary,
),
),
const Gap(4),
Text(
'${transaction.amount.toStringAsFixed(2)} ${transaction.currency}',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isIncome ? Colors.green : Colors.red,
),
),
const Gap(16),
// Remarks
if (transaction.remarks != null &&
transaction.remarks!.isNotEmpty) ...[
Text(
'remarks'.tr(),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.primary,
),
),
const Gap(4),
Text(
transaction.remarks!,
style: Theme.of(context).textTheme.bodyMedium,
),
const Gap(16),
],
// Date
Text(
'date'.tr(),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.primary,
),
),
const Gap(4),
Text(
DateFormat.yMd().add_Hm().format(transaction.createdAt),
style: Theme.of(context).textTheme.bodyMedium,
),
const Gap(16),
// Payer
Text(
'payer'.tr(),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.primary,
),
),
const Gap(4),
AccountPfcGestureDetector(
uname: transaction.payerWallet?.account?.name,
child: Row(
spacing: 8,
children: [
if (transaction.payerWallet?.account != null)
ProfilePictureWidget(
file: transaction.payerWallet!.account!.profile.picture,
radius: 12,
),
Text(
transaction.payerWallet?.account?.nick ??
'systemWallet'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
const Gap(16),
// Payee
Text(
'payee'.tr(),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.primary,
),
),
const Gap(4),
AccountPfcGestureDetector(
uname: transaction.payeeWallet?.account?.name,
child: Row(
spacing: 8,
children: [
if (transaction.payeeWallet?.account != null)
ProfilePictureWidget(
file: transaction.payeeWallet!.account!.profile.picture,
radius: 12,
),
Text(
transaction.payeeWallet?.account?.nick ??
'systemWallet'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
const Gap(16),
// Transaction Type
Text(
'transactionType'.tr(),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.primary,
),
),
const Gap(4),
Text(
_getTransactionTypeText(transaction.type),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
);
}
String _getTransactionTypeText(int type) {
// Assuming types: 0: transfer, 1: payment, etc. Adjust based on actual types
switch (type) {
case 0:
return 'transfer'.tr();
case 1:
return 'payment'.tr();
default:
return 'unknown'.tr();
}
}
}
class WalletScreen extends HookConsumerWidget { class WalletScreen extends HookConsumerWidget {
const WalletScreen({super.key}); const WalletScreen({super.key});
@@ -694,6 +831,29 @@ class WalletScreen extends HookConsumerWidget {
return 'walletCurrency${isShort ? 'Short' : ''}${currency[0].toUpperCase()}${currency.substring(1).toLowerCase()}'; return 'walletCurrency${isShort ? 'Short' : ''}${currency[0].toUpperCase()}${currency.substring(1).toLowerCase()}';
} }
List<SnWalletPocket> _getAllCurrencies(List<SnWalletPocket> pockets) {
final allCurrencies = <String>{};
allCurrencies.addAll(kCurrencyIconData.keys);
allCurrencies.addAll(pockets.map((p) => p.currency));
return allCurrencies.map((currency) {
final existingPocket = pockets.firstWhere(
(p) => p.currency == currency,
orElse:
() => SnWalletPocket(
id: '',
currency: currency,
amount: 0.0,
walletId: '',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
deletedAt: null,
),
);
return existingPocket;
}).toList();
}
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: Text('wallet').tr(), title: Text('wallet').tr(),
@@ -740,7 +900,7 @@ class WalletScreen extends HookConsumerWidget {
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: Column( child: Column(
children: [ children: [
...data.pockets.map( ..._getAllCurrencies(data.pockets).map(
(pocket) => ListTile( (pocket) => ListTile(
leading: Icon( leading: Icon(
kCurrencyIconData[pocket.currency] ?? kCurrencyIconData[pocket.currency] ??
@@ -764,6 +924,8 @@ class WalletScreen extends HookConsumerWidget {
).padding(horizontal: 12, top: 12), ).padding(horizontal: 12, top: 12),
), ),
SliverGap(8),
// Tab Bar // Tab Bar
SliverToBoxAdapter( SliverToBoxAdapter(
child: TabBar( child: TabBar(
@@ -802,27 +964,41 @@ class WalletScreen extends HookConsumerWidget {
final isIncome = final isIncome =
transaction.payeeWalletId == wallet.value?.id; transaction.payeeWalletId == wallet.value?.id;
return ListTile( return InkWell(
key: ValueKey(transaction.id), onTap: () {
leading: Icon( showModalBottomSheet(
isIncome context: context,
? Symbols.payment_arrow_down useRootNavigator: true,
: Symbols.paid, isScrollControlled: true,
), builder:
title: Text( (context) => TransactionDetailSheet(
transaction.remarks ?? '', transaction: transaction,
maxLines: 1, ),
overflow: TextOverflow.ellipsis, );
), },
subtitle: Text( child: ListTile(
DateFormat.yMd().add_Hm().format( key: ValueKey(transaction.id),
transaction.createdAt, leading: Icon(
isIncome
? Symbols.payment_arrow_down
: Symbols.paid,
), ),
), title: Text(
trailing: Text( transaction.remarks ?? '',
'${isIncome ? '+' : '-'}${transaction.amount.toStringAsFixed(2)} ${transaction.currency}', maxLines: 1,
style: TextStyle( overflow: TextOverflow.ellipsis,
color: isIncome ? Colors.green : Colors.red, ),
subtitle: Text(
DateFormat.yMd().add_Hm().format(
transaction.createdAt,
),
),
trailing: Text(
'${isIncome ? '+' : '-'}${transaction.amount.toStringAsFixed(2)} ${transaction.currency}',
style: TextStyle(
color:
isIncome ? Colors.green : Colors.red,
),
), ),
), ),
); );

View File

@@ -189,7 +189,7 @@ class AccountProfileCard extends HookConsumerWidget {
} }
class AccountPfcGestureDetector extends StatelessWidget { class AccountPfcGestureDetector extends StatelessWidget {
final String uname; final String? uname;
final Widget child; final Widget child;
const AccountPfcGestureDetector({ const AccountPfcGestureDetector({
super.key, super.key,
@@ -202,7 +202,13 @@ class AccountPfcGestureDetector extends StatelessWidget {
return GestureDetector( return GestureDetector(
child: child, child: child,
onTapDown: (details) { onTapDown: (details) {
showAccountProfileCard(context, uname, offset: details.localPosition); if (uname != null) {
showAccountProfileCard(
context,
uname!,
offset: details.localPosition,
);
}
}, },
); );
} }

View File

@@ -188,7 +188,7 @@ Add these keys to your localization files:
"description": "Description", "description": "Description",
"pinCode": "PIN Code", "pinCode": "PIN Code",
"biometric": "Biometric", "biometric": "Biometric",
"enterPinToConfirm": "Enter your 6-digit PIN to confirm payment", "enterPinToConfirmPayment": "Enter your 6-digit PIN to confirm payment",
"clearPin": "Clear PIN", "clearPin": "Clear PIN",
"useBiometricToConfirm": "Use biometric authentication to confirm payment", "useBiometricToConfirm": "Use biometric authentication to confirm payment",
"touchSensorToAuthenticate": "Touch the sensor to authenticate", "touchSensorToAuthenticate": "Touch the sensor to authenticate",

View File

@@ -385,7 +385,7 @@ class _PaymentContentState extends ConsumerState<_PaymentContent> {
return Column( return Column(
children: [ children: [
Text( Text(
'enterPinToConfirm'.tr(), 'enterPinToConfirmPayment'.tr(),
style: Theme.of( style: Theme.of(
context, context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500),