Compare commits

...

2 Commits

Author SHA1 Message Date
46b062bc48 splash gif switched by lottie animation 2026-03-05 10:12:55 +01:00
88275c4ae6 confetti animation 2026-03-03 12:15:50 +01:00
21 changed files with 451 additions and 326 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 KiB

View File

@@ -32,6 +32,9 @@ Future<void> initApp(EnvironmentEnum env) async {
log: env.isDevelopment || kDebugMode,
onTokenExpired: () => appRouter.go(AppRoutes.scaTreezor),
onUnauthorized: () async {
final currentLocation =
appRouter.routerDelegate.currentConfiguration.uri.path;
if (currentLocation == AppRoutes.splash) return;
try {
await GetIt.I<TreezorWalletConnectionService>().logout();
} catch (_) {}

View File

@@ -199,6 +199,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
confetti:
dependency: transitive
description:
name: confetti
sha256: "979aafde2428c53947892c95eb244466c109c129b7eee9011f0a66caaca52267"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
convert:
dependency: transitive
description:

View File

@@ -121,6 +121,12 @@
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "confetti",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/confetti-0.7.0",
"packageUri": "lib/",
"languageVersion": "2.17"
},
{
"name": "convert",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/convert-3.1.2",

View File

@@ -104,6 +104,7 @@
"name": "design_system",
"version": "0.0.1",
"dependencies": [
"confetti",
"country_code_picker",
"flutter",
"flutter_riverpod",
@@ -356,6 +357,14 @@
"meta"
]
},
{
"name": "confetti",
"version": "0.7.0",
"dependencies": [
"flutter",
"vector_math"
]
},
{
"name": "lottie",
"version": "3.3.2",

View File

@@ -161,6 +161,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
confetti:
dependency: transitive
description:
name: confetti
sha256: "979aafde2428c53947892c95eb244466c109c129b7eee9011f0a66caaca52267"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
convert:
dependency: transitive
description:

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;
}
}

View File

@@ -1,9 +1,7 @@
import 'dart:async';
import 'package:animated_splash_screen/animated_splash_screen.dart';
import 'package:flutter/material.dart';
import 'package:navigation/app_routes.dart';
import 'package:navigation/navigation_contract.dart';
import 'package:page_transition/page_transition.dart';
import 'package:lottie/lottie.dart';
import 'package:navigation/navigation.dart';
import 'package:splash/src/domain/check_session_use_case.dart';
import 'package:splash/src/domain/initial_route.dart';
@@ -21,40 +19,61 @@ class SplashScreen extends StatefulWidget {
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
class _SplashScreenState extends State<SplashScreen>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
InitialRoute? _route;
bool _animationDone = false;
@override
void initState() {
super.initState();
unawaited(nextRoute());
_controller = AnimationController(vsync: this)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_animationDone = true;
_navigateIfReady();
}
});
unawaited(_checkSession());
}
Future<void> nextRoute() async {
final results = await Future.wait([
widget.checkSessionUseCase.execute(),
Future.delayed(const Duration(milliseconds: 3500)),
]);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
if (!mounted) return;
Future<void> _checkSession() async {
_route = await widget.checkSessionUseCase.execute();
_navigateIfReady();
}
final route = results[0] as InitialRoute;
switch (route) {
case InitialRoute.login:
widget.navigationContract.goTo(AppRoutes.login);
case InitialRoute.home:
widget.navigationContract.goTo(AppRoutes.dashboardHome);
}
void _navigateIfReady() {
if (!_animationDone || _route == null || !mounted) return;
final destination = switch (_route!) {
InitialRoute.login => AppRoutes.login,
InitialRoute.home => AppRoutes.dashboardHome,
};
widget.navigationContract.goTo(destination);
}
@override
Widget build(BuildContext context) {
return AnimatedSplashScreen(
splash: Image.asset('assets/shared/images/splash.gif'),
splashIconSize: 900.0,
nextScreen: const SizedBox.shrink(),
disableNavigation: true,
return Scaffold(
backgroundColor: Colors.white,
splashTransition: SplashTransition.fadeTransition,
pageTransitionType: PageTransitionType.fade,
body: Center(
child: Lottie.asset(
'assets/shared/images/intro_app.json',
controller: _controller,
onLoaded: (composition) {
_controller
..duration = composition.duration
..forward();
},
),
),
);
}
}

View File

@@ -11,10 +11,9 @@ environment:
dependencies:
flutter:
sdk: flutter
animated_splash_screen: ^1.3.0
lottie: ^3.3.1
go_router: ^17.0.0
get_it: ^9.0.5
page_transition: ^2.2.1
navigation:
path: ../../packages/navigation
sf_infrastructure:

View File

@@ -15,3 +15,4 @@ export 'src/containers/section_container.dart';
export 'src/containers/footer_container.dart';
export 'src/rows/editable_row.dart';
export 'src/loading/app_loading_indicator.dart';
export 'src/confetti/confetti_overlay.dart';

View File

@@ -0,0 +1,91 @@
import 'dart:math';
import 'package:confetti/confetti.dart';
import 'package:flutter/material.dart';
class ConfettiOverlay extends StatefulWidget {
final bool play;
final Widget child;
final VoidCallback? onPlayed;
final Duration duration;
final List<Color> colors;
const ConfettiOverlay({
super.key,
required this.play,
required this.child,
this.onPlayed,
this.duration = const Duration(seconds: 3),
this.colors = const [
Color(0xFFFF69B4),
Color(0xFF4169E1),
Color(0xFFFF8C00),
Color(0xFF8A2BE2),
Color(0xFFFFD700),
],
});
@override
State<ConfettiOverlay> createState() => _ConfettiOverlayState();
}
class _ConfettiOverlayState extends State<ConfettiOverlay> {
late final ConfettiController _controller;
@override
void initState() {
super.initState();
_controller = ConfettiController(duration: widget.duration);
if (widget.play) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_controller.play();
widget.onPlayed?.call();
});
}
}
@override
void didUpdateWidget(covariant ConfettiOverlay oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.play && !oldWidget.play) {
_controller.play();
if (widget.onPlayed != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onPlayed!();
});
}
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
widget.child,
Align(
alignment: Alignment.topCenter,
child: ConfettiWidget(
confettiController: _controller,
blastDirectionality: BlastDirectionality.explosive,
gravity: 0.1,
numberOfParticles: 10,
emissionFrequency: 0.2,
colors: widget.colors,
createParticlePath: (size) {
final random = Random();
final w = 10.0 + random.nextDouble() * 5.0;
final h = 5.0 + random.nextDouble() * 2.5;
return Path()..addRect(Rect.fromLTWH(0, 0, w, h));
},
),
),
],
);
}
}

View File

@@ -21,6 +21,7 @@ dependencies:
path: ../../packages/fonts
top_snackbar_flutter: ^3.3.0
lottie: ^3.3.1
confetti: ^0.7.0
dev_dependencies:
flutter_test:

View File

@@ -7,6 +7,6 @@
<versions>
<version>2.6.4</version>
</versions>
<lastUpdated>20260301000000</lastUpdated>
<lastUpdated>20260302000000</lastUpdated>
</versioning>
</metadata>

View File

@@ -1 +1 @@
ab3c3fa378ac166364ea7cfd739ddb6f
9728121fa548f85a30f9c665eaf069c7

View File

@@ -1 +1 @@
8a764ee335f810b5ecc8b687de8da58391118e7d
995b4bcedd19cee87123779a9acc8835b2eac2c6

View File

@@ -145,6 +145,12 @@
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "confetti",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/confetti-0.7.0",
"packageUri": "lib/",
"languageVersion": "2.17"
},
{
"name": "convert",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/convert-3.1.2",

View File

@@ -80,6 +80,7 @@
"name": "design_system",
"version": "0.0.1",
"dependencies": [
"confetti",
"country_code_picker",
"flutter",
"flutter_riverpod",
@@ -349,6 +350,14 @@
"dio"
]
},
{
"name": "confetti",
"version": "0.7.0",
"dependencies": [
"flutter",
"vector_math"
]
},
{
"name": "lottie",
"version": "3.3.2",

View File

@@ -193,6 +193,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
confetti:
dependency: transitive
description:
name: confetti
sha256: "979aafde2428c53947892c95eb244466c109c129b7eee9011f0a66caaca52267"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
convert:
dependency: transitive
description: