diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index e0003cb4..9f3ed435 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -1132,5 +1132,43 @@ "giftRedeemedSuccessfully": "You have successfully redeemed the gift. Your new subscription is now active.", "cancelGift": "Cancel Gift", "cancelGiftConfirm": "Are you sure you want to cancel this gift? This action cannot be undone.", - "giftCancelledSuccessfully": "Gift cancelled successfully" + "giftCancelledSuccessfully": "Gift cancelled successfully", + "createFund": "Create Fund", + "fundAmount": "Fund Amount", + "enterAmount": "Enter Amount", + "selectCurrency": "Select Currency", + "splitType": "Split Type", + "evenSplit": "Even Split", + "equalAmountEach": "Equal amount for each recipient", + "randomSplit": "Random Split", + "randomAmountEach": "Random amount for each recipient", + "recipientCount": "Recipient Count", + "numberOfRecipients": "Number of Recipients", + "addPersonalMessageForRecipients": "Add a personal message for recipients", + "invalidAmount": "Invalid amount", + "invalidRecipientCount": "Invalid recipient count", + "fundOverview": "Fund Overview", + "totalFundsSent": "Total Funds Sent", + "totalFundsReceived": "Total Funds Received", + "transactions": "Transactions", + "myFunds": "My Funds", + "availableFunds": "Available Funds", + "fundStatusCreated": "Created", + "fundStatusPartial": "Partially Claimed", + "fundStatusCompleted": "Fully Claimed", + "fundStatusExpired": "Expired", + "fundStatusUnknown": "Unknown", + "recipients": "Recipients", + "fundClaimedSuccessfully": "Fund claimed successfully!", + "claim": "Claim", + "noFundsCreated": "No funds created yet", + "createYourFirstFund": "Create your first fund to get started", + "noAvailableFunds": "No available funds", + "fundsWillAppearHere": "Funds you can claim will appear here", + "fundCreatedSuccessfully": "Fund created successfully!", + "selectRecipients": "Select Recipients", + "noRecipientsSelected": "No recipients selected", + "selectRecipientsToSendFund": "Select recipients to send the fund to", + "addRecipient": "Add Recipient", + "addMoreRecipients": "Add More Recipients" } diff --git a/ios/SolianNotificationService/NotificationService.swift b/ios/SolianNotificationService/NotificationService.swift index 190a4089..baf127a4 100644 --- a/ios/SolianNotificationService/NotificationService.swift +++ b/ios/SolianNotificationService/NotificationService.swift @@ -88,8 +88,8 @@ class NotificationService: UNNotificationServiceExtension { let intent = self.createMessageIntent(with: sender, meta: metaCopy, body: content.body) self.donateInteraction(for: intent) let updatedContent = try? request.content.updating(from: intent) + content.categoryIdentifier = "CHAT_MESSAGE" if let updatedContent = updatedContent { - updatedContent.categoryIdentifier = "CHAT_MESSAGE" self.contentHandler?(updatedContent) } else { self.contentHandler?(content) diff --git a/lib/models/wallet.dart b/lib/models/wallet.dart index 940429af..827848fb 100644 --- a/lib/models/wallet.dart +++ b/lib/models/wallet.dart @@ -151,3 +151,45 @@ sealed class SnWalletGift with _$SnWalletGift { factory SnWalletGift.fromJson(Map json) => _$SnWalletGiftFromJson(json); } + +@freezed +sealed class SnWalletFund with _$SnWalletFund { + const factory SnWalletFund({ + required String id, + required String currency, + required double totalAmount, + required int splitType, // 0: even, 1: random + required int + status, // 0: created, 1: partially claimed, 2: fully claimed, 3: expired + required String? message, + required String creatorAccountId, + required SnAccount? creatorAccount, + required DateTime expiredAt, + required List recipients, + required DateTime createdAt, + required DateTime updatedAt, + required DateTime? deletedAt, + }) = _SnWalletFund; + + factory SnWalletFund.fromJson(Map json) => + _$SnWalletFundFromJson(json); +} + +@freezed +sealed class SnWalletFundRecipient with _$SnWalletFundRecipient { + const factory SnWalletFundRecipient({ + required String id, + required String fundId, + required String recipientAccountId, + required SnAccount? recipientAccount, + required double amount, + required bool isReceived, + required DateTime? receivedAt, + required DateTime createdAt, + required DateTime updatedAt, + required DateTime? deletedAt, + }) = _SnWalletFundRecipient; + + factory SnWalletFundRecipient.fromJson(Map json) => + _$SnWalletFundRecipientFromJson(json); +} diff --git a/lib/models/wallet.freezed.dart b/lib/models/wallet.freezed.dart index 0185679a..0eae114c 100644 --- a/lib/models/wallet.freezed.dart +++ b/lib/models/wallet.freezed.dart @@ -2256,4 +2256,639 @@ $SnWalletSubscriptionCopyWith<$Res>? get subscription { } } + +/// @nodoc +mixin _$SnWalletFund { + + String get id; String get currency; double get totalAmount; int get splitType;// 0: even, 1: random + int get status;// 0: created, 1: partially claimed, 2: fully claimed, 3: expired + String? get message; String get creatorAccountId; SnAccount? get creatorAccount; DateTime get expiredAt; List get recipients; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; +/// Create a copy of SnWalletFund +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SnWalletFundCopyWith get copyWith => _$SnWalletFundCopyWithImpl(this as SnWalletFund, _$identity); + + /// Serializes this SnWalletFund to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SnWalletFund&&(identical(other.id, id) || other.id == id)&&(identical(other.currency, currency) || other.currency == currency)&&(identical(other.totalAmount, totalAmount) || other.totalAmount == totalAmount)&&(identical(other.splitType, splitType) || other.splitType == splitType)&&(identical(other.status, status) || other.status == status)&&(identical(other.message, message) || other.message == message)&&(identical(other.creatorAccountId, creatorAccountId) || other.creatorAccountId == creatorAccountId)&&(identical(other.creatorAccount, creatorAccount) || other.creatorAccount == creatorAccount)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&const DeepCollectionEquality().equals(other.recipients, recipients)&&(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,totalAmount,splitType,status,message,creatorAccountId,creatorAccount,expiredAt,const DeepCollectionEquality().hash(recipients),createdAt,updatedAt,deletedAt); + +@override +String toString() { + return 'SnWalletFund(id: $id, currency: $currency, totalAmount: $totalAmount, splitType: $splitType, status: $status, message: $message, creatorAccountId: $creatorAccountId, creatorAccount: $creatorAccount, expiredAt: $expiredAt, recipients: $recipients, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; +} + + +} + +/// @nodoc +abstract mixin class $SnWalletFundCopyWith<$Res> { + factory $SnWalletFundCopyWith(SnWalletFund value, $Res Function(SnWalletFund) _then) = _$SnWalletFundCopyWithImpl; +@useResult +$Res call({ + String id, String currency, double totalAmount, int splitType, int status, String? message, String creatorAccountId, SnAccount? creatorAccount, DateTime expiredAt, List recipients, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt +}); + + +$SnAccountCopyWith<$Res>? get creatorAccount; + +} +/// @nodoc +class _$SnWalletFundCopyWithImpl<$Res> + implements $SnWalletFundCopyWith<$Res> { + _$SnWalletFundCopyWithImpl(this._self, this._then); + + final SnWalletFund _self; + final $Res Function(SnWalletFund) _then; + +/// Create a copy of SnWalletFund +/// 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? totalAmount = null,Object? splitType = null,Object? status = null,Object? message = freezed,Object? creatorAccountId = null,Object? creatorAccount = freezed,Object? expiredAt = null,Object? recipients = null,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,totalAmount: null == totalAmount ? _self.totalAmount : totalAmount // ignore: cast_nullable_to_non_nullable +as double,splitType: null == splitType ? _self.splitType : splitType // ignore: cast_nullable_to_non_nullable +as int,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as int,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable +as String?,creatorAccountId: null == creatorAccountId ? _self.creatorAccountId : creatorAccountId // ignore: cast_nullable_to_non_nullable +as String,creatorAccount: freezed == creatorAccount ? _self.creatorAccount : creatorAccount // ignore: cast_nullable_to_non_nullable +as SnAccount?,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable +as DateTime,recipients: null == recipients ? _self.recipients : recipients // ignore: cast_nullable_to_non_nullable +as List,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 SnWalletFund +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnAccountCopyWith<$Res>? get creatorAccount { + if (_self.creatorAccount == null) { + return null; + } + + return $SnAccountCopyWith<$Res>(_self.creatorAccount!, (value) { + return _then(_self.copyWith(creatorAccount: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [SnWalletFund]. +extension SnWalletFundPatterns on SnWalletFund { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _SnWalletFund value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _SnWalletFund() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _SnWalletFund value) $default,){ +final _that = this; +switch (_that) { +case _SnWalletFund(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _SnWalletFund value)? $default,){ +final _that = this; +switch (_that) { +case _SnWalletFund() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String currency, double totalAmount, int splitType, int status, String? message, String creatorAccountId, SnAccount? creatorAccount, DateTime expiredAt, List recipients, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _SnWalletFund() when $default != null: +return $default(_that.id,_that.currency,_that.totalAmount,_that.splitType,_that.status,_that.message,_that.creatorAccountId,_that.creatorAccount,_that.expiredAt,_that.recipients,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, String currency, double totalAmount, int splitType, int status, String? message, String creatorAccountId, SnAccount? creatorAccount, DateTime expiredAt, List recipients, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; +switch (_that) { +case _SnWalletFund(): +return $default(_that.id,_that.currency,_that.totalAmount,_that.splitType,_that.status,_that.message,_that.creatorAccountId,_that.creatorAccount,_that.expiredAt,_that.recipients,_that.createdAt,_that.updatedAt,_that.deletedAt);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String currency, double totalAmount, int splitType, int status, String? message, String creatorAccountId, SnAccount? creatorAccount, DateTime expiredAt, List recipients, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; +switch (_that) { +case _SnWalletFund() when $default != null: +return $default(_that.id,_that.currency,_that.totalAmount,_that.splitType,_that.status,_that.message,_that.creatorAccountId,_that.creatorAccount,_that.expiredAt,_that.recipients,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _SnWalletFund implements SnWalletFund { + const _SnWalletFund({required this.id, required this.currency, required this.totalAmount, required this.splitType, required this.status, required this.message, required this.creatorAccountId, required this.creatorAccount, required this.expiredAt, required final List recipients, required this.createdAt, required this.updatedAt, required this.deletedAt}): _recipients = recipients; + factory _SnWalletFund.fromJson(Map json) => _$SnWalletFundFromJson(json); + +@override final String id; +@override final String currency; +@override final double totalAmount; +@override final int splitType; +// 0: even, 1: random +@override final int status; +// 0: created, 1: partially claimed, 2: fully claimed, 3: expired +@override final String? message; +@override final String creatorAccountId; +@override final SnAccount? creatorAccount; +@override final DateTime expiredAt; + final List _recipients; +@override List get recipients { + if (_recipients is EqualUnmodifiableListView) return _recipients; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_recipients); +} + +@override final DateTime createdAt; +@override final DateTime updatedAt; +@override final DateTime? deletedAt; + +/// Create a copy of SnWalletFund +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$SnWalletFundCopyWith<_SnWalletFund> get copyWith => __$SnWalletFundCopyWithImpl<_SnWalletFund>(this, _$identity); + +@override +Map toJson() { + return _$SnWalletFundToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnWalletFund&&(identical(other.id, id) || other.id == id)&&(identical(other.currency, currency) || other.currency == currency)&&(identical(other.totalAmount, totalAmount) || other.totalAmount == totalAmount)&&(identical(other.splitType, splitType) || other.splitType == splitType)&&(identical(other.status, status) || other.status == status)&&(identical(other.message, message) || other.message == message)&&(identical(other.creatorAccountId, creatorAccountId) || other.creatorAccountId == creatorAccountId)&&(identical(other.creatorAccount, creatorAccount) || other.creatorAccount == creatorAccount)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&const DeepCollectionEquality().equals(other._recipients, _recipients)&&(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,totalAmount,splitType,status,message,creatorAccountId,creatorAccount,expiredAt,const DeepCollectionEquality().hash(_recipients),createdAt,updatedAt,deletedAt); + +@override +String toString() { + return 'SnWalletFund(id: $id, currency: $currency, totalAmount: $totalAmount, splitType: $splitType, status: $status, message: $message, creatorAccountId: $creatorAccountId, creatorAccount: $creatorAccount, expiredAt: $expiredAt, recipients: $recipients, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; +} + + +} + +/// @nodoc +abstract mixin class _$SnWalletFundCopyWith<$Res> implements $SnWalletFundCopyWith<$Res> { + factory _$SnWalletFundCopyWith(_SnWalletFund value, $Res Function(_SnWalletFund) _then) = __$SnWalletFundCopyWithImpl; +@override @useResult +$Res call({ + String id, String currency, double totalAmount, int splitType, int status, String? message, String creatorAccountId, SnAccount? creatorAccount, DateTime expiredAt, List recipients, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt +}); + + +@override $SnAccountCopyWith<$Res>? get creatorAccount; + +} +/// @nodoc +class __$SnWalletFundCopyWithImpl<$Res> + implements _$SnWalletFundCopyWith<$Res> { + __$SnWalletFundCopyWithImpl(this._self, this._then); + + final _SnWalletFund _self; + final $Res Function(_SnWalletFund) _then; + +/// Create a copy of SnWalletFund +/// 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? totalAmount = null,Object? splitType = null,Object? status = null,Object? message = freezed,Object? creatorAccountId = null,Object? creatorAccount = freezed,Object? expiredAt = null,Object? recipients = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { + return _then(_SnWalletFund( +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,totalAmount: null == totalAmount ? _self.totalAmount : totalAmount // ignore: cast_nullable_to_non_nullable +as double,splitType: null == splitType ? _self.splitType : splitType // ignore: cast_nullable_to_non_nullable +as int,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as int,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable +as String?,creatorAccountId: null == creatorAccountId ? _self.creatorAccountId : creatorAccountId // ignore: cast_nullable_to_non_nullable +as String,creatorAccount: freezed == creatorAccount ? _self.creatorAccount : creatorAccount // ignore: cast_nullable_to_non_nullable +as SnAccount?,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable +as DateTime,recipients: null == recipients ? _self._recipients : recipients // ignore: cast_nullable_to_non_nullable +as List,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 SnWalletFund +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnAccountCopyWith<$Res>? get creatorAccount { + if (_self.creatorAccount == null) { + return null; + } + + return $SnAccountCopyWith<$Res>(_self.creatorAccount!, (value) { + return _then(_self.copyWith(creatorAccount: value)); + }); +} +} + + +/// @nodoc +mixin _$SnWalletFundRecipient { + + String get id; String get fundId; String get recipientAccountId; SnAccount? get recipientAccount; double get amount; bool get isReceived; DateTime? get receivedAt; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; +/// Create a copy of SnWalletFundRecipient +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SnWalletFundRecipientCopyWith get copyWith => _$SnWalletFundRecipientCopyWithImpl(this as SnWalletFundRecipient, _$identity); + + /// Serializes this SnWalletFundRecipient to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SnWalletFundRecipient&&(identical(other.id, id) || other.id == id)&&(identical(other.fundId, fundId) || other.fundId == fundId)&&(identical(other.recipientAccountId, recipientAccountId) || other.recipientAccountId == recipientAccountId)&&(identical(other.recipientAccount, recipientAccount) || other.recipientAccount == recipientAccount)&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.isReceived, isReceived) || other.isReceived == isReceived)&&(identical(other.receivedAt, receivedAt) || other.receivedAt == receivedAt)&&(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,fundId,recipientAccountId,recipientAccount,amount,isReceived,receivedAt,createdAt,updatedAt,deletedAt); + +@override +String toString() { + return 'SnWalletFundRecipient(id: $id, fundId: $fundId, recipientAccountId: $recipientAccountId, recipientAccount: $recipientAccount, amount: $amount, isReceived: $isReceived, receivedAt: $receivedAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; +} + + +} + +/// @nodoc +abstract mixin class $SnWalletFundRecipientCopyWith<$Res> { + factory $SnWalletFundRecipientCopyWith(SnWalletFundRecipient value, $Res Function(SnWalletFundRecipient) _then) = _$SnWalletFundRecipientCopyWithImpl; +@useResult +$Res call({ + String id, String fundId, String recipientAccountId, SnAccount? recipientAccount, double amount, bool isReceived, DateTime? receivedAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt +}); + + +$SnAccountCopyWith<$Res>? get recipientAccount; + +} +/// @nodoc +class _$SnWalletFundRecipientCopyWithImpl<$Res> + implements $SnWalletFundRecipientCopyWith<$Res> { + _$SnWalletFundRecipientCopyWithImpl(this._self, this._then); + + final SnWalletFundRecipient _self; + final $Res Function(SnWalletFundRecipient) _then; + +/// Create a copy of SnWalletFundRecipient +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? fundId = null,Object? recipientAccountId = null,Object? recipientAccount = freezed,Object? amount = null,Object? isReceived = null,Object? receivedAt = 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,fundId: null == fundId ? _self.fundId : fundId // ignore: cast_nullable_to_non_nullable +as String,recipientAccountId: null == recipientAccountId ? _self.recipientAccountId : recipientAccountId // ignore: cast_nullable_to_non_nullable +as String,recipientAccount: freezed == recipientAccount ? _self.recipientAccount : recipientAccount // ignore: cast_nullable_to_non_nullable +as SnAccount?,amount: null == amount ? _self.amount : amount // ignore: cast_nullable_to_non_nullable +as double,isReceived: null == isReceived ? _self.isReceived : isReceived // ignore: cast_nullable_to_non_nullable +as bool,receivedAt: freezed == receivedAt ? _self.receivedAt : receivedAt // ignore: cast_nullable_to_non_nullable +as DateTime?,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 SnWalletFundRecipient +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnAccountCopyWith<$Res>? get recipientAccount { + if (_self.recipientAccount == null) { + return null; + } + + return $SnAccountCopyWith<$Res>(_self.recipientAccount!, (value) { + return _then(_self.copyWith(recipientAccount: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [SnWalletFundRecipient]. +extension SnWalletFundRecipientPatterns on SnWalletFundRecipient { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _SnWalletFundRecipient value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _SnWalletFundRecipient() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _SnWalletFundRecipient value) $default,){ +final _that = this; +switch (_that) { +case _SnWalletFundRecipient(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _SnWalletFundRecipient value)? $default,){ +final _that = this; +switch (_that) { +case _SnWalletFundRecipient() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String fundId, String recipientAccountId, SnAccount? recipientAccount, double amount, bool isReceived, DateTime? receivedAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _SnWalletFundRecipient() when $default != null: +return $default(_that.id,_that.fundId,_that.recipientAccountId,_that.recipientAccount,_that.amount,_that.isReceived,_that.receivedAt,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, String fundId, String recipientAccountId, SnAccount? recipientAccount, double amount, bool isReceived, DateTime? receivedAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; +switch (_that) { +case _SnWalletFundRecipient(): +return $default(_that.id,_that.fundId,_that.recipientAccountId,_that.recipientAccount,_that.amount,_that.isReceived,_that.receivedAt,_that.createdAt,_that.updatedAt,_that.deletedAt);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String fundId, String recipientAccountId, SnAccount? recipientAccount, double amount, bool isReceived, DateTime? receivedAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; +switch (_that) { +case _SnWalletFundRecipient() when $default != null: +return $default(_that.id,_that.fundId,_that.recipientAccountId,_that.recipientAccount,_that.amount,_that.isReceived,_that.receivedAt,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _SnWalletFundRecipient implements SnWalletFundRecipient { + const _SnWalletFundRecipient({required this.id, required this.fundId, required this.recipientAccountId, required this.recipientAccount, required this.amount, required this.isReceived, required this.receivedAt, required this.createdAt, required this.updatedAt, required this.deletedAt}); + factory _SnWalletFundRecipient.fromJson(Map json) => _$SnWalletFundRecipientFromJson(json); + +@override final String id; +@override final String fundId; +@override final String recipientAccountId; +@override final SnAccount? recipientAccount; +@override final double amount; +@override final bool isReceived; +@override final DateTime? receivedAt; +@override final DateTime createdAt; +@override final DateTime updatedAt; +@override final DateTime? deletedAt; + +/// Create a copy of SnWalletFundRecipient +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$SnWalletFundRecipientCopyWith<_SnWalletFundRecipient> get copyWith => __$SnWalletFundRecipientCopyWithImpl<_SnWalletFundRecipient>(this, _$identity); + +@override +Map toJson() { + return _$SnWalletFundRecipientToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnWalletFundRecipient&&(identical(other.id, id) || other.id == id)&&(identical(other.fundId, fundId) || other.fundId == fundId)&&(identical(other.recipientAccountId, recipientAccountId) || other.recipientAccountId == recipientAccountId)&&(identical(other.recipientAccount, recipientAccount) || other.recipientAccount == recipientAccount)&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.isReceived, isReceived) || other.isReceived == isReceived)&&(identical(other.receivedAt, receivedAt) || other.receivedAt == receivedAt)&&(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,fundId,recipientAccountId,recipientAccount,amount,isReceived,receivedAt,createdAt,updatedAt,deletedAt); + +@override +String toString() { + return 'SnWalletFundRecipient(id: $id, fundId: $fundId, recipientAccountId: $recipientAccountId, recipientAccount: $recipientAccount, amount: $amount, isReceived: $isReceived, receivedAt: $receivedAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; +} + + +} + +/// @nodoc +abstract mixin class _$SnWalletFundRecipientCopyWith<$Res> implements $SnWalletFundRecipientCopyWith<$Res> { + factory _$SnWalletFundRecipientCopyWith(_SnWalletFundRecipient value, $Res Function(_SnWalletFundRecipient) _then) = __$SnWalletFundRecipientCopyWithImpl; +@override @useResult +$Res call({ + String id, String fundId, String recipientAccountId, SnAccount? recipientAccount, double amount, bool isReceived, DateTime? receivedAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt +}); + + +@override $SnAccountCopyWith<$Res>? get recipientAccount; + +} +/// @nodoc +class __$SnWalletFundRecipientCopyWithImpl<$Res> + implements _$SnWalletFundRecipientCopyWith<$Res> { + __$SnWalletFundRecipientCopyWithImpl(this._self, this._then); + + final _SnWalletFundRecipient _self; + final $Res Function(_SnWalletFundRecipient) _then; + +/// Create a copy of SnWalletFundRecipient +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? fundId = null,Object? recipientAccountId = null,Object? recipientAccount = freezed,Object? amount = null,Object? isReceived = null,Object? receivedAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { + return _then(_SnWalletFundRecipient( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,fundId: null == fundId ? _self.fundId : fundId // ignore: cast_nullable_to_non_nullable +as String,recipientAccountId: null == recipientAccountId ? _self.recipientAccountId : recipientAccountId // ignore: cast_nullable_to_non_nullable +as String,recipientAccount: freezed == recipientAccount ? _self.recipientAccount : recipientAccount // ignore: cast_nullable_to_non_nullable +as SnAccount?,amount: null == amount ? _self.amount : amount // ignore: cast_nullable_to_non_nullable +as double,isReceived: null == isReceived ? _self.isReceived : isReceived // ignore: cast_nullable_to_non_nullable +as bool,receivedAt: freezed == receivedAt ? _self.receivedAt : receivedAt // ignore: cast_nullable_to_non_nullable +as DateTime?,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 SnWalletFundRecipient +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnAccountCopyWith<$Res>? get recipientAccount { + if (_self.recipientAccount == null) { + return null; + } + + return $SnAccountCopyWith<$Res>(_self.recipientAccount!, (value) { + return _then(_self.copyWith(recipientAccount: value)); + }); +} +} + // dart format on diff --git a/lib/models/wallet.g.dart b/lib/models/wallet.g.dart index 32ffaa30..fd894f95 100644 --- a/lib/models/wallet.g.dart +++ b/lib/models/wallet.g.dart @@ -295,3 +295,88 @@ Map _$SnWalletGiftToJson(_SnWalletGift instance) => 'updated_at': instance.updatedAt.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(), }; + +_SnWalletFund _$SnWalletFundFromJson( + Map json, +) => _SnWalletFund( + id: json['id'] as String, + currency: json['currency'] as String, + totalAmount: (json['total_amount'] as num).toDouble(), + splitType: (json['split_type'] as num).toInt(), + status: (json['status'] as num).toInt(), + message: json['message'] as String?, + creatorAccountId: json['creator_account_id'] as String, + creatorAccount: + json['creator_account'] == null + ? null + : SnAccount.fromJson(json['creator_account'] as Map), + expiredAt: DateTime.parse(json['expired_at'] as String), + recipients: + (json['recipients'] as List) + .map((e) => SnWalletFundRecipient.fromJson(e as Map)) + .toList(), + 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 _$SnWalletFundToJson(_SnWalletFund instance) => + { + 'id': instance.id, + 'currency': instance.currency, + 'total_amount': instance.totalAmount, + 'split_type': instance.splitType, + 'status': instance.status, + 'message': instance.message, + 'creator_account_id': instance.creatorAccountId, + 'creator_account': instance.creatorAccount?.toJson(), + 'expired_at': instance.expiredAt.toIso8601String(), + 'recipients': instance.recipients.map((e) => e.toJson()).toList(), + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'deleted_at': instance.deletedAt?.toIso8601String(), + }; + +_SnWalletFundRecipient _$SnWalletFundRecipientFromJson( + Map json, +) => _SnWalletFundRecipient( + id: json['id'] as String, + fundId: json['fund_id'] as String, + recipientAccountId: json['recipient_account_id'] as String, + recipientAccount: + json['recipient_account'] == null + ? null + : SnAccount.fromJson( + json['recipient_account'] as Map, + ), + amount: (json['amount'] as num).toDouble(), + isReceived: json['is_received'] as bool, + receivedAt: + json['received_at'] == null + ? null + : DateTime.parse(json['received_at'] as String), + 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 _$SnWalletFundRecipientToJson( + _SnWalletFundRecipient instance, +) => { + 'id': instance.id, + 'fund_id': instance.fundId, + 'recipient_account_id': instance.recipientAccountId, + 'recipient_account': instance.recipientAccount?.toJson(), + 'amount': instance.amount, + 'is_received': instance.isReceived, + 'received_at': instance.receivedAt?.toIso8601String(), + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'deleted_at': instance.deletedAt?.toIso8601String(), +}; diff --git a/lib/screens/auth/captcha.config.g.dart b/lib/screens/auth/captcha.config.g.dart index 578422f0..fe3912df 100644 --- a/lib/screens/auth/captcha.config.g.dart +++ b/lib/screens/auth/captcha.config.g.dart @@ -6,7 +6,7 @@ part of 'captcha.config.dart'; // RiverpodGenerator // ************************************************************************** -String _$captchaUrlHash() => r'd46bc43032cef504547cd528a40c23cf76f27cc8'; +String _$captchaUrlHash() => r'5d59de4f26a0544bf4fbd5209943f0b111959ce6'; /// See also [captchaUrl]. @ProviderFor(captchaUrl) diff --git a/lib/screens/wallet.dart b/lib/screens/wallet.dart index 69869ab9..d4788b87 100644 --- a/lib/screens/wallet.dart +++ b/lib/screens/wallet.dart @@ -1,16 +1,25 @@ import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/account.dart'; import 'package:island/models/wallet.dart'; import 'package:island/pods/network.dart'; +import 'package:island/widgets/account/account_picker.dart'; import 'package:island/widgets/app_scaffold.dart'; +import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/alert.dart'; +import 'package:island/widgets/content/sheet.dart'; +import 'package:island/widgets/payment/payment_overlay.dart'; import 'package:island/widgets/response.dart'; import 'package:material_symbols_icons/symbols.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:flutter_otp_text_field/flutter_otp_text_field.dart'; part 'wallet.g.dart'; @@ -28,6 +37,545 @@ Future walletCurrent(Ref ref) async { } } +class CreateFundSheet extends StatefulWidget { + const CreateFundSheet({super.key}); + + @override + State createState() => _CreateFundSheetState(); +} + +class _CreateFundSheetState extends State { + final amountController = TextEditingController(); + final messageController = TextEditingController(); + String selectedCurrency = 'golds'; + int selectedSplitType = 0; // 0: even, 1: random + List selectedRecipients = []; + + @override + void dispose() { + amountController.dispose(); + messageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SheetScaffold( + titleText: 'createFund'.tr(), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Amount Section + Text( + 'fundAmount'.tr(), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary, + ), + ), + const Gap(8), + TextField( + controller: amountController, + keyboardType: TextInputType.numberWithOptions( + decimal: true, + ), + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp(r'^\d+\.?\d{0,2}'), + ), + ], + decoration: InputDecoration( + labelText: 'enterAmount'.tr(), + hintText: '0.00', + prefixIcon: Icon(kCurrencyIconData[selectedCurrency]), + border: OutlineInputBorder(), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of( + context, + ).colorScheme.outline.withOpacity(0.2), + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + onTapOutside: + (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + + const Gap(16), + + // Currency Selection + Text( + 'selectCurrency'.tr(), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary, + ), + ), + const Gap(8), + DropdownButtonFormField( + value: selectedCurrency, + decoration: InputDecoration( + border: OutlineInputBorder(), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of( + context, + ).colorScheme.outline.withOpacity(0.2), + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + items: + kCurrencyIconData.keys.map((currency) { + return DropdownMenuItem( + value: currency, + child: Row( + children: [ + Icon(kCurrencyIconData[currency]), + const Gap(8), + Text( + 'walletCurrency${currency[0].toUpperCase()}${currency.substring(1).toLowerCase()}' + .tr(), + ), + ], + ), + ); + }).toList(), + onChanged: (value) { + if (value != null) { + setState(() => selectedCurrency = value); + } + }, + ), + + // Split Type Section (only show when there are 2+ recipients) + if (selectedRecipients.length >= 2) ...[ + const Gap(16), + Text( + 'splitType'.tr(), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary, + ), + ), + const Gap(8), + Row( + children: [ + Expanded( + child: RadioListTile( + title: Text('evenSplit'.tr()), + subtitle: Text('equalAmountEach'.tr()), + value: 0, + groupValue: selectedSplitType, + onChanged: (value) { + if (value != null) { + setState(() => selectedSplitType = value); + } + }, + ), + ), + Expanded( + child: RadioListTile( + title: Text('randomSplit'.tr()), + subtitle: Text('randomAmountEach'.tr()), + value: 1, + groupValue: selectedSplitType, + onChanged: (value) { + if (value != null) { + setState(() => selectedSplitType = value); + } + }, + ), + ), + ], + ), + ], + + const Gap(16), + + // Recipient Selection Section + Text( + 'selectRecipients'.tr(), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary, + ), + ), + const Gap(8), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Theme.of( + context, + ).colorScheme.outline.withOpacity(0.2), + ), + ), + child: + selectedRecipients.isNotEmpty + ? Column( + children: [ + ...selectedRecipients.map((recipient) { + return ListTile( + contentPadding: const EdgeInsets.only( + left: 20, + right: 12, + ), + leading: ProfilePictureWidget( + file: recipient.profile.picture, + ), + title: Text( + recipient.nick, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + subtitle: Text( + 'selectedRecipient'.tr(), + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith( + color: + Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + trailing: IconButton( + onPressed: + () => setState( + () => selectedRecipients.remove( + recipient, + ), + ), + icon: Icon( + Icons.clear, + color: + Theme.of(context).colorScheme.error, + ), + tooltip: 'Remove recipient', + ), + ); + }), + if (selectedRecipients.length < 10) + OutlinedButton.icon( + onPressed: () async { + final recipient = + await showModalBottomSheet( + 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( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.person_add_outlined, + size: 48, + color: + Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + const Gap(8), + Text( + 'noRecipientsSelected'.tr(), + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith( + color: + Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + const Gap(4), + Text( + 'selectRecipientsToSendFund'.tr(), + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith( + color: + Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ], + ).padding(vertical: 32), + ), + const Gap(12), + OutlinedButton.icon( + onPressed: () async { + final recipient = await showModalBottomSheet( + 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_search), + label: Text( + selectedRecipients.isNotEmpty + ? 'addMoreRecipients'.tr() + : 'selectRecipients'.tr(), + ), + style: OutlinedButton.styleFrom( + minimumSize: const Size(double.infinity, 48), + ), + ), + + const Gap(16), + + // Message Section + Text( + 'addMessage'.tr(), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary, + ), + ), + const Gap(8), + TextField( + controller: messageController, + decoration: InputDecoration( + labelText: 'personalMessage'.tr(), + hintText: 'addPersonalMessageForRecipients'.tr(), + alignLabelWithHint: true, + border: OutlineInputBorder(), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of( + context, + ).colorScheme.outline.withOpacity(0.2), + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + maxLines: 3, + onTapOutside: + (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + ], + ), + ), + ), + + // Action Buttons + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('cancel'.tr()), + ), + ), + const Gap(8), + Expanded( + child: FilledButton( + onPressed: _createFund, + child: Text('createFund'.tr()), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Future _showPinVerificationDialog(BuildContext context) async { + String enteredPin = ''; + + await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + useSafeArea: true, + builder: + (context) => Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16), + ), + ), + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: SheetScaffold( + titleText: 'enterPinToConfirm'.tr(), + heightFactor: 0.5, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'enterPinToConfirm'.tr(), + style: Theme.of(context).textTheme.titleMedium + ?.copyWith(fontWeight: FontWeight.w500), + textAlign: TextAlign.center, + ), + const Gap(24), + OtpTextField( + numberOfFields: 6, + borderColor: + Theme.of(context).colorScheme.outline, + focusedBorderColor: + Theme.of(context).colorScheme.primary, + showFieldAsBox: true, + obscureText: true, + keyboardType: TextInputType.number, + fieldWidth: 48, + fieldHeight: 56, + borderRadius: BorderRadius.circular(8), + borderWidth: 1, + textStyle: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(fontWeight: FontWeight.w600), + onSubmit: (pin) { + enteredPin = pin; + Navigator.of(context).pop(pin); + }, + onCodeChanged: (String code) { + enteredPin = code; + }, + ), + ], + ), + ), + const Gap(24), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('cancel'.tr()), + ), + ), + if (enteredPin.length == 6) ...[ + const Gap(12), + Expanded( + child: FilledButton( + onPressed: () { + Navigator.of(context).pop(enteredPin); + }, + child: Text('confirm'.tr()), + ), + ), + ], + ], + ), + ], + ), + ), + ), + ), + ), + ); + + return enteredPin.isNotEmpty ? enteredPin : null; + } + + Future _createFund() async { + final amount = double.tryParse(amountController.text); + + if (amount == null || amount <= 0) { + showErrorAlert('invalidAmount'.tr()); + return; + } + + if (selectedRecipients.isEmpty) { + showErrorAlert('noRecipientsSelected'.tr()); + return; + } + + final data = { + 'currency': selectedCurrency, + 'total_amount': amount, + 'split_type': selectedSplitType, + 'recipient_account_ids': selectedRecipients.map((r) => r.id).toList(), + 'message': + messageController.text.trim().isEmpty + ? null + : messageController.text.trim(), + 'pin_code': '', // Will be filled by PIN verification + }; + + // Ask for PIN confirmation before creating fund + final enteredPin = await _showPinVerificationDialog(context); + if (enteredPin == null || enteredPin.isEmpty) return; + + // Add PIN to the fund data + data['pin_code'] = enteredPin; + + if (mounted) Navigator.of(context).pop(data); + } +} + const Map kCurrencyIconData = { 'points': Symbols.toll, 'golds': Symbols.attach_money, @@ -71,12 +619,54 @@ class TransactionListNotifier extends _$TransactionListNotifier } } +@riverpod +Future> walletFunds( + Ref ref, { + int offset = 0, + int take = 20, +}) async { + final client = ref.watch(apiClientProvider); + final resp = await client.get('/id/wallets/funds?offset=$offset&take=$take'); + return (resp.data as List).map((e) => SnWalletFund.fromJson(e)).toList(); +} + +@riverpod +Future> walletFundRecipients( + Ref ref, { + int offset = 0, + int take = 20, +}) async { + final client = ref.watch(apiClientProvider); + final resp = await client.get( + '/id/wallets/funds/recipients?offset=$offset&take=$take', + ); + return (resp.data as List) + .map((e) => SnWalletFundRecipient.fromJson(e)) + .toList(); +} + +@riverpod +Future walletFund(Ref ref, String fundId) async { + final client = ref.watch(apiClientProvider); + final resp = await client.get('/id/wallets/funds/$fundId'); + return SnWalletFund.fromJson(resp.data); +} + +@riverpod +Future> walletFundStats(Ref ref) async { + final client = ref.watch(apiClientProvider); + final resp = await client.get('/id/wallets/stats'); + return resp.data as Map; +} + class WalletScreen extends HookConsumerWidget { const WalletScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final wallet = ref.watch(walletCurrentProvider); + final tabController = useTabController(initialLength: 3); + final fundStats = ref.watch(walletFundStatsProvider); Future createWallet() async { final client = ref.read(apiClientProvider); @@ -88,12 +678,35 @@ class WalletScreen extends HookConsumerWidget { } } + Future createFund() async { + final result = await showModalBottomSheet>( + context: context, + useRootNavigator: true, + isScrollControlled: true, + builder: (context) => const CreateFundSheet(), + ); + + if (result != null && context.mounted) { + await _handleFundCreation(context, ref, result); + } + } + String getCurrencyTranslationKey(String currency, {bool isShort = false}) { return 'walletCurrency${isShort ? 'Short' : ''}${currency[0].toUpperCase()}${currency.substring(1).toLowerCase()}'; } return AppScaffold( - appBar: AppBar(title: Text('wallet').tr()), + appBar: AppBar( + title: Text('wallet').tr(), + actions: [ + IconButton( + icon: const Icon(Symbols.add), + onPressed: createFund, + tooltip: 'createFund'.tr(), + ), + const Gap(8), + ], + ), body: wallet.when( data: (data) { if (data == null) { @@ -115,9 +728,11 @@ class WalletScreen extends HookConsumerWidget { return Column( children: [ + // Wallet Overview Column( spacing: 8, children: [ + // Pockets ...data.pockets.map( (pocket) => Card( margin: EdgeInsets.zero, @@ -136,49 +751,132 @@ class WalletScreen extends HookConsumerWidget { ), ), ), + + // Fund Stats + fundStats.when( + data: + (stats) => Card( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Symbols.celebration, + color: + Theme.of(context).colorScheme.primary, + ), + const Gap(8), + Text( + 'fundOverview'.tr(), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const Gap(8), + Row( + children: [ + Expanded( + child: _buildStatItem( + context, + 'totalFundsSent'.tr(), + '${stats['total_sent'] ?? 0}', + Icons.send, + ), + ), + Expanded( + child: _buildStatItem( + context, + 'totalFundsReceived'.tr(), + '${stats['total_received'] ?? 0}', + Icons.call_received, + ), + ), + ], + ), + ], + ), + ), + ), + loading: + () => const Card( + margin: EdgeInsets.zero, + child: Padding( + padding: EdgeInsets.all(16), + child: Center(child: CircularProgressIndicator()), + ), + ), + error: (error, stack) => const SizedBox.shrink(), + ), ], ).padding(horizontal: 16, vertical: 16), - const Divider(height: 1), + + // Tab Bar + TabBar( + controller: tabController, + tabs: [ + Tab(text: 'transactions'.tr()), + Tab(text: 'myFunds'.tr()), + ], + ), + + // Tab Content 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; - } + child: TabBarView( + controller: tabController, + children: [ + // Transactions Tab + 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; + 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, - ), - ), - ); - }, - ), + 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, + ), + ), + ); + }, + ), + ), + + // My Funds Tab + _buildFundsList(context, ref), + ], ), ), ], @@ -193,4 +891,230 @@ class WalletScreen extends HookConsumerWidget { ), ); } + + Widget _buildStatItem( + BuildContext context, + String label, + String value, + IconData icon, + ) { + return Column( + children: [ + Icon(icon, size: 20, color: Theme.of(context).colorScheme.primary), + const Gap(4), + Text( + value, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + Text( + label, + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ), + ], + ); + } + + Widget _buildFundsList(BuildContext context, WidgetRef ref) { + final funds = ref.watch(walletFundsProvider()); + + return funds.when( + data: (fundList) { + if (fundList.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Symbols.celebration, + size: 48, + color: Theme.of(context).colorScheme.outline, + ), + const Gap(16), + Text( + 'noFundsCreated'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + const Gap(8), + Text( + 'createYourFirstFund'.tr(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: fundList.length, + itemBuilder: (context, index) { + final fund = fundList[index]; + final claimedCount = + fund.recipients.where((r) => r.isReceived).length; + final totalRecipients = fund.recipients.length; + + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Symbols.celebration, + color: Theme.of(context).colorScheme.primary, + ), + const Gap(8), + Expanded( + child: Text( + '${fund.totalAmount.toStringAsFixed(2)} ${fund.currency}', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: _getFundStatusColor( + context, + fund.status, + ).withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + _getFundStatusText(fund.status), + style: TextStyle( + color: _getFundStatusColor(context, fund.status), + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const Gap(8), + Text( + '${'recipients'.tr()}: $claimedCount/$totalRecipients', + style: Theme.of(context).textTheme.bodyMedium, + ), + if (fund.message != null && fund.message!.isNotEmpty) ...[ + const Gap(4), + Text( + fund.message!, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + const Gap(8), + Text( + DateFormat.yMd().add_Hm().format(fund.createdAt), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center(child: Text('Error: $error')), + ); + } + + Future _handleFundCreation( + BuildContext context, + WidgetRef ref, + Map fundData, + ) async { + final client = ref.read(apiClientProvider); + try { + showLoadingModal(context); + final resp = await client.post( + '/id/wallets/funds', + data: fundData, + options: Options(headers: {'X-Noop': true}), + ); + final fund = SnWalletFund.fromJson(resp.data); + if (fund.status == 0) return; // Already created + + final orderResp = await client.post('/id/wallets/funds/${fund.id}/order'); + final order = SnWalletOrder.fromJson(orderResp.data); + + if (context.mounted) hideLoadingModal(context); + + // Show payment overlay to complete the payment + if (!context.mounted) return; + final paidOrder = await PaymentOverlay.show( + context: context, + order: order, + enableBiometric: true, + ); + + if (context.mounted) showLoadingModal(context); + + if (paidOrder != null) { + // Wait for server to handle order + await Future.delayed(const Duration(seconds: 1)); + ref.invalidate(walletFundsProvider); + ref.invalidate(walletFundStatsProvider); + ref.invalidate(walletCurrentProvider); + if (context.mounted) { + showSnackBar('fundCreatedSuccessfully'.tr()); + } + } + } catch (err) { + showErrorAlert(err); + } finally { + if (context.mounted) hideLoadingModal(context); + } + } + + String _getFundStatusText(int status) { + switch (status) { + case 0: + return 'fundStatusCreated'.tr(); + case 1: + return 'fundStatusPartial'.tr(); + case 2: + return 'fundStatusCompleted'.tr(); + case 3: + return 'fundStatusExpired'.tr(); + default: + return 'fundStatusUnknown'.tr(); + } + } + + Color _getFundStatusColor(BuildContext context, int status) { + switch (status) { + case 0: + return Colors.blue; + case 1: + return Colors.orange; + case 2: + return Colors.green; + case 3: + return Colors.red; + default: + return Theme.of(context).colorScheme.primary; + } + } } diff --git a/lib/screens/wallet.dart.backup b/lib/screens/wallet.dart.backup new file mode 100644 index 00000000..e6b62709 --- /dev/null +++ b/lib/screens/wallet.dart.backup @@ -0,0 +1,1012 @@ +import 'package:dio/dio.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/account.dart'; +import 'package:island/models/wallet.dart'; +import 'package:island/pods/network.dart'; +import 'package:island/widgets/account/account_picker.dart'; +import 'package:island/widgets/app_scaffold.dart'; +import 'package:island/widgets/content/cloud_files.dart'; +import 'package:island/widgets/alert.dart'; +import 'package:island/widgets/content/sheet.dart'; +import 'package:island/widgets/payment/payment_overlay.dart'; +import 'package:island/widgets/response.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; +import 'package:styled_widget/styled_widget.dart'; + +part 'wallet.g.dart'; + +@riverpod +Future walletCurrent(Ref ref) async { + try { + final apiClient = ref.watch(apiClientProvider); + final resp = await apiClient.get('/id/wallets'); + return SnWallet.fromJson(resp.data); + } catch (err) { + if (err is DioException && err.response?.statusCode == 404) { + return null; + } + rethrow; + } +} + +class CreateFundSheet extends StatefulWidget { + const CreateFundSheet({super.key}); + + @override + State createState() => _CreateFundSheetState(); +} + +class _CreateFundSheetState extends State { + final amountController = TextEditingController(); + final messageController = TextEditingController(); + String selectedCurrency = 'golds'; + int selectedSplitType = 0; // 0: even, 1: random + List selectedRecipients = []; + + @override + void dispose() { + amountController.dispose(); + messageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SheetScaffold( + titleText: 'createFund'.tr(), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Amount Section + Text( + 'fundAmount'.tr(), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary, + ), + ), + const Gap(8), + TextField( + controller: amountController, + keyboardType: TextInputType.numberWithOptions( + decimal: true, + ), + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp(r'^\d+\.?\d{0,2}'), + ), + ], + decoration: InputDecoration( + labelText: 'enterAmount'.tr(), + hintText: '0.00', + prefixIcon: Icon(kCurrencyIconData[selectedCurrency]), + border: OutlineInputBorder(), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of( + context, + ).colorScheme.outline.withOpacity(0.2), + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + onTapOutside: + (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + + const Gap(16), + + // Currency Selection + Text( + 'selectCurrency'.tr(), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary, + ), + ), + const Gap(8), + DropdownButtonFormField( + value: selectedCurrency, + decoration: InputDecoration( + border: OutlineInputBorder(), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of( + context, + ).colorScheme.outline.withOpacity(0.2), + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + items: + kCurrencyIconData.keys.map((currency) { + return DropdownMenuItem( + value: currency, + child: Row( + children: [ + Icon(kCurrencyIconData[currency]), + const Gap(8), + Text( + 'walletCurrency${currency[0].toUpperCase()}${currency.substring(1).toLowerCase()}' + .tr(), + ), + ], + ), + ); + }).toList(), + onChanged: (value) { + if (value != null) { + setState(() => selectedCurrency = value); + } + }, + ), + + // Split Type Section (only show when there are 2+ recipients) + if (selectedRecipients.length >= 2) ...[ + const Gap(16), + Text( + 'splitType'.tr(), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary, + ), + ), + const Gap(8), + Row( + children: [ + Expanded( + child: RadioListTile( + title: Text('evenSplit'.tr()), + subtitle: Text('equalAmountEach'.tr()), + value: 0, + groupValue: selectedSplitType, + onChanged: (value) { + if (value != null) { + setState(() => selectedSplitType = value); + } + }, + ), + ), + Expanded( + child: RadioListTile( + title: Text('randomSplit'.tr()), + subtitle: Text('randomAmountEach'.tr()), + value: 1, + groupValue: selectedSplitType, + onChanged: (value) { + if (value != null) { + setState(() => selectedSplitType = value); + } + }, + ), + ), + ], + ), + ], + + const Gap(16), + + // Recipient Selection Section + Text( + 'selectRecipients'.tr(), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary, + ), + ), + const Gap(8), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Theme.of( + context, + ).colorScheme.outline.withOpacity(0.2), + ), + ), + child: + selectedRecipients.isNotEmpty + ? Column( + children: [ + ...selectedRecipients.map((recipient) { + return ListTile( + contentPadding: const EdgeInsets.only( + left: 20, + right: 12, + ), + leading: ProfilePictureWidget( + file: recipient.profile.picture, + ), + title: Text( + recipient.nick, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + subtitle: Text( + 'selectedRecipient'.tr(), + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith( + color: + Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + trailing: IconButton( + onPressed: + () => setState( + () => selectedRecipients.remove( + recipient, + ), + ), + icon: Icon( + Icons.clear, + color: + Theme.of(context).colorScheme.error, + ), + tooltip: 'Remove recipient', + ), + ); + }), + if (selectedRecipients.length < 10) + OutlinedButton.icon( + onPressed: () async { + final recipient = + await showModalBottomSheet( + 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( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.person_add_outlined, + size: 48, + color: + Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + const Gap(8), + Text( + 'noRecipientsSelected'.tr(), + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith( + color: + Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + const Gap(4), + Text( + 'selectRecipientsToSendFund'.tr(), + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith( + color: + Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ], + ).padding(vertical: 32), + ), + const Gap(12), + OutlinedButton.icon( + onPressed: () async { + final recipient = await showModalBottomSheet( + 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_search), + label: Text( + selectedRecipients.isNotEmpty + ? 'addMoreRecipients'.tr() + : 'selectRecipients'.tr(), + ), + style: OutlinedButton.styleFrom( + minimumSize: const Size(double.infinity, 48), + ), + ), + + const Gap(16), + + // Message Section + Text( + 'addMessage'.tr(), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary, + ), + ), + const Gap(8), + TextField( + controller: messageController, + decoration: InputDecoration( + labelText: 'personalMessage'.tr(), + hintText: 'addPersonalMessageForRecipients'.tr(), + alignLabelWithHint: true, + border: OutlineInputBorder(), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of( + context, + ).colorScheme.outline.withOpacity(0.2), + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + maxLines: 3, + onTapOutside: + (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + ], + ), + ), + ), + + // Action Buttons + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('cancel'.tr()), + ), + ), + const Gap(8), + Expanded( + child: FilledButton( + onPressed: _createFund, + child: Text('createFund'.tr()), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Future _createFund() async { + final amount = double.tryParse(amountController.text); + + if (amount == null || amount <= 0) { + showErrorAlert('invalidAmount'.tr()); + return; + } + + if (selectedRecipients.isEmpty) { + showErrorAlert('noRecipientsSelected'.tr()); + return; + } + + final data = { + 'currency': selectedCurrency, + 'total_amount': amount, + 'split_type': selectedSplitType, + 'recipient_account_ids': selectedRecipients.map((r) => r.id).toList(), + 'message': + messageController.text.trim().isEmpty + ? null + : messageController.text.trim(), + }; + + Navigator.of(context).pop(data); + } +} + +const Map kCurrencyIconData = { + 'points': Symbols.toll, + 'golds': Symbols.attach_money, +}; + +@riverpod +class TransactionListNotifier extends _$TransactionListNotifier + with CursorPagingNotifierMixin { + static const int _pageSize = 20; + + @override + Future> build() => fetch(cursor: null); + + @override + Future> 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( + '/id/wallets/transactions', + queryParameters: queryParams, + ); + final total = int.parse(response.headers.value('X-Total') ?? '0'); + final List 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, + ); + } +} + +@riverpod +Future> walletFunds( + Ref ref, { + int offset = 0, + int take = 20, +}) async { + final client = ref.watch(apiClientProvider); + final resp = await client.get('/id/wallets/funds?offset=$offset&take=$take'); + return (resp.data as List).map((e) => SnWalletFund.fromJson(e)).toList(); +} + +@riverpod +Future> walletFundRecipients( + Ref ref, { + int offset = 0, + int take = 20, +}) async { + final client = ref.watch(apiClientProvider); + final resp = await client.get( + '/id/wallets/funds/recipients?offset=$offset&take=$take', + ); + return (resp.data as List) + .map((e) => SnWalletFundRecipient.fromJson(e)) + .toList(); +} + +@riverpod +Future walletFund(Ref ref, String fundId) async { + final client = ref.watch(apiClientProvider); + final resp = await client.get('/id/wallets/funds/$fundId'); + return SnWalletFund.fromJson(resp.data); +} + +@riverpod +Future> walletFundStats(Ref ref) async { + final client = ref.watch(apiClientProvider); + final resp = await client.get('/id/wallets/funds/stats'); + return resp.data as Map; +} + +class WalletScreen extends HookConsumerWidget { + const WalletScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final wallet = ref.watch(walletCurrentProvider); + final tabController = useTabController(initialLength: 3); + final fundStats = ref.watch(walletFundStatsProvider); + + Future createWallet() async { + final client = ref.read(apiClientProvider); + try { + await client.post('/id/wallets'); + ref.invalidate(walletCurrentProvider); + } catch (err) { + showErrorAlert(err); + } + } + + Future createFund() async { + final result = await showModalBottomSheet>( + context: context, + useRootNavigator: true, + isScrollControlled: true, + builder: (context) => const CreateFundSheet(), + ); + + if (result != null && context.mounted) { + await _handleFundCreation(context, ref, result); + } + } + + String getCurrencyTranslationKey(String currency, {bool isShort = false}) { + return 'walletCurrency${isShort ? 'Short' : ''}${currency[0].toUpperCase()}${currency.substring(1).toLowerCase()}'; + } + + return AppScaffold( + appBar: AppBar( + title: Text('wallet').tr(), + actions: [ + IconButton( + icon: const Icon(Symbols.add), + onPressed: createFund, + tooltip: 'createFund'.tr(), + ), + const Gap(8), + ], + ), + body: wallet.when( + data: (data) { + if (data == null) { + return ConstrainedBox( + constraints: BoxConstraints(maxWidth: 280), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('walletNotFound').tr().fontSize(16).bold(), + Text('walletCreateHint', textAlign: TextAlign.center).tr(), + TextButton( + onPressed: createWallet, + child: Text('walletCreate').tr(), + ), + ], + ), + ).center(); + } + + return Column( + children: [ + // Wallet Overview + Column( + spacing: 8, + children: [ + // Pockets + ...data.pockets.map( + (pocket) => Card( + margin: EdgeInsets.zero, + child: ListTile( + leading: Icon( + kCurrencyIconData[pocket.currency] ?? + Symbols.universal_currency_alt, + ), + title: + Text( + getCurrencyTranslationKey(pocket.currency), + ).tr(), + subtitle: Text( + '${pocket.amount.toStringAsFixed(2)} ${getCurrencyTranslationKey(pocket.currency, isShort: true).tr()}', + ), + ), + ), + ), + + // Fund Stats + fundStats.when( + data: + (stats) => Card( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Symbols.celebration, + color: + Theme.of(context).colorScheme.primary, + ), + const Gap(8), + Text( + 'fundOverview'.tr(), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const Gap(8), + Row( + children: [ + Expanded( + child: _buildStatItem( + context, + 'totalFundsSent'.tr(), + '${stats['total_sent'] ?? 0}', + Icons.send, + ), + ), + Expanded( + child: _buildStatItem( + context, + 'totalFundsReceived'.tr(), + '${stats['total_received'] ?? 0}', + Icons.call_received, + ), + ), + ], + ), + ], + ), + ), + ), + loading: + () => const Card( + margin: EdgeInsets.zero, + child: Padding( + padding: EdgeInsets.all(16), + child: Center(child: CircularProgressIndicator()), + ), + ), + error: (error, stack) => const SizedBox.shrink(), + ), + ], + ).padding(horizontal: 16, vertical: 16), + + // Tab Bar + TabBar( + controller: tabController, + tabs: [ + Tab(text: 'transactions'.tr()), + Tab(text: 'myFunds'.tr()), + ], + ), + + // Tab Content + Expanded( + child: TabBarView( + controller: tabController, + children: [ + // Transactions Tab + 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, + ), + ), + ); + }, + ), + ), + + // My Funds Tab + _buildFundsList(context, ref), + ], + ), + ), + ], + ); + }, + error: + (error, stackTrace) => ResponseErrorWidget( + error: error, + onRetry: () => ref.invalidate(walletCurrentProvider), + ), + loading: () => const Center(child: CircularProgressIndicator()), + ), + ); + } + + Widget _buildStatItem( + BuildContext context, + String label, + String value, + IconData icon, + ) { + return Column( + children: [ + Icon(icon, size: 20, color: Theme.of(context).colorScheme.primary), + const Gap(4), + Text( + value, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + Text( + label, + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ), + ], + ); + } + + Widget _buildFundsList(BuildContext context, WidgetRef ref) { + final funds = ref.watch(walletFundsProvider()); + + return funds.when( + data: (fundList) { + if (fundList.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Symbols.celebration, + size: 48, + color: Theme.of(context).colorScheme.outline, + ), + const Gap(16), + Text( + 'noFundsCreated'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + const Gap(8), + Text( + 'createYourFirstFund'.tr(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: fundList.length, + itemBuilder: (context, index) { + final fund = fundList[index]; + final claimedCount = + fund.recipients.where((r) => r.status == 1).length; + final totalRecipients = fund.recipients.length; + + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Symbols.celebration, + color: Theme.of(context).colorScheme.primary, + ), + const Gap(8), + Expanded( + child: Text( + '${fund.totalAmount.toStringAsFixed(2)} ${fund.currency}', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: _getFundStatusColor( + context, + fund.status, + ).withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + _getFundStatusText(fund.status), + style: TextStyle( + color: _getFundStatusColor(context, fund.status), + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const Gap(8), + Text( + '${'recipients'.tr()}: $claimedCount/$totalRecipients', + style: Theme.of(context).textTheme.bodyMedium, + ), + if (fund.message != null && fund.message!.isNotEmpty) ...[ + const Gap(4), + Text( + fund.message!, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + const Gap(8), + Text( + DateFormat.yMd().add_Hm().format(fund.createdAt), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center(child: Text('Error: $error')), + ); + } + + Future _handleFundCreation( + BuildContext context, + WidgetRef ref, + Map fundData, + ) async { + final client = ref.read(apiClientProvider); + try { + showLoadingModal(context); + final resp = await client.post( + '/id/wallets/funds', + data: fundData, + options: Options(headers: {'X-Noop': true}), + ); + final fund = SnWalletFund.fromJson(resp.data); + if (fund.status == 0) return; // Already created + + final orderResp = await client.post('/id/wallets/funds/${fund.id}/order'); + final order = SnWalletOrder.fromJson(orderResp.data); + + if (context.mounted) hideLoadingModal(context); + + // Show payment overlay to complete the payment + if (!context.mounted) return; + final paidOrder = await PaymentOverlay.show( + context: context, + order: order, + enableBiometric: true, + ); + + if (context.mounted) showLoadingModal(context); + + if (paidOrder != null) { + // Wait for server to handle order + await Future.delayed(const Duration(seconds: 1)); + ref.invalidate(walletFundsProvider); + ref.invalidate(walletFundStatsProvider); + ref.invalidate(walletCurrentProvider); + if (context.mounted) { + showSnackBar('fundCreatedSuccessfully'.tr()); + } + } + } catch (err) { + showErrorAlert(err); + } finally { + if (context.mounted) hideLoadingModal(context); + } + } + + String _getFundStatusText(int status) { + switch (status) { + case 0: + return 'fundStatusCreated'.tr(); + case 1: + return 'fundStatusPartial'.tr(); + case 2: + return 'fundStatusCompleted'.tr(); + case 3: + return 'fundStatusExpired'.tr(); + default: + return 'fundStatusUnknown'.tr(); + } + } + + Color _getFundStatusColor(BuildContext context, int status) { + switch (status) { + case 0: + return Colors.blue; + case 1: + return Colors.orange; + case 2: + return Colors.green; + case 3: + return Colors.red; + default: + return Theme.of(context).colorScheme.primary; + } + } +} diff --git a/lib/screens/wallet.g.dart b/lib/screens/wallet.g.dart index a24080b8..9fb4320e 100644 --- a/lib/screens/wallet.g.dart +++ b/lib/screens/wallet.g.dart @@ -24,6 +24,444 @@ final walletCurrentProvider = AutoDisposeFutureProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef WalletCurrentRef = AutoDisposeFutureProviderRef; +String _$walletFundsHash() => r'7ceb415f64fcadab2b10461e27b95bf92352c707'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [walletFunds]. +@ProviderFor(walletFunds) +const walletFundsProvider = WalletFundsFamily(); + +/// See also [walletFunds]. +class WalletFundsFamily extends Family>> { + /// See also [walletFunds]. + const WalletFundsFamily(); + + /// See also [walletFunds]. + WalletFundsProvider call({int offset = 0, int take = 20}) { + return WalletFundsProvider(offset: offset, take: take); + } + + @override + WalletFundsProvider getProviderOverride( + covariant WalletFundsProvider provider, + ) { + return call(offset: provider.offset, take: provider.take); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'walletFundsProvider'; +} + +/// See also [walletFunds]. +class WalletFundsProvider + extends AutoDisposeFutureProvider> { + /// See also [walletFunds]. + WalletFundsProvider({int offset = 0, int take = 20}) + : this._internal( + (ref) => walletFunds(ref as WalletFundsRef, offset: offset, take: take), + from: walletFundsProvider, + name: r'walletFundsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$walletFundsHash, + dependencies: WalletFundsFamily._dependencies, + allTransitiveDependencies: WalletFundsFamily._allTransitiveDependencies, + offset: offset, + take: take, + ); + + WalletFundsProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.offset, + required this.take, + }) : super.internal(); + + final int offset; + final int take; + + @override + Override overrideWith( + FutureOr> Function(WalletFundsRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: WalletFundsProvider._internal( + (ref) => create(ref as WalletFundsRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + offset: offset, + take: take, + ), + ); + } + + @override + AutoDisposeFutureProviderElement> createElement() { + return _WalletFundsProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is WalletFundsProvider && + other.offset == offset && + other.take == take; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, offset.hashCode); + hash = _SystemHash.combine(hash, take.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin WalletFundsRef on AutoDisposeFutureProviderRef> { + /// The parameter `offset` of this provider. + int get offset; + + /// The parameter `take` of this provider. + int get take; +} + +class _WalletFundsProviderElement + extends AutoDisposeFutureProviderElement> + with WalletFundsRef { + _WalletFundsProviderElement(super.provider); + + @override + int get offset => (origin as WalletFundsProvider).offset; + @override + int get take => (origin as WalletFundsProvider).take; +} + +String _$walletFundRecipientsHash() => + r'18eb815eb709449dd5c545d81fc0ee43ca667578'; + +/// See also [walletFundRecipients]. +@ProviderFor(walletFundRecipients) +const walletFundRecipientsProvider = WalletFundRecipientsFamily(); + +/// See also [walletFundRecipients]. +class WalletFundRecipientsFamily + extends Family>> { + /// See also [walletFundRecipients]. + const WalletFundRecipientsFamily(); + + /// See also [walletFundRecipients]. + WalletFundRecipientsProvider call({int offset = 0, int take = 20}) { + return WalletFundRecipientsProvider(offset: offset, take: take); + } + + @override + WalletFundRecipientsProvider getProviderOverride( + covariant WalletFundRecipientsProvider provider, + ) { + return call(offset: provider.offset, take: provider.take); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'walletFundRecipientsProvider'; +} + +/// See also [walletFundRecipients]. +class WalletFundRecipientsProvider + extends AutoDisposeFutureProvider> { + /// See also [walletFundRecipients]. + WalletFundRecipientsProvider({int offset = 0, int take = 20}) + : this._internal( + (ref) => walletFundRecipients( + ref as WalletFundRecipientsRef, + offset: offset, + take: take, + ), + from: walletFundRecipientsProvider, + name: r'walletFundRecipientsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$walletFundRecipientsHash, + dependencies: WalletFundRecipientsFamily._dependencies, + allTransitiveDependencies: + WalletFundRecipientsFamily._allTransitiveDependencies, + offset: offset, + take: take, + ); + + WalletFundRecipientsProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.offset, + required this.take, + }) : super.internal(); + + final int offset; + final int take; + + @override + Override overrideWith( + FutureOr> Function( + WalletFundRecipientsRef provider, + ) + create, + ) { + return ProviderOverride( + origin: this, + override: WalletFundRecipientsProvider._internal( + (ref) => create(ref as WalletFundRecipientsRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + offset: offset, + take: take, + ), + ); + } + + @override + AutoDisposeFutureProviderElement> + createElement() { + return _WalletFundRecipientsProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is WalletFundRecipientsProvider && + other.offset == offset && + other.take == take; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, offset.hashCode); + hash = _SystemHash.combine(hash, take.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin WalletFundRecipientsRef + on AutoDisposeFutureProviderRef> { + /// The parameter `offset` of this provider. + int get offset; + + /// The parameter `take` of this provider. + int get take; +} + +class _WalletFundRecipientsProviderElement + extends AutoDisposeFutureProviderElement> + with WalletFundRecipientsRef { + _WalletFundRecipientsProviderElement(super.provider); + + @override + int get offset => (origin as WalletFundRecipientsProvider).offset; + @override + int get take => (origin as WalletFundRecipientsProvider).take; +} + +String _$walletFundHash() => r'a690b0def8f4293b4a8f244e44f8bb735687e5dd'; + +/// See also [walletFund]. +@ProviderFor(walletFund) +const walletFundProvider = WalletFundFamily(); + +/// See also [walletFund]. +class WalletFundFamily extends Family> { + /// See also [walletFund]. + const WalletFundFamily(); + + /// See also [walletFund]. + WalletFundProvider call(String fundId) { + return WalletFundProvider(fundId); + } + + @override + WalletFundProvider getProviderOverride( + covariant WalletFundProvider provider, + ) { + return call(provider.fundId); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'walletFundProvider'; +} + +/// See also [walletFund]. +class WalletFundProvider extends AutoDisposeFutureProvider { + /// See also [walletFund]. + WalletFundProvider(String fundId) + : this._internal( + (ref) => walletFund(ref as WalletFundRef, fundId), + from: walletFundProvider, + name: r'walletFundProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$walletFundHash, + dependencies: WalletFundFamily._dependencies, + allTransitiveDependencies: WalletFundFamily._allTransitiveDependencies, + fundId: fundId, + ); + + WalletFundProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.fundId, + }) : super.internal(); + + final String fundId; + + @override + Override overrideWith( + FutureOr Function(WalletFundRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: WalletFundProvider._internal( + (ref) => create(ref as WalletFundRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + fundId: fundId, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _WalletFundProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is WalletFundProvider && other.fundId == fundId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, fundId.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin WalletFundRef on AutoDisposeFutureProviderRef { + /// The parameter `fundId` of this provider. + String get fundId; +} + +class _WalletFundProviderElement + extends AutoDisposeFutureProviderElement + with WalletFundRef { + _WalletFundProviderElement(super.provider); + + @override + String get fundId => (origin as WalletFundProvider).fundId; +} + +String _$walletFundStatsHash() => r'fac8761cf7828fa151e8cc9115416265148bd00e'; + +/// See also [walletFundStats]. +@ProviderFor(walletFundStats) +final walletFundStatsProvider = + AutoDisposeFutureProvider>.internal( + walletFundStats, + name: r'walletFundStatsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$walletFundStatsHash, + dependencies: null, + allTransitiveDependencies: null, + ); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef WalletFundStatsRef = AutoDisposeFutureProviderRef>; String _$transactionListNotifierHash() => r'7b777cd44f3351f68f7bd1dd76bfe8b388381bdb'; diff --git a/lib/widgets/account/stellar_program_tab.g.dart b/lib/widgets/account/stellar_program_tab.g.dart index 06e728d5..8c1cc959 100644 --- a/lib/widgets/account/stellar_program_tab.g.dart +++ b/lib/widgets/account/stellar_program_tab.g.dart @@ -27,7 +27,7 @@ final accountStellarSubscriptionProvider = // ignore: unused_element typedef AccountStellarSubscriptionRef = AutoDisposeFutureProviderRef; -String _$accountSentGiftsHash() => r'32a282ec863023c749d81423704787943110a188'; +String _$accountSentGiftsHash() => r'36fdf4e2be3805e8501cca1657d8be27aa891ef4'; /// Copied from Dart SDK class _SystemHash { @@ -187,7 +187,7 @@ class _AccountSentGiftsProviderElement } String _$accountReceivedGiftsHash() => - r'7c0dfcc109f6f50ec326dd64c2d944aaccd9f775'; + r'15f6f7e27aa0277b67b02dd72875ba3efd147dfd'; /// See also [accountReceivedGifts]. @ProviderFor(accountReceivedGifts) @@ -328,7 +328,7 @@ class _AccountReceivedGiftsProviderElement int get take => (origin as AccountReceivedGiftsProvider).take; } -String _$accountGiftHash() => r'7169d355f78e4fe3bf6b3ff444350faa46a0d216'; +String _$accountGiftHash() => r'598c5071fff3cc1dd7eb867df48d69fef04543db'; /// See also [accountGift]. @ProviderFor(accountGift)