Skip to content

Commit 02d341a

Browse files
authored
Merge pull request #2682 from SatoshiPortal/fix/home-hidden-wallet-card
fix(wallet): make every wallet card reachable on home
2 parents cc0bf65 + fa6e9aa commit 02d341a

5 files changed

Lines changed: 139 additions & 14 deletions

File tree

lib/core/widgets/bb_pullable_body.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class BBPullableBody extends StatelessWidget {
2424
required this.onRefresh,
2525
required this.slivers,
2626
this.bottomChild,
27+
this.bottomInset = 0,
2728
});
2829

2930
/// Forwarded to the inner [BBRefreshIndicator]. Use a
@@ -33,6 +34,9 @@ class BBPullableBody extends StatelessWidget {
3334
final List<Widget> slivers;
3435
final Widget? bottomChild;
3536

37+
/// Space reserved at the end of the scroll content.
38+
final double bottomInset;
39+
3640
@override
3741
Widget build(BuildContext context) {
3842
return BBRefreshIndicator(
@@ -42,6 +46,8 @@ class BBPullableBody extends StatelessWidget {
4246
physics: const AlwaysScrollableScrollPhysics(),
4347
slivers: [
4448
...slivers,
49+
if (bottomInset > 0)
50+
SliverToBoxAdapter(child: SizedBox(height: bottomInset)),
4551
SliverFillRemaining(
4652
hasScrollBody: false,
4753
child: bottomChild == null

lib/features/wallet/ui/screens/wallet_home_screen.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ class WalletHomeScreen extends StatefulWidget {
2020
}
2121

2222
class _WalletHomeScreenState extends State<WalletHomeScreen> {
23+
/// Height the pinned Receive/Send bar occupies
24+
static const double _bottomBarHeight = 52.0 + 16.0 * 2;
25+
2326
final GlobalKey<RefreshIndicatorState> _indicatorKey =
2427
GlobalKey<RefreshIndicatorState>();
2528

@@ -116,8 +119,15 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
116119
BBPullableBody(
117120
indicatorKey: _indicatorKey,
118121
onRefresh: () => context.read<WalletBloc>().refresh(),
122+
// Clearance for the bar pinned at the bottom of this Stack, so
123+
// the last wallet card can be scrolled out from under it.
124+
bottomInset:
125+
_bottomBarHeight + MediaQuery.paddingOf(context).bottom,
119126
slivers: [
120-
const SliverToBoxAdapter(child: WalletHomeTopSection()),
127+
// Pinned rather than scrolled away: the balance and the
128+
// Buy/Sell/Pay/Transfer actions stay put while the wallet cards
129+
// scroll underneath.
130+
const PinnedHeaderSliver(child: WalletHomeTopSection()),
121131
const SliverToBoxAdapter(child: AnnouncementCarousel()),
122132
const SliverToBoxAdapter(child: HomeWarnings()),
123133
const SliverToBoxAdapter(child: HomeConsolidationBanner()),

lib/features/wallet/ui/widgets/wallet_cards.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ class WalletCards extends StatelessWidget {
5252
child: Column(
5353
crossAxisAlignment: .stretch,
5454
children: [
55-
for (final w in wallets) ...[
55+
for (final (index, w) in wallets.indexed) ...[
56+
if (index > 0) const Gap(8),
5657
WalletCard(
5758
tagColor: cardDetails(context, w),
5859
title: w.displayLabel(context),
@@ -62,7 +63,6 @@ class WalletCards extends StatelessWidget {
6263
fiatCurrency: fiatCurrency,
6364
onTap: () => onTap?.call(w),
6465
),
65-
const Gap(8),
6666
],
6767
],
6868
),

lib/features/wallet/ui/widgets/wallet_home_top_section.dart

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,28 @@ import 'package:shimmer/shimmer.dart';
1919
class WalletHomeTopSection extends StatelessWidget {
2020
const WalletHomeTopSection({super.key});
2121

22+
/// Height of the dark area behind the balance (and the price chart, when it
23+
/// is toggled on).
24+
static const double _balanceAreaHeight = 264;
25+
26+
/// Distance from the top of the dark area down to the balance.
27+
static const double _balanceTopSpacing = 123;
28+
29+
/// How far the [ActionCard] hangs below the dark area.
30+
static const double _actionCardOverhang = 76;
31+
32+
/// Fixed height of the section.
33+
static const double _height = _balanceAreaHeight + _actionCardOverhang;
34+
2235
@override
2336
Widget build(BuildContext context) {
2437
return const SizedBox(
25-
height: 264 + 78 + 46,
38+
height: _height,
2639
child: Stack(
2740
children: [
2841
Column(
2942
crossAxisAlignment: CrossAxisAlignment.stretch,
30-
children: [
31-
SizedBox(
32-
height: 264 + 78,
33-
// color: Colors.red,
34-
child: _UI(),
35-
),
36-
// const Gap(40),
37-
],
43+
children: [SizedBox(height: _balanceAreaHeight, child: _UI())],
3844
),
3945
Positioned(
4046
bottom: 0,
@@ -111,9 +117,8 @@ class _Amounts extends StatelessWidget {
111117
@override
112118
Widget build(BuildContext context) {
113119
return const Column(
114-
mainAxisAlignment: MainAxisAlignment.center,
115120
children: [
116-
Gap(32),
121+
Gap(WalletHomeTopSection._balanceTopSpacing),
117122
Row(
118123
mainAxisAlignment: MainAxisAlignment.center,
119124
children: [Spacer(), _BtcTotalAmt(), Gap(16), EyeToggle(), Spacer()],
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import 'package:bb_mobile/core/widgets/bb_pullable_body.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter_test/flutter_test.dart';
4+
5+
void main() {
6+
const lastItemKey = Key('last-item');
7+
const lastItemHeight = 40.0;
8+
// Comfortably taller than the 600dp test viewport, so the content overflows
9+
// and the scroll extent is what decides whether the last item is reachable.
10+
const contentHeight = 1200.0;
11+
12+
Future<void> pumpBody(
13+
WidgetTester tester, {
14+
required double bottomInset,
15+
}) async {
16+
await tester.pumpWidget(
17+
MaterialApp(
18+
home: Scaffold(
19+
body: BBPullableBody(
20+
onRefresh: () async {},
21+
bottomInset: bottomInset,
22+
slivers: const [
23+
SliverToBoxAdapter(
24+
child: SizedBox(height: contentHeight - lastItemHeight),
25+
),
26+
SliverToBoxAdapter(
27+
child: SizedBox(key: lastItemKey, height: lastItemHeight),
28+
),
29+
],
30+
),
31+
),
32+
),
33+
);
34+
}
35+
36+
/// Distance from the bottom of the last item to the bottom of the viewport,
37+
/// once scrolled as far as the body allows. Zero means the item is flush with
38+
/// the viewport edge — where a pinned footer would cover it.
39+
Future<double> clearanceAtEndOfScroll(WidgetTester tester) async {
40+
await tester.drag(find.byType(CustomScrollView), const Offset(0, -2000));
41+
await tester.pumpAndSettle();
42+
43+
final viewportBottom = tester.getRect(find.byType(CustomScrollView)).bottom;
44+
final lastItemBottom = tester.getRect(find.byKey(lastItemKey)).bottom;
45+
46+
return viewportBottom - lastItemBottom;
47+
}
48+
49+
testWidgets('leaves the last item flush with the viewport by default', (
50+
tester,
51+
) async {
52+
await pumpBody(tester, bottomInset: 0);
53+
54+
expect(await clearanceAtEndOfScroll(tester), moreOrLessEquals(0));
55+
});
56+
57+
testWidgets('scrolls the last item clear of the reserved bottom inset', (
58+
tester,
59+
) async {
60+
await pumpBody(tester, bottomInset: 84);
61+
62+
// Without the reservation this is 0 and a footer pinned over the body hides
63+
// the last item for good — the wallet home regression this guards against.
64+
expect(await clearanceAtEndOfScroll(tester), moreOrLessEquals(84));
65+
});
66+
67+
testWidgets('adds the bottom inset to the scrollable extent', (tester) async {
68+
await pumpBody(tester, bottomInset: 84);
69+
final withInset = tester
70+
.state<ScrollableState>(find.byType(Scrollable))
71+
.position
72+
.maxScrollExtent;
73+
74+
await pumpBody(tester, bottomInset: 0);
75+
final withoutInset = tester
76+
.state<ScrollableState>(find.byType(Scrollable))
77+
.position
78+
.maxScrollExtent;
79+
80+
expect(withInset - withoutInset, moreOrLessEquals(84));
81+
});
82+
83+
testWidgets('keeps short content unscrollable when an inset is reserved', (
84+
tester,
85+
) async {
86+
await tester.pumpWidget(
87+
MaterialApp(
88+
home: Scaffold(
89+
body: BBPullableBody(
90+
onRefresh: () async {},
91+
bottomInset: 84,
92+
slivers: const [SliverToBoxAdapter(child: SizedBox(height: 100))],
93+
),
94+
),
95+
),
96+
);
97+
98+
final position = tester
99+
.state<ScrollableState>(find.byType(Scrollable))
100+
.position;
101+
102+
expect(position.maxScrollExtent, 0);
103+
});
104+
}

0 commit comments

Comments
 (0)