Files
jwinterm 7e368f92e6 brand: de-cake the app (hash-bag success art, asset + widget renames, filter, icon padding)
Send-success art: replace the Cake birthday cake with a Hash Bags money sack
that has a # on it and erupts #/$/₿ (assets/images/hash_bag.png). Used by the
success page, send-confirm sheet, exchange-trade, and dEURO savings.

Asset renames (files + refs):
- birthday_cake.png            -> hash_bag.png
- restore_cupcake(.png/_dark)  -> restore_hashbag.png
- qr-cake.png                  -> qr-hashbag.png
- arrow_bottom_cake_green.png  -> arrow_bottom_green.png (+ 2.0x/3.0x)

Identifier renames:
- CakeImageWidget -> HashImageWidget (widget + file + 93 refs)
- typography _cakeRegular/_cakeBold/_cakeSemiBold -> _hash*

Transaction filter: drop the Cake Pay "Orders" row and every non-Trocador
trade provider; the swap gate is already Trocador-only, so this just aligns the
filter UI with reality.

Android launcher: add breathing room to the adaptive-foreground # (all
densities, monochrome included); iOS icon unchanged.

About page: swap the Cake easter eggs for Hash Bags ones.

Deferred (needs a build to verify): new-ui .svg.vec filename renames
(cake_squircle_icon, cakewallet-wordmark, cake-setting) — already Hash Bags in
content, only the filenames are cake, and they route through the .vec precompile.
2026-08-07 02:57:29 -04:00

302 lines
14 KiB
Dart

import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/new-ui/pages/bridge/bridge_history_page.dart';
import 'package:hash_wallet/routes.dart';
import 'package:hash_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
import 'package:hash_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
import 'package:hash_wallet/utils/request_review_handler.dart';
import 'package:hash_wallet/new-ui/widgets/modern_button.dart';
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:hash_wallet/view_model/bridge/bridge_history_view_model.dart';
import 'package:hash_wallet/view_model/bridge/bridge_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
class BridgeAmountPage extends StatefulWidget {
const BridgeAmountPage({
super.key,
required this.bridgeViewModel,
required this.bridgeHistoryViewModel,
required this.initialToken,
});
final BridgeViewModel bridgeViewModel;
final BridgeHistoryViewModel bridgeHistoryViewModel;
final CryptoCurrency initialToken;
@override
State<BridgeAmountPage> createState() => _BridgeAmountPageState();
}
class _BridgeAmountPageState extends State<BridgeAmountPage> {
final TextEditingController _amountController = TextEditingController();
final FocusNode _amountFocusNode = FocusNode();
bool _amountFocused = false;
BridgeViewModel get bridgeViewModel => widget.bridgeViewModel;
@override
void initState() {
super.initState();
bridgeViewModel.applyInitialBridgeToken(widget.initialToken);
bridgeViewModel.onBridgeSuccess = _showBridgeSuccessBottomSheet;
_amountFocusNode.addListener(() => setState(() => _amountFocused = _amountFocusNode.hasFocus));
WidgetsBinding.instance.addPostFrameCallback((_) {
bridgeViewModel.ensureFiatPriceForSelectedToken();
_amountFocusNode.requestFocus();
});
}
void _showBridgeSuccessBottomSheet() {
if (!mounted) return;
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!mounted) return;
final ctx = context;
if (!ctx.mounted) return;
await showModalBottomSheet<void>(
context: ctx,
isScrollControlled: true,
builder: (BuildContext bottomSheetContext) {
return InfoBottomSheet(
footerType: FooterType.doubleActionButton,
titleText: 'Bridge initiated!',
contentImage: 'assets/images/hash_bag.png',
content: 'The bridging will take between 30 seconds and 3 '
'minutes to complete.',
doubleActionLeftButtonText: S.of(bottomSheetContext).close,
doubleActionRightButtonText: 'View history',
onLeftActionButtonPressed: () {
bridgeViewModel.clearOnBridgeSuccess();
Navigator.of(context, rootNavigator: true).pop();
RequestReviewHandler.requestReview();
},
onRightActionButtonPressed: () {
bridgeViewModel.clearOnBridgeSuccess();
Navigator.of(context).popUntil((route) => route.isFirst);
showMaterialModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (ctx) => BridgeHistoryPage(widget.bridgeHistoryViewModel),
);
},
);
},
);
});
}
@override
void dispose() {
bridgeViewModel.onBridgeSuccess = null;
bridgeViewModel.setAmount('');
_amountController.dispose();
bridgeViewModel.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
),
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Column(
children: [
ModalTopBar(
title: S.of(context).enter_amount,
leadingIcon: Icon(Icons.arrow_back_ios_new),
onLeadingPressed: () => Navigator.of(context, rootNavigator: true).pop(),
trailingIcon: Icon(Icons.history),
onTrailingPressed: () {
Navigator.pushNamed(context, Routes.bridgeHistoryPage,
arguments: widget.bridgeHistoryViewModel);
},
),
Expanded(
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Observer(
builder: (_) {
final canNext = bridgeViewModel.canProceedToDestinationNetwork;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Spacer(flex: 2),
Center(
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
IntrinsicWidth(
child: TextFormField(
controller: _amountController,
focusNode: _amountFocusNode,
maxLines: 1,
onChanged: bridgeViewModel.setAmount,
autovalidateMode: AutovalidateMode.always,
validator: bridgeViewModel.decimalAmountValidator,
keyboardType: TextInputType.numberWithOptions(
signed: false,
decimal: true,
),
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(
RegExp(r'^\d*[.,]?\d*$'),
),
],
decoration: InputDecoration(
isDense: true,
isCollapsed: true,
contentPadding: EdgeInsets.zero,
fillColor: Colors.transparent,
hoverColor: Colors.transparent,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
hintText: _amountFocused || _amountController.text.isNotEmpty
? null
: "0.00",
hintStyle: theme.textTheme.displayMedium?.copyWith(
fontWeight: FontWeight.w400,
color: theme.colorScheme.onSurfaceVariant,
),
),
style: theme.textTheme.displayMedium?.copyWith(
fontWeight: FontWeight.w400,
fontSize: 45,
color: theme.colorScheme.onSurface,
),
),
),
const SizedBox(width: 8),
Text(
bridgeViewModel.selectedToken?.title ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.displayMedium?.copyWith(
fontWeight: FontWeight.w400,
fontSize: 45,
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
const SizedBox(height: 12),
Center(
child: Text(
bridgeViewModel.fiatAmountFormatted.isEmpty
? ''
: '~${bridgeViewModel.fiatAmountFormatted} ${bridgeViewModel.fiatCurrencyTitle}',
style: theme.textTheme.headlineSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
fontSize: 22,
letterSpacing: -0.11,
),
),
),
if (bridgeViewModel.amountError != null) ...[
const SizedBox(height: 10),
Text(
bridgeViewModel.amountError!,
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.error,
),
),
],
const Spacer(flex: 3),
Row(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Row(
spacing: 8,
children: [
Text(
S.of(context).available,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
Text(
"${bridgeViewModel.tokenBalanceFormatted} ${bridgeViewModel.selectedToken?.title ?? ""}",
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface),
)
],
)),
GestureDetector(
onTap: () {
bridgeViewModel.setMaxAmount();
_amountController.text = bridgeViewModel.amount;
_amountController.selection = TextSelection.collapsed(
offset: _amountController.text.length,
);
},
child: Container(
height: 34,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(999999)),
child: Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
S.of(context).max,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.primary),
),
)),
),
),
IgnorePointer(
ignoring: !canNext,
child: ModernButton(
size: 34,
backgroundColor: theme.colorScheme.primary,
iconColor: theme.colorScheme.onPrimary,
icon: Icon(Icons.arrow_forward, size: 20),
onPressed: () {
Navigator.pushNamed(
context,
Routes.bridgeDestinationNetworkPage,
arguments: bridgeViewModel,
);
},
),
),
],
),
SizedBox(height: 24)
],
);
},
),
),
),
),
],
),
),
);
}
}