Wallet

This commit is contained in:
LittleSheep 2025-05-16 00:11:59 +08:00
parent dfd216b84b
commit 9fcc70d659
5 changed files with 399 additions and 14 deletions

View File

@ -35,3 +35,24 @@ abstract class SnWalletPocket with _$SnWalletPocket {
factory SnWalletPocket.fromJson(Map<String, dynamic> json) => factory SnWalletPocket.fromJson(Map<String, dynamic> json) =>
_$SnWalletPocketFromJson(json); _$SnWalletPocketFromJson(json);
} }
@freezed
abstract class SnTransaction with _$SnTransaction {
const factory SnTransaction({
required String id,
required String currency,
required double amount,
required String? remarks,
required int type,
required String? payerWalletId,
required SnWallet? payerWallet,
required String? payeeWalletId,
required SnWallet? payeeWallet,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnTransaction;
factory SnTransaction.fromJson(Map<String, dynamic> json) =>
_$SnTransactionFromJson(json);
}

View File

@ -344,4 +344,218 @@ as DateTime?,
} }
/// @nodoc
mixin _$SnTransaction {
String get id; String get currency; double get amount; String? get remarks; int get type; String? get payerWalletId; SnWallet? get payerWallet; String? get payeeWalletId; SnWallet? get payeeWallet; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnTransaction
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnTransactionCopyWith<SnTransaction> get copyWith => _$SnTransactionCopyWithImpl<SnTransaction>(this as SnTransaction, _$identity);
/// Serializes this SnTransaction to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnTransaction&&(identical(other.id, id) || other.id == id)&&(identical(other.currency, currency) || other.currency == currency)&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.remarks, remarks) || other.remarks == remarks)&&(identical(other.type, type) || other.type == type)&&(identical(other.payerWalletId, payerWalletId) || other.payerWalletId == payerWalletId)&&(identical(other.payerWallet, payerWallet) || other.payerWallet == payerWallet)&&(identical(other.payeeWalletId, payeeWalletId) || other.payeeWalletId == payeeWalletId)&&(identical(other.payeeWallet, payeeWallet) || other.payeeWallet == payeeWallet)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,currency,amount,remarks,type,payerWalletId,payerWallet,payeeWalletId,payeeWallet,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnTransaction(id: $id, currency: $currency, amount: $amount, remarks: $remarks, type: $type, payerWalletId: $payerWalletId, payerWallet: $payerWallet, payeeWalletId: $payeeWalletId, payeeWallet: $payeeWallet, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnTransactionCopyWith<$Res> {
factory $SnTransactionCopyWith(SnTransaction value, $Res Function(SnTransaction) _then) = _$SnTransactionCopyWithImpl;
@useResult
$Res call({
String id, String currency, double amount, String? remarks, int type, String? payerWalletId, SnWallet? payerWallet, String? payeeWalletId, SnWallet? payeeWallet, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
$SnWalletCopyWith<$Res>? get payerWallet;$SnWalletCopyWith<$Res>? get payeeWallet;
}
/// @nodoc
class _$SnTransactionCopyWithImpl<$Res>
implements $SnTransactionCopyWith<$Res> {
_$SnTransactionCopyWithImpl(this._self, this._then);
final SnTransaction _self;
final $Res Function(SnTransaction) _then;
/// Create a copy of SnTransaction
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? currency = null,Object? amount = null,Object? remarks = freezed,Object? type = null,Object? payerWalletId = freezed,Object? payerWallet = freezed,Object? payeeWalletId = freezed,Object? payeeWallet = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,currency: null == currency ? _self.currency : currency // ignore: cast_nullable_to_non_nullable
as String,amount: null == amount ? _self.amount : amount // ignore: cast_nullable_to_non_nullable
as double,remarks: freezed == remarks ? _self.remarks : remarks // ignore: cast_nullable_to_non_nullable
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,payerWalletId: freezed == payerWalletId ? _self.payerWalletId : payerWalletId // ignore: cast_nullable_to_non_nullable
as String?,payerWallet: freezed == payerWallet ? _self.payerWallet : payerWallet // ignore: cast_nullable_to_non_nullable
as SnWallet?,payeeWalletId: freezed == payeeWalletId ? _self.payeeWalletId : payeeWalletId // ignore: cast_nullable_to_non_nullable
as String?,payeeWallet: freezed == payeeWallet ? _self.payeeWallet : payeeWallet // ignore: cast_nullable_to_non_nullable
as SnWallet?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
/// Create a copy of SnTransaction
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnWalletCopyWith<$Res>? get payerWallet {
if (_self.payerWallet == null) {
return null;
}
return $SnWalletCopyWith<$Res>(_self.payerWallet!, (value) {
return _then(_self.copyWith(payerWallet: value));
});
}/// Create a copy of SnTransaction
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnWalletCopyWith<$Res>? get payeeWallet {
if (_self.payeeWallet == null) {
return null;
}
return $SnWalletCopyWith<$Res>(_self.payeeWallet!, (value) {
return _then(_self.copyWith(payeeWallet: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _SnTransaction implements SnTransaction {
const _SnTransaction({required this.id, required this.currency, required this.amount, required this.remarks, required this.type, required this.payerWalletId, required this.payerWallet, required this.payeeWalletId, required this.payeeWallet, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnTransaction.fromJson(Map<String, dynamic> json) => _$SnTransactionFromJson(json);
@override final String id;
@override final String currency;
@override final double amount;
@override final String? remarks;
@override final int type;
@override final String? payerWalletId;
@override final SnWallet? payerWallet;
@override final String? payeeWalletId;
@override final SnWallet? payeeWallet;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnTransaction
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnTransactionCopyWith<_SnTransaction> get copyWith => __$SnTransactionCopyWithImpl<_SnTransaction>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnTransactionToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnTransaction&&(identical(other.id, id) || other.id == id)&&(identical(other.currency, currency) || other.currency == currency)&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.remarks, remarks) || other.remarks == remarks)&&(identical(other.type, type) || other.type == type)&&(identical(other.payerWalletId, payerWalletId) || other.payerWalletId == payerWalletId)&&(identical(other.payerWallet, payerWallet) || other.payerWallet == payerWallet)&&(identical(other.payeeWalletId, payeeWalletId) || other.payeeWalletId == payeeWalletId)&&(identical(other.payeeWallet, payeeWallet) || other.payeeWallet == payeeWallet)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,currency,amount,remarks,type,payerWalletId,payerWallet,payeeWalletId,payeeWallet,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnTransaction(id: $id, currency: $currency, amount: $amount, remarks: $remarks, type: $type, payerWalletId: $payerWalletId, payerWallet: $payerWallet, payeeWalletId: $payeeWalletId, payeeWallet: $payeeWallet, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnTransactionCopyWith<$Res> implements $SnTransactionCopyWith<$Res> {
factory _$SnTransactionCopyWith(_SnTransaction value, $Res Function(_SnTransaction) _then) = __$SnTransactionCopyWithImpl;
@override @useResult
$Res call({
String id, String currency, double amount, String? remarks, int type, String? payerWalletId, SnWallet? payerWallet, String? payeeWalletId, SnWallet? payeeWallet, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@override $SnWalletCopyWith<$Res>? get payerWallet;@override $SnWalletCopyWith<$Res>? get payeeWallet;
}
/// @nodoc
class __$SnTransactionCopyWithImpl<$Res>
implements _$SnTransactionCopyWith<$Res> {
__$SnTransactionCopyWithImpl(this._self, this._then);
final _SnTransaction _self;
final $Res Function(_SnTransaction) _then;
/// Create a copy of SnTransaction
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? currency = null,Object? amount = null,Object? remarks = freezed,Object? type = null,Object? payerWalletId = freezed,Object? payerWallet = freezed,Object? payeeWalletId = freezed,Object? payeeWallet = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnTransaction(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,currency: null == currency ? _self.currency : currency // ignore: cast_nullable_to_non_nullable
as String,amount: null == amount ? _self.amount : amount // ignore: cast_nullable_to_non_nullable
as double,remarks: freezed == remarks ? _self.remarks : remarks // ignore: cast_nullable_to_non_nullable
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,payerWalletId: freezed == payerWalletId ? _self.payerWalletId : payerWalletId // ignore: cast_nullable_to_non_nullable
as String?,payerWallet: freezed == payerWallet ? _self.payerWallet : payerWallet // ignore: cast_nullable_to_non_nullable
as SnWallet?,payeeWalletId: freezed == payeeWalletId ? _self.payeeWalletId : payeeWalletId // ignore: cast_nullable_to_non_nullable
as String?,payeeWallet: freezed == payeeWallet ? _self.payeeWallet : payeeWallet // ignore: cast_nullable_to_non_nullable
as SnWallet?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
/// Create a copy of SnTransaction
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnWalletCopyWith<$Res>? get payerWallet {
if (_self.payerWallet == null) {
return null;
}
return $SnWalletCopyWith<$Res>(_self.payerWallet!, (value) {
return _then(_self.copyWith(payerWallet: value));
});
}/// Create a copy of SnTransaction
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnWalletCopyWith<$Res>? get payeeWallet {
if (_self.payeeWallet == null) {
return null;
}
return $SnWalletCopyWith<$Res>(_self.payeeWallet!, (value) {
return _then(_self.copyWith(payeeWallet: value));
});
}
}
// dart format on // dart format on

View File

@ -59,3 +59,44 @@ Map<String, dynamic> _$SnWalletPocketToJson(_SnWalletPocket instance) =>
'updated_at': instance.updatedAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(),
}; };
_SnTransaction _$SnTransactionFromJson(Map<String, dynamic> json) =>
_SnTransaction(
id: json['id'] as String,
currency: json['currency'] as String,
amount: (json['amount'] as num).toDouble(),
remarks: json['remarks'] as String?,
type: (json['type'] as num).toInt(),
payerWalletId: json['payer_wallet_id'] as String?,
payerWallet:
json['payer_wallet'] == null
? null
: SnWallet.fromJson(json['payer_wallet'] as Map<String, dynamic>),
payeeWalletId: json['payee_wallet_id'] as String?,
payeeWallet:
json['payee_wallet'] == null
? null
: SnWallet.fromJson(json['payee_wallet'] as Map<String, dynamic>),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
Map<String, dynamic> _$SnTransactionToJson(_SnTransaction instance) =>
<String, dynamic>{
'id': instance.id,
'currency': instance.currency,
'amount': instance.amount,
'remarks': instance.remarks,
'type': instance.type,
'payer_wallet_id': instance.payerWalletId,
'payer_wallet': instance.payerWallet?.toJson(),
'payee_wallet_id': instance.payeeWalletId,
'payee_wallet': instance.payeeWallet?.toJson(),
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@ -7,6 +7,7 @@ import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
part 'wallet.g.dart'; part 'wallet.g.dart';
@ -23,6 +24,44 @@ const Map<String, IconData> kCurrencyIconData = {
'golds': Symbols.attach_money, 'golds': Symbols.attach_money,
}; };
@riverpod
class TransactionListNotifier extends _$TransactionListNotifier
with CursorPagingNotifierMixin<SnTransaction> {
static const int _pageSize = 20;
@override
Future<CursorPagingData<SnTransaction>> build() => fetch(cursor: null);
@override
Future<CursorPagingData<SnTransaction>> fetch({
required String? cursor,
}) async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final queryParams = {'offset': offset, 'take': _pageSize};
final response = await client.get(
'/wallets/transactions',
queryParameters: queryParams,
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final transactions =
data.map((json) => SnTransaction.fromJson(json)).toList();
final hasMore = offset + transactions.length < total;
final nextCursor =
hasMore ? (offset + transactions.length).toString() : null;
return CursorPagingData(
items: transactions,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
}
@RoutePage() @RoutePage()
class WalletScreen extends HookConsumerWidget { class WalletScreen extends HookConsumerWidget {
const WalletScreen({super.key}); const WalletScreen({super.key});
@ -40,6 +79,8 @@ class WalletScreen extends HookConsumerWidget {
body: wallet.when( body: wallet.when(
data: (data) { data: (data) {
return Column( return Column(
children: [
Column(
spacing: 8, spacing: 8,
children: [ children: [
...data.pockets.map( ...data.pockets.map(
@ -51,7 +92,9 @@ class WalletScreen extends HookConsumerWidget {
Symbols.universal_currency_alt, Symbols.universal_currency_alt,
), ),
title: title:
Text(getCurrencyTranslationKey(pocket.currency)).tr(), Text(
getCurrencyTranslationKey(pocket.currency),
).tr(),
subtitle: Text( subtitle: Text(
'${pocket.amount.toStringAsFixed(2)} ${getCurrencyTranslationKey(pocket.currency, isShort: true).tr()}', '${pocket.amount.toStringAsFixed(2)} ${getCurrencyTranslationKey(pocket.currency, isShort: true).tr()}',
), ),
@ -59,7 +102,52 @@ class WalletScreen extends HookConsumerWidget {
), ),
), ),
], ],
).padding(horizontal: 16, vertical: 16); ).padding(horizontal: 16, vertical: 16),
const Divider(height: 1),
Expanded(
child: PagingHelperView(
provider: transactionListNotifierProvider,
futureRefreshable: transactionListNotifierProvider.future,
notifierRefreshable: transactionListNotifierProvider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => ListView.builder(
padding: EdgeInsets.zero,
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final transaction = data.items[index];
final isIncome =
transaction.payeeWalletId == wallet.value?.id;
return ListTile(
key: ValueKey(transaction.id),
leading: Icon(
isIncome
? Symbols.arrow_upward
: Symbols.arrow_downward,
),
title: Text(transaction.remarks ?? ''),
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,
),
),
);
},
),
),
),
],
);
}, },
error: (error, stackTrace) => Center(child: Text('Error: $error')), error: (error, stackTrace) => Center(child: Text('Error: $error')),
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),

View File

@ -24,5 +24,26 @@ final walletCurrentProvider = AutoDisposeFutureProvider<SnWallet>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
typedef WalletCurrentRef = AutoDisposeFutureProviderRef<SnWallet>; typedef WalletCurrentRef = AutoDisposeFutureProviderRef<SnWallet>;
String _$transactionListNotifierHash() =>
r'148ffb0ee9e3be3b92de432f314d8ee2f09e9a24';
/// See also [TransactionListNotifier].
@ProviderFor(TransactionListNotifier)
final transactionListNotifierProvider = AutoDisposeAsyncNotifierProvider<
TransactionListNotifier,
CursorPagingData<SnTransaction>
>.internal(
TransactionListNotifier.new,
name: r'transactionListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$transactionListNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$TransactionListNotifier =
AutoDisposeAsyncNotifier<CursorPagingData<SnTransaction>>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package