diff --git a/assets/images/hash_bag_base.png b/assets/images/hash_bag_base.png new file mode 100644 index 000000000..d8f586860 Binary files /dev/null and b/assets/images/hash_bag_base.png differ diff --git a/assets/images/hashbag_sym_btc.png b/assets/images/hashbag_sym_btc.png new file mode 100644 index 000000000..40eed0f79 Binary files /dev/null and b/assets/images/hashbag_sym_btc.png differ diff --git a/assets/images/hashbag_sym_dollar.png b/assets/images/hashbag_sym_dollar.png new file mode 100644 index 000000000..70ff3d00d Binary files /dev/null and b/assets/images/hashbag_sym_dollar.png differ diff --git a/assets/images/hashbag_sym_hash.png b/assets/images/hashbag_sym_hash.png new file mode 100644 index 000000000..1591fed09 Binary files /dev/null and b/assets/images/hashbag_sym_hash.png differ diff --git a/lib/src/screens/Info_page.dart b/lib/src/screens/Info_page.dart index f6f0e9941..d148247f6 100644 --- a/lib/src/screens/Info_page.dart +++ b/lib/src/screens/Info_page.dart @@ -21,6 +21,12 @@ abstract class InfoPage extends BasePage { Key? get buttonKey; void Function(BuildContext) get onPressed; + /// The illustration shown at the top of the page. Defaults to the static + /// image at [imageLightPath]/[imageDarkPath]; subclasses can override to + /// supply an animated widget instead. + Widget buildIllustration(BuildContext context, String image) => + HashImageWidget(imageUrl: image); + @override bool get gradientBackground => true; @@ -56,7 +62,7 @@ abstract class InfoPage extends BasePage { ), child: AspectRatio( aspectRatio: 0.9, - child: HashImageWidget(imageUrl: image), + child: buildIllustration(context, image), ), ), ), diff --git a/lib/src/screens/send/transaction_success_info_page.dart b/lib/src/screens/send/transaction_success_info_page.dart index e057b7f01..f3f0123e0 100644 --- a/lib/src/screens/send/transaction_success_info_page.dart +++ b/lib/src/screens/send/transaction_success_info_page.dart @@ -1,5 +1,6 @@ import 'package:hash_wallet/generated/i18n.dart'; import 'package:hash_wallet/src/screens/Info_page.dart'; +import 'package:hash_wallet/src/widgets/hash_bag_success_animation.dart'; import 'package:flutter/cupertino.dart'; class TransactionSuccessPage extends InfoPage { @@ -11,6 +12,10 @@ class TransactionSuccessPage extends InfoPage { final String content; + @override + Widget buildIllustration(BuildContext context, String image) => + const HashBagSuccessAnimation(); + @override bool get onWillPop => false; diff --git a/lib/src/widgets/hash_bag_success_animation.dart b/lib/src/widgets/hash_bag_success_animation.dart new file mode 100644 index 000000000..f7f8d08bd --- /dev/null +++ b/lib/src/widgets/hash_bag_success_animation.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; + +/// "Transaction sent" celebration: the Hash Bags money sack sits at the bottom +/// and money glyphs (#, $, ₿) erupt from its mouth, rising and fading on a loop. +/// +/// The glyphs are image assets (not text) because the app doesn't bundle a font +/// containing a bitcoin glyph — rendering them as PNGs keeps them consistent +/// across platforms. +class HashBagSuccessAnimation extends StatefulWidget { + const HashBagSuccessAnimation({super.key}); + + @override + State createState() => _HashBagSuccessAnimationState(); +} + +class _Particle { + final String asset; + final double dx; // horizontal fan, fraction of width + final double phase; // stagger, 0..1 + final double size; // fraction of width + const _Particle(this.asset, this.dx, this.phase, this.size); +} + +class _HashBagSuccessAnimationState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + + static const String _hash = 'assets/images/hashbag_sym_hash.png'; + static const String _dollar = 'assets/images/hashbag_sym_dollar.png'; + static const String _btc = 'assets/images/hashbag_sym_btc.png'; + + static const List<_Particle> _particles = <_Particle>[ + _Particle(_btc, 0.00, 0.00, 0.20), + _Particle(_hash, -0.18, 0.13, 0.16), + _Particle(_dollar, 0.19, 0.24, 0.16), + _Particle(_hash, 0.11, 0.40, 0.12), + _Particle(_dollar, -0.13, 0.54, 0.11), + _Particle(_btc, 0.06, 0.68, 0.12), + _Particle(_hash, -0.07, 0.83, 0.10), + ]; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 2800), + )..repeat(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final double w = constraints.maxWidth; + final double h = constraints.maxHeight; + // Sack anchored at the bottom (~62% of the height); mouth ≈ its top. + final double bagH = h * 0.62; + final double bagW = bagH * 0.825; // hash_bag_base.png aspect (w/h) + final double bagLeft = (w - bagW) / 2; + final double bagTop = h - bagH; + final double mouthX = w / 2; + final double mouthY = bagTop + bagH * 0.06; + + return AnimatedBuilder( + animation: _controller, + builder: (BuildContext context, Widget? _) { + final List children = [ + Positioned( + left: bagLeft, + top: bagTop, + width: bagW, + height: bagH, + child: Image.asset( + 'assets/images/hash_bag_base.png', + fit: BoxFit.contain, + ), + ), + ]; + + for (final _Particle p in _particles) { + final double t = (_controller.value + p.phase) % 1.0; + final double sz = w * p.size; + final double rise = h * 0.52 * t; + final double spread = w * p.dx * (0.35 + t); + final double x = mouthX + spread - sz / 2; + final double y = mouthY - rise - sz / 2; + final double opacity = (t < 0.15 + ? t / 0.15 + : (t > 0.78 ? (1.0 - t) / 0.22 : 1.0)) + .clamp(0.0, 1.0) + .toDouble(); + final double scale = 0.55 + 0.45 * (t < 0.3 ? t / 0.3 : 1.0); + + children.add( + Positioned( + left: x, + top: y, + width: sz, + height: sz, + child: Opacity( + opacity: opacity, + child: Transform.scale( + scale: scale, + child: Image.asset(p.asset, fit: BoxFit.contain), + ), + ), + ), + ); + } + + return Stack(clipBehavior: Clip.none, children: children); + }, + ); + }, + ); + } +}