confetti animation

This commit is contained in:
2026-03-03 12:15:50 +01:00
parent 77fa21b572
commit 88275c4ae6
16 changed files with 401 additions and 298 deletions

View File

@@ -8,10 +8,11 @@ import 'package:sf_localizations/sf_localizations.dart';
import '../../card_colors.dart';
import '../../presentation/state/home_view_model.dart';
import '../deposit/deposit_success_provider.dart';
import 'child_wallet_view_model.dart';
import 'wallet_actions_bar.dart';
class ChildWalletScreen extends ConsumerWidget {
class ChildWalletScreen extends ConsumerStatefulWidget {
final String childId;
final NavigationContract navigation;
@@ -22,16 +23,21 @@ class ChildWalletScreen extends ConsumerWidget {
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
final viewState = ref.watch(childWalletViewModelProvider(childId));
ConsumerState<ChildWalletScreen> createState() => _ChildWalletScreenState();
}
ref.listen(childWalletViewModelProvider(childId), (prev, next) {
class _ChildWalletScreenState extends ConsumerState<ChildWalletScreen> {
@override
Widget build(BuildContext context) {
final theme = ref.watch(themePortProvider);
final viewState = ref.watch(childWalletViewModelProvider(widget.childId));
final depositSuccess = ref.watch(depositSuccessProvider(widget.childId));
ref.listen(childWalletViewModelProvider(widget.childId), (prev, next) {
if (next.cardStatusSuccess && !(prev?.cardStatusSuccess ?? false)) {
ref.read(homeViewModelProvider.notifier).updateChildCardStatus(
childId,
next.cardStatus,
);
ref
.read(homeViewModelProvider.notifier)
.updateChildCardStatus(widget.childId, next.cardStatus);
showTopSnackbar(
context,
message: context.translate(I18n.cardStatusSuccess),
@@ -50,7 +56,7 @@ class ChildWalletScreen extends ConsumerWidget {
if (viewState.showPin) {
final viewModel = ref.read(
childWalletViewModelProvider(childId).notifier,
childWalletViewModelProvider(widget.childId).notifier,
);
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundSecondary),
@@ -117,197 +123,219 @@ class ChildWalletScreen extends ConsumerWidget {
cardStatus: viewState.cardStatus,
);
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundSecondary),
body: Stack(
children: [
DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
bottomRight: Radius.circular(24),
bottomLeft: Radius.circular(24),
),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: locked ? theme.getDisabledCardColors() : cardColors,
return ConfettiOverlay(
play: depositSuccess,
onPlayed: () =>
ref.read(depositSuccessProvider(widget.childId).notifier).reset(),
child: Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundSecondary),
body: Stack(
children: [
DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
bottomRight: Radius.circular(24),
bottomLeft: Radius.circular(24),
),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: locked ? theme.getDisabledCardColors() : cardColors,
),
),
child: SizedBox(width: double.infinity, height: 420),
),
child: SizedBox(width: double.infinity, height: 420),
),
SingleChildScrollView(
padding: EdgeInsets.symmetric(vertical: 50, horizontal: 20),
child: Column(
spacing: 24,
children: [
Row(
spacing: 7,
children: [
IconButton(
onPressed: () => navigation.goBack(),
icon: Icon(
Icons.arrow_back_ios_new_outlined,
color: theme.getColorFor(ThemeCode.backgroundPrimary),
size: 24,
),
),
_buildGenderAvatar(device?.carrierGenre, 50),
Text(
childName,
style: TextStyle(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
],
),
Column(
spacing: 16,
children: [
MoneyText(
text: "${availableBalance.toString()}",
size: 60,
secondarySize: 24,
color: theme.getColorFor(ThemeCode.textSecondary),
),
Text(
context.translate(I18n.childWalletAvailableBalance),
style: TextStyle(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
),
),
LinearProgressIndicator(
value: 0.7,
color: theme.getColorFor(ThemeCode.backgroundPrimary),
backgroundColor: theme
.getColorFor(ThemeCode.backgroundPrimary)
.withAlpha(0x4C),
minHeight: 10,
borderRadius: BorderRadius.all(Radius.circular(5)),
),
if (CardStatus.fromString(viewState.cardStatus) == CardStatus.lost ||
CardStatus.fromString(viewState.cardStatus) == CardStatus.stolen)
TextButton(
style: ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.all(0)),
),
onPressed: viewState.isUpdatingCard
? null
: () => _showDeleteConfirmation(context, ref),
child: Row(
spacing: 10,
children: [
Icon(
Icons.delete_outline,
size: 24,
color: theme.getColorFor(ThemeCode.textSecondary),
),
Text(
context.translate(I18n.deleteDevice),
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
color: theme.getColorFor(ThemeCode.textSecondary),
),
),
],
),
)
else
TextButton(
style: ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.all(0)),
),
onPressed: () =>
_showCardStatusSheet(context, ref, theme),
child: Row(
spacing: 10,
children: [
Icon(
Icons.lock_outline,
size: 24,
color: theme.getColorFor(ThemeCode.textSecondary),
),
Text(
locked
? context.translate(I18n.childWalletUnlockCard)
: context.translate(I18n.childWalletLockCard),
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
color: theme.getColorFor(ThemeCode.textSecondary),
),
),
],
SingleChildScrollView(
padding: EdgeInsets.symmetric(vertical: 50, horizontal: 20),
child: Column(
spacing: 24,
children: [
Row(
spacing: 7,
children: [
IconButton(
onPressed: () => widget.navigation.goBack(),
icon: Icon(
Icons.arrow_back_ios_new_outlined,
color: theme.getColorFor(ThemeCode.backgroundPrimary),
size: 24,
),
),
],
),
Column(
spacing: 16,
children: [
if (!locked)
WalletActionsBar(childId: childId, navigation: navigation),
Container(
padding: EdgeInsets.all(15),
decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: BorderRadius.all(Radius.circular(20)),
_buildGenderAvatar(device?.carrierGenre, 50),
Text(
childName,
style: TextStyle(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.translate(
I18n.childWalletRecentTransactions,
),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
],
),
Column(
spacing: 16,
children: [
MoneyText(
text: "${availableBalance.toString()}",
size: 60,
secondarySize: 24,
color: theme.getColorFor(ThemeCode.textSecondary),
),
Text(
context.translate(I18n.childWalletAvailableBalance),
style: TextStyle(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
),
),
LinearProgressIndicator(
value: 0.7,
color: theme.getColorFor(ThemeCode.backgroundPrimary),
backgroundColor: theme
.getColorFor(ThemeCode.backgroundPrimary)
.withAlpha(0x4C),
minHeight: 10,
borderRadius: BorderRadius.all(Radius.circular(5)),
),
if (CardStatus.fromString(viewState.cardStatus) ==
CardStatus.lost ||
CardStatus.fromString(viewState.cardStatus) ==
CardStatus.stolen)
TextButton(
style: ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.all(0)),
),
const SizedBox(height: 16),
if (viewState.isLoadingTransactions)
const Center(
child: Padding(
padding: EdgeInsets.all(24),
child: AppLoadingIndicator(size: 48),
onPressed: viewState.isUpdatingCard
? null
: () => _showDeleteConfirmation(context, ref),
child: Row(
spacing: 10,
children: [
Icon(
Icons.delete_outline,
size: 24,
color: theme.getColorFor(
ThemeCode.textSecondary,
),
),
)
else if (viewState.transactions.isEmpty)
Padding(
padding: const EdgeInsets.all(24),
child: Center(
child: Text(
context.translate(
I18n.activityNoTransactions,
),
style: TextStyle(
fontSize: 14,
color: theme.getColorFor(
ThemeCode.textPrimary,
),
Text(
context.translate(I18n.deleteDevice),
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
color: theme.getColorFor(
ThemeCode.textSecondary,
),
),
),
)
else
...viewState.transactions.map(
(tx) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: TransactionTile(transaction: tx),
],
),
)
else
TextButton(
style: ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.all(0)),
),
onPressed: () =>
_showCardStatusSheet(context, ref, theme),
child: Row(
spacing: 10,
children: [
Icon(
Icons.lock_outline,
size: 24,
color: theme.getColorFor(
ThemeCode.textSecondary,
),
),
Text(
locked
? context.translate(
I18n.childWalletUnlockCard,
)
: context.translate(
I18n.childWalletLockCard,
),
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
color: theme.getColorFor(
ThemeCode.textSecondary,
),
),
),
],
),
),
],
),
Column(
spacing: 16,
children: [
if (!locked)
WalletActionsBar(
childId: widget.childId,
navigation: widget.navigation,
),
Container(
padding: EdgeInsets.all(15),
decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: BorderRadius.all(Radius.circular(20)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.translate(
I18n.childWalletRecentTransactions,
),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
],
const SizedBox(height: 16),
if (viewState.isLoadingTransactions)
const Center(
child: Padding(
padding: EdgeInsets.all(24),
child: AppLoadingIndicator(size: 48),
),
)
else if (viewState.transactions.isEmpty)
Padding(
padding: const EdgeInsets.all(24),
child: Center(
child: Text(
context.translate(
I18n.activityNoTransactions,
),
style: TextStyle(
fontSize: 14,
color: theme.getColorFor(
ThemeCode.textPrimary,
),
),
),
),
)
else
...viewState.transactions.map(
(tx) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: TransactionTile(transaction: tx),
),
),
],
),
),
),
],
),
],
],
),
],
),
),
),
],
],
),
),
);
}
@@ -344,7 +372,7 @@ class ChildWalletScreen extends ConsumerWidget {
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
builder: (_) => _CardStatusSheet(childId: childId),
builder: (_) => _CardStatusSheet(childId: widget.childId),
);
}
@@ -354,9 +382,7 @@ class ChildWalletScreen extends ConsumerWidget {
context: context,
builder: (_) => AlertDialog(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Text(context.translate(I18n.deleteDeviceConfirmTitle)),
content: Text(context.translate(I18n.deleteDeviceConfirmMessage)),
actions: [
@@ -368,17 +394,19 @@ class ChildWalletScreen extends ConsumerWidget {
onPressed: () async {
Navigator.of(context).pop();
final viewModel = ref.read(
childWalletViewModelProvider(childId).notifier,
childWalletViewModelProvider(widget.childId).notifier,
);
final success = await viewModel.deleteDevice();
if (success && context.mounted) {
ref.read(homeViewModelProvider.notifier).removeChild(childId);
ref
.read(homeViewModelProvider.notifier)
.removeChild(widget.childId);
showTopSnackbar(
context,
message: context.translate(I18n.deleteDeviceSuccess),
type: MessageType.success,
);
navigation.goBack();
widget.navigation.goBack();
}
},
child: Text(
@@ -417,7 +445,11 @@ class _CardStatusSheetState extends ConsumerState<_CardStatusSheet> {
final viewState = ref.watch(childWalletViewModelProvider(widget.childId));
final currentStatus = viewState.cardStatus;
final statuses = CardStatus.fromString(currentStatus) == CardStatus.unlock
? [CardStatus.lock.value, CardStatus.lost.value, CardStatus.stolen.value]
? [
CardStatus.lock.value,
CardStatus.lost.value,
CardStatus.stolen.value,
]
: [CardStatus.unlock.value];
_selected ??= statuses.first;

View File

@@ -7,6 +7,7 @@ import 'package:sf_localizations/sf_localizations.dart';
import '../../card_colors.dart';
import '../child_wallet/child_data_provider.dart';
import 'deposit_success_provider.dart';
import 'deposit_view_model.dart';
class DepositScreen extends ConsumerWidget {
@@ -33,6 +34,7 @@ class DepositScreen extends ConsumerWidget {
message: context.translate(I18n.walletMoveSuccess),
type: MessageType.success,
);
ref.read(depositSuccessProvider(childId).notifier).trigger();
navigation.goBack();
}
if (next.errorMessage.isNotEmpty &&
@@ -122,106 +124,6 @@ class DepositScreen extends ConsumerWidget {
),
],
),
// Container(
// decoration: BoxDecoration(
// color: theme.getColorFor(ThemeCode.backgroundPrimary),
// borderRadius: const BorderRadius.all(Radius.circular(20)),
// ),
// padding: const EdgeInsets.all(10),
// child: Column(
// spacing: 10,
// children: [
// Text(
// context.translate(I18n.depositReason),
// style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
// ),
// Text(context.translate(I18n.watchInfo)),
// CheckboxListTile(
// contentPadding: EdgeInsets.zero,
// title: Text(context.translate(I18n.depositReasonWeekly)),
// controlAffinity: ListTileControlAffinity.leading,
// value: viewState.reason == 'weekly',
// onChanged: (_) => viewModel.selectReason('weekly'),
// activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
// ),
// CheckboxListTile(
// contentPadding: EdgeInsets.zero,
// title: Text(context.translate(I18n.depositReasonGoalMet)),
// controlAffinity: ListTileControlAffinity.leading,
// value: viewState.reason == 'goal',
// onChanged: (_) => viewModel.selectReason('goal'),
// activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
// ),
// CheckboxListTile(
// contentPadding: EdgeInsets.zero,
// title: Text(context.translate(I18n.depositReasonExtraordinary)),
// controlAffinity: ListTileControlAffinity.leading,
// value: viewState.reason == 'extraordinary',
// onChanged: (_) => viewModel.selectReason('extraordinary'),
// activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
// ),
// CheckboxListTile(
// contentPadding: EdgeInsets.zero,
// title: Text(context.translate(I18n.depositReasonOther)),
// controlAffinity: ListTileControlAffinity.leading,
// value: viewState.reason == 'other',
// onChanged: (_) => viewModel.selectReason('other'),
// activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
// ),
// CustomTextField(
// controller: viewModel.messageController,
// lines: 3,
// length: 150,
// label: context.translate(I18n.depositMessageLabel, args: {'name': childName}),
// hint: context.translate(I18n.allowanceMessageHint),
// ),
// Align(
// alignment: Alignment.topLeft,
// child: Text(context.translate(I18n.allowanceMaxChars, args: {'count': '150'})),
// ),
// ],
// ),
// ),
// Container(
// decoration: BoxDecoration(
// color: theme.getColorFor(ThemeCode.backgroundPrimary),
// borderRadius: const BorderRadius.all(Radius.circular(20)),
// ),
// padding: const EdgeInsets.all(10),
// child: Column(
// spacing: 10,
// children: [
// Text(
// context.translate(I18n.depositWhenSend),
// style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
// ),
// Text(context.translate(I18n.watchInfo)),
// CheckboxListTile(
// contentPadding: EdgeInsets.zero,
// title: Text(context.translate(I18n.depositNow)),
// controlAffinity: ListTileControlAffinity.leading,
// value: !viewState.program,
// onChanged: (_) {
// if (viewState.program) viewModel.toggleProgram();
// },
// activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
// ),
// CheckboxListTile(
// contentPadding: EdgeInsets.zero,
// title: Text(context.translate(I18n.depositSchedule)),
// controlAffinity: ListTileControlAffinity.leading,
// value: viewState.program,
// onChanged: (_) {
// if (!viewState.program) viewModel.toggleProgram();
// },
// activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
// ),
// if (viewState.program) CustomTextField(),
// ],
// ),
// ),
],
);
}

View File

@@ -0,0 +1,22 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
final depositSuccessProvider =
NotifierProvider.family<DepositSuccessNotifier, bool, String>(
DepositSuccessNotifier.new,
);
class DepositSuccessNotifier extends Notifier<bool> {
final String childId;
DepositSuccessNotifier(this.childId);
@override
bool build() => false;
void trigger() {
state = true;
}
void reset() {
state = false;
}
}