fix bridge ui (#3189)

* fix bridge ui * padding on info bottom sheet * fix cutoff on infobottomsheet * fix wording on bridge details ui * no _amountController.clear() on bridge page navigation * autofocus keyboard on bridge page opened * remove KeyboardHideOverlay * fix text field resize * fix text field padding

malik1004x committed Apr 17, 2026 at 23:46 UTC 1cb05d5211e1e96d4b98ff55d484889ca7a0b5dc
8 files changed +170 -174
lib/new-ui/pages/bridge/bridge_amount_page.dart
+111 -118
@@ -4,7 +4,6 @@ import 'package:cake_wallet/routes.dart';
4 import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
5 import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
6 import 'package:cake_wallet/utils/request_review_handler.dart';
7 -import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
7 import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
8 import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
9 import 'package:cake_wallet/view_model/bridge_history_view_model.dart';
@@ -33,7 +32,10 @@ class BridgeAmountPage extends StatefulWidget {
32 }
33
34 class _BridgeAmountPageState extends State<BridgeAmountPage> {
36 - late final TextEditingController _amountController;
35 + final TextEditingController _amountController = TextEditingController();
36 + final FocusNode _amountFocusNode = FocusNode();
37 + bool _amountFocused = false;
38 +
39 BridgeViewModel get bridgeViewModel => widget.bridgeViewModel;
40
41 @override
@@ -42,10 +44,11 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
44 bridgeViewModel.applyInitialBridgeToken(widget.initialToken);
45 bridgeViewModel.onBridgeSuccess = _showBridgeSuccessBottomSheet;
46
45 - _amountController = TextEditingController();
47 + _amountFocusNode.addListener(() => setState(() => _amountFocused = _amountFocusNode.hasFocus));
48
49 WidgetsBinding.instance.addPostFrameCallback((_) {
50 bridgeViewModel.ensureFiatPriceForSelectedToken();
51 + _amountFocusNode.requestFocus();
52 });
53 }
54
@@ -100,22 +103,23 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
103 Widget build(BuildContext context) {
104 final theme = Theme.of(context);
105
103 - return KeyboardHideOverlay(
104 - unfocusOnTap: true,
105 - child: Container(
106 - decoration: BoxDecoration(
107 - color: theme.colorScheme.surface,
108 - borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
109 - ),
106 + return Container(
107 + decoration: BoxDecoration(
108 + color: theme.colorScheme.surface,
109 + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
110 + ),
111 + child: Padding(
112 + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
113 child: Column(
114 children: [
115 ModalTopBar(
113 - title: 'Enter Amount',
114 - leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
116 + title: S.of(context).enter_amount,
117 + leadingIcon: Icon(Icons.arrow_back_ios_new),
118 onLeadingPressed: () => Navigator.of(context, rootNavigator: true).pop(),
116 - trailingIcon: const Icon(Icons.calendar_month, size: 18),
119 + trailingIcon: Icon(Icons.history),
120 onTrailingPressed: () {
118 - Navigator.pushNamed(context, Routes.bridgeHistoryPage, arguments: widget.bridgeHistoryViewModel);
121 + Navigator.pushNamed(context, Routes.bridgeHistoryPage,
122 + arguments: widget.bridgeHistoryViewModel);
123 },
124 ),
125 Expanded(
@@ -130,66 +134,63 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
134 crossAxisAlignment: CrossAxisAlignment.stretch,
135 children: [
136 const Spacer(flex: 2),
133 - LayoutBuilder(
134 - builder: (context, constraints) {
135 - return Center(
136 - child: Row(
137 - mainAxisSize: MainAxisSize.min,
138 - crossAxisAlignment: CrossAxisAlignment.baseline,
139 - textBaseline: TextBaseline.alphabetic,
140 - children: [
141 - ConstrainedBox(
142 - constraints: BoxConstraints(
143 - maxWidth: (constraints.maxWidth - 200)
144 - .clamp(40.0, constraints.maxWidth),
145 - minWidth: 40,
146 - ),
147 - child: TextFormField(
148 - controller: _amountController,
149 - maxLines: 1,
150 - onChanged: bridgeViewModel.setAmount,
151 - autovalidateMode: AutovalidateMode.always,
152 - validator: bridgeViewModel.decimalAmountValidator,
153 - keyboardType: TextInputType.numberWithOptions(
154 - signed: false,
155 - decimal: true,
156 - ),
157 - inputFormatters: <TextInputFormatter>[
158 - FilteringTextInputFormatter.allow(
159 - RegExp(r'^\d*[.,]?\d*$'),
160 - ),
161 - ],
162 - textAlign: TextAlign.center,
163 - decoration: InputDecoration(
164 - isDense: true,
165 - hintText: '0.00',
166 - hintStyle: theme.textTheme.displayMedium?.copyWith(
167 - fontWeight: FontWeight.w400,
168 - color: theme.colorScheme.onSurfaceVariant,
169 - ),
170 - ),
171 - style: theme.textTheme.displayMedium?.copyWith(
172 - fontWeight: FontWeight.w400,
173 - fontSize: 45,
174 - color: theme.colorScheme.onSurface,
175 - ),
176 - ),
137 + Center(
138 + child: Row(
139 + mainAxisSize: MainAxisSize.min,
140 + crossAxisAlignment: CrossAxisAlignment.baseline,
141 + textBaseline: TextBaseline.alphabetic,
142 + children: [
143 + IntrinsicWidth(
144 + child: TextFormField(
145 + controller: _amountController,
146 + focusNode: _amountFocusNode,
147 + maxLines: 1,
148 + onChanged: bridgeViewModel.setAmount,
149 + autovalidateMode: AutovalidateMode.always,
150 + validator: bridgeViewModel.decimalAmountValidator,
151 + keyboardType: TextInputType.numberWithOptions(
152 + signed: false,
153 + decimal: true,
154 ),
178 - const SizedBox(width: 8),
179 - Text(
180 - bridgeViewModel.selectedToken?.title ?? '',
181 - maxLines: 1,
182 - overflow: TextOverflow.ellipsis,
183 - style: theme.textTheme.displayMedium?.copyWith(
155 + inputFormatters: <TextInputFormatter>[
156 + FilteringTextInputFormatter.allow(
157 + RegExp(r'^\d*[.,]?\d*$'),
158 + ),
159 + ],
160 + decoration: InputDecoration(
161 + isDense: true,
162 + isCollapsed: true,
163 + contentPadding: EdgeInsets.zero,
164 + fillColor: Colors.transparent,
165 + hoverColor: Colors.transparent,
166 + focusedBorder: InputBorder.none,
167 + enabledBorder: InputBorder.none,
168 + hintText: _amountFocused || _amountController.text.isNotEmpty ? null : "0.00",
169 + hintStyle: theme.textTheme.displayMedium?.copyWith(
170 fontWeight: FontWeight.w400,
185 - fontSize: 45,
171 color: theme.colorScheme.onSurfaceVariant,
172 ),
173 ),
189 - ],
174 + style: theme.textTheme.displayMedium?.copyWith(
175 + fontWeight: FontWeight.w400,
176 + fontSize: 45,
177 + color: theme.colorScheme.onSurface,
178 + ),
179 + ),
180 + ),
181 + const SizedBox(width: 8),
182 + Text(
183 + bridgeViewModel.selectedToken?.title ?? '',
184 + maxLines: 1,
185 + overflow: TextOverflow.ellipsis,
186 + style: theme.textTheme.displayMedium?.copyWith(
187 + fontWeight: FontWeight.w400,
188 + fontSize: 45,
189 + color: theme.colorScheme.onSurfaceVariant,
190 + ),
191 ),
191 - );
192 - },
192 + ],
193 + ),
194 ),
195 const SizedBox(height: 12),
196 Center(
@@ -217,78 +218,71 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
218 ],
219 const Spacer(flex: 3),
220 Row(
221 + spacing: 8,
222 crossAxisAlignment: CrossAxisAlignment.center,
223 children: [
224 Expanded(
223 - child: RichText(
224 - maxLines: 2,
225 - overflow: TextOverflow.ellipsis,
226 - text: TextSpan(
227 - children: [
228 - TextSpan(
229 - text: 'Available ',
230 - style: theme.textTheme.bodyLarge?.copyWith(
231 - color: theme.colorScheme.onSurfaceVariant,
232 - fontWeight: FontWeight.w400,
233 - fontSize: 16,
234 - letterSpacing: -0.08,
235 - ),
236 - ),
237 - TextSpan(
238 - text: '${bridgeViewModel.tokenBalanceFormatted} '
239 - '${bridgeViewModel.selectedToken?.title ?? ''}',
240 - style: theme.textTheme.bodyLarge?.copyWith(
241 - color: theme.colorScheme.onSurface,
242 - fontWeight: FontWeight.w400,
243 - fontSize: 16,
244 - letterSpacing: -0.08,
245 - ),
246 - ),
247 - ],
248 - ),
249 - ),
250 - ),
251 - const SizedBox(width: 10),
252 - TextButton(
253 - style: TextButton.styleFrom(
254 - textStyle: theme.textTheme.bodyMedium?.copyWith(
255 - fontWeight: FontWeight.w400,
256 - fontSize: 14,
257 - letterSpacing: -0.07,
225 + child: Row(
226 + spacing: 8,
227 + children: [
228 + Text(
229 + S.of(context).available,
230 + style: TextStyle(
231 + fontSize: 16,
232 + color: Theme.of(context).colorScheme.onSurfaceVariant),
233 ),
259 - foregroundColor: theme.colorScheme.primary,
260 - backgroundColor: theme.colorScheme.surfaceContainer,
261 - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
262 - shape: RoundedRectangleBorder(
263 - borderRadius: BorderRadius.circular(80),
264 - ),
265 - ),
266 - onPressed: () {
234 + Text(
235 + "${bridgeViewModel.tokenBalanceFormatted} ${bridgeViewModel.selectedToken?.title ?? ""}",
236 + style: TextStyle(
237 + fontSize: 16,
238 + color: Theme.of(context).colorScheme.onSurface),
239 + )
240 + ],
241 + )),
242 + GestureDetector(
243 + onTap: () {
244 bridgeViewModel.setMaxAmount();
245 _amountController.text = bridgeViewModel.amount;
246 _amountController.selection = TextSelection.collapsed(
247 offset: _amountController.text.length,
248 );
249 },
273 - child: Text(S.of(context).max),
250 + child: Container(
251 + height: 34,
252 + decoration: BoxDecoration(
253 + color: Theme.of(context).colorScheme.surfaceContainer,
254 + borderRadius: BorderRadius.circular(999999)),
255 + child: Center(
256 + child: Padding(
257 + padding: EdgeInsets.symmetric(horizontal: 16),
258 + child: Text(
259 + S.of(context).max,
260 + style: TextStyle(
261 + fontSize: 14,
262 + color: Theme.of(context).colorScheme.primary),
263 + ),
264 + )),
265 + ),
266 ),
275 - const SizedBox(width: 10),
267 IgnorePointer(
268 ignoring: !canNext,
269 child: ModernButton(
279 - size: 48,
270 + size: 34,
271 backgroundColor: theme.colorScheme.primary,
272 iconColor: theme.colorScheme.onPrimary,
282 - icon: Icon(Icons.arrow_forward, size: 25),
273 + icon: Icon(Icons.arrow_forward, size: 20),
274 onPressed: () {
284 - _amountController.clear();
275 Navigator.pushNamed(
286 - context, Routes.bridgeDestinationNetworkPage, arguments: bridgeViewModel);
276 + context, Routes.bridgeDestinationNetworkPage,
277 + arguments: bridgeViewModel);
278 },
279 ),
280 ),
281 ],
282 ),
283 + SizedBox(
284 + height: 24,
285 + )
286 ],
287 );
288 },
@@ -302,4 +296,3 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
296 );
297 }
298 }
305 -
lib/new-ui/pages/bridge/bridge_confirm_sheet.dart
+19 -19
@@ -6,6 +6,7 @@ import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
6 import 'package:cake_wallet/new-ui/widgets/send_page/send_confirm_bottom_widget.dart';
7 import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
8 import 'package:cake_wallet/view_model/bridge/bridge_view_model.dart';
9 +import 'package:flutter/cupertino.dart';
10 import 'package:flutter/material.dart';
11 import 'package:flutter_mobx/flutter_mobx.dart';
12 import 'package:mobx/mobx.dart';
@@ -98,7 +99,7 @@ class _BridgeConfirmSheetState extends State<BridgeConfirmSheet> {
99 return const Padding(
100 padding: EdgeInsets.symmetric(vertical: 48),
101 child: Center(
101 - child: CircularProgressIndicator(),
102 + child: CupertinoActivityIndicator(),
103 ),
104 );
105 }
@@ -176,9 +177,14 @@ class _BridgeConfirmSheetState extends State<BridgeConfirmSheet> {
177 ),
178 ],
179 ),
179 - NetworkPathPill(
180 - sourceChainName: bridgeViewModel.wallet.type.name,
181 - destChainName: bridgeViewModel.destinationChainInfo?.name ?? '',
180 + Row(
181 + mainAxisAlignment: MainAxisAlignment.center,
182 + children: [
183 + NetworkPathPill(
184 + sourceChainName: bridgeViewModel.wallet.type.name,
185 + destChainName: bridgeViewModel.destinationChainInfo?.name ?? '',
186 + ),
187 + ],
188 ),
189 ConfirmDetailsCard(bridgeViewModel: bridgeViewModel),
190 if (bridgeViewModel.executeError != null) ...[
@@ -199,22 +205,16 @@ class _BridgeConfirmSheetState extends State<BridgeConfirmSheet> {
205 child: Observer(
206 builder: (_) {
207 if (bridgeViewModel.isExecuting) {
202 - return const LoadingBottomWidget(
203 - text: 'Bridging...',
204 - );
208 + return LoadingBottomWidget(text: "${S.of(context).bridging}...",);
209 }
206 - final canSwipe = bridgeViewModel.quote != null && !bridgeViewModel.isQuoteLoading;
207 - return IgnorePointer(
208 - ignoring: !canSwipe,
209 - child: Opacity(
210 - opacity: canSwipe ? 1 : 0.45,
211 - child: ConfirmSwiper(
212 - onConfirmed: () {
213 - bridgeViewModel.executeBridge();
214 - },
215 - swiperText: 'Swipe to bridge',
216 - ),
217 - ),
210 + if(bridgeViewModel.isQuoteLoading || bridgeViewModel.quote == null) {
211 + return LoadingBottomWidget(text: "${S.of(context).loading}...");
212 + }
213 + return ConfirmSwiper(
214 + onConfirmed: () {
215 + bridgeViewModel.executeBridge();
216 + },
217 + swiperText: S.of(context).swipe_to_bridge,
218 );
219 },
220 ),
lib/new-ui/widgets/bridge/network_path_pill.dart
+20 -24
@@ -28,18 +28,16 @@ class NetworkPathPill extends StatelessWidget {
28 color: scheme.onSurfaceVariant,
29 ),
30 const SizedBox(width: 8),
31 - Flexible(
32 - child: Text(
33 - sourceChainName.capitalized(),
34 - maxLines: 1,
35 - overflow: TextOverflow.ellipsis,
36 - style: Theme.of(context).textTheme.bodyLarge?.copyWith(
37 - color: scheme.onSurface,
38 - fontWeight: FontWeight.w400,
39 - fontSize: 16,
40 - letterSpacing: -0.08,
41 - ),
42 - ),
31 + Text(
32 + sourceChainName.capitalized(),
33 + maxLines: 1,
34 + overflow: TextOverflow.ellipsis,
35 + style: Theme.of(context).textTheme.bodyLarge?.copyWith(
36 + color: scheme.onSurface,
37 + fontWeight: FontWeight.w400,
38 + fontSize: 16,
39 + letterSpacing: -0.08,
40 + ),
41 ),
42 Padding(
43 padding: const EdgeInsets.symmetric(horizontal: 8),
@@ -56,18 +54,16 @@ class NetworkPathPill extends StatelessWidget {
54 color: scheme.onSurfaceVariant,
55 ),
56 const SizedBox(width: 8),
59 - Flexible(
60 - child: Text(
61 - destChainName,
62 - maxLines: 1,
63 - overflow: TextOverflow.ellipsis,
64 - style: Theme.of(context).textTheme.bodyLarge?.copyWith(
65 - color: scheme.onSurface,
66 - fontWeight: FontWeight.w400,
67 - fontSize: 16,
68 - letterSpacing: -0.08,
69 - ),
70 - ),
57 + Text(
58 + destChainName,
59 + maxLines: 1,
60 + overflow: TextOverflow.ellipsis,
61 + style: Theme.of(context).textTheme.bodyLarge?.copyWith(
62 + color: scheme.onSurface,
63 + fontWeight: FontWeight.w400,
64 + fontSize: 16,
65 + letterSpacing: -0.08,
66 + ),
67 ),
68 ],
69 );
lib/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart
+12 -10
@@ -55,13 +55,15 @@ abstract class BaseBottomSheet extends StatelessWidget {
55 topLeft: Radius.circular(30.0), topRight: Radius.circular(30.0)),
56 child: Container(
57 color: Theme.of(context).colorScheme.surface,
58 - child: Column(
59 - mainAxisSize: MainAxisSize.min,
60 - children: <Widget>[
61 - _buildHeader(context),
62 - contentWidget(context),
63 - _buildFooter(context),
64 - ],
58 + child: SafeArea(
59 + child: Column(
60 + mainAxisSize: MainAxisSize.min,
61 + children: <Widget>[
62 + _buildHeader(context),
63 + contentWidget(context),
64 + _buildFooter(context),
65 + ],
66 + ),
67 ),
68 ),
69 ),
@@ -108,7 +110,7 @@ abstract class BaseBottomSheet extends StatelessWidget {
110
111 case FooterType.slideActionButton:
112 return Padding(
111 - padding: const EdgeInsets.fromLTRB(40, 12, 40, 34),
113 + padding: const EdgeInsets.fromLTRB(40, 12, 40, 24),
114 child: StandardSlideButton(
115 key: ValueKey('base_bottom_sheet_widget_standard_slide_button_key'),
116 buttonText: slideActionButtonText ?? '',
@@ -120,7 +122,7 @@ abstract class BaseBottomSheet extends StatelessWidget {
122
123 case FooterType.singleActionButton:
124 return Padding(
123 - padding: const EdgeInsets.fromLTRB(16, 12, 16, 34),
125 + padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
126 child: LoadingPrimaryButton(
127 key: singleActionButtonKey,
128 text: singleActionButtonText ?? '',
@@ -134,7 +136,7 @@ abstract class BaseBottomSheet extends StatelessWidget {
136
137 case FooterType.doubleActionButton:
138 return Padding(
137 - padding: const EdgeInsets.fromLTRB(16, 0, 16, 34),
139 + padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
140 child: Row(
141 children: [
142 Expanded(
lib/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart
+1
@@ -100,6 +100,7 @@ class InfoBottomSheet extends BaseBottomSheet {
100 )
101 else
102 Container(),
103 + SizedBox(height: 24,),
104 if (content != null)
105 Expanded(
106 flex: 2,
lib/view_model/bridge/bridge_view_model.dart
+2 -1
@@ -17,6 +17,7 @@ import 'package:cake_wallet/store/app_store.dart';
17 import 'package:cake_wallet/store/bridge_transfers_store.dart';
18 import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
19 import 'package:cake_wallet/store/settings_store.dart';
20 +import 'package:cw_core/crypto_amount_format.dart';
21 import 'package:cw_core/crypto_currency.dart';
22 import 'package:cw_core/erc20_token.dart';
23 import 'package:cw_core/wallet_base.dart';
@@ -187,7 +188,7 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
188 String get quoteNativeFeeFormattedForDisplay {
189 if (quoteNativeFee.isEmpty) return '';
190
190 - return '${quoteNativeFee} ${wallet.currency.title}';
191 + return '${quoteNativeFee.withMaxDecimals(8)} ${wallet.currency.title}';
192 }
193
194 @computed
lib/view_model/bridge_details_view_model.dart
+2 -2
@@ -97,9 +97,9 @@ abstract class BridgeDetailsViewModelBase with Store {
97 );
98
99 final sourceName =
100 - evm?.getChainNameByChainId(transfer.sourceChainId) ?? '${transfer.sourceChainId}';
100 + evm?.getChainInfoByChainId(transfer.sourceChainId)?.name ?? '${transfer.sourceChainId}';
101 final destName =
102 - evm?.getChainNameByChainId(transfer.destinationChainId) ?? '${transfer.destinationChainId}';
102 + evm?.getChainInfoByChainId(transfer.destinationChainId)?.name ?? '${transfer.destinationChainId}';
103
104 items.add(
105 StandartListItem(
res/values/strings_en.arb
+3
@@ -87,6 +87,7 @@
87 "auto_generate_addresses": "Auto generate addresses",
88 "auto_generate_subaddresses": "Auto generate subaddresses",
89 "automatic": "Automatic",
90 + "available": "Available",
91 "available_balance": "Available Balance",
92 "available_balance_description": "The “Available Balance” or “Confirmed Balance” are funds that can be spent immediately. If funds appear in the lower balance but not the top balance, then you must wait a few minutes for the incoming funds to get more network confirmations. After they get more confirmations, they will be spendable.",
93 "avg_savings": "Avg. Savings",
@@ -123,6 +124,7 @@
124 "block_remaining": "1 Block Remaining",
125 "Blocks_remaining": "${status} Blocks Remaining",
126 "bluetooth": "Bluetooth",
127 + "bridging": "Bridging",
128 "bright_theme": "Bright",
129 "bscscan_history": "BSCScan History",
130 "bump_fee": "Bump fee",
@@ -1091,6 +1093,7 @@
1093 "swap_providers": "Swap providers",
1094 "sweeping_wallet": "Sweeping wallet",
1095 "sweeping_wallet_alert": "This shouldn’t take long. DO NOT LEAVE THIS SCREEN OR THE SWEPT FUNDS MAY BE LOST.",
1096 + "swipe_to_bridge": "Swipe to bridge",
1097 "swipe_to_send": "Swipe to send",
1098 "switch_wallet": "Switch Wallet",
1099 "switchToETHWallet": "Please switch to an Ethereum wallet and try again",