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",