From 4fce8f9a57ef822eb6f57e10fff2f6cacb67a69e Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Fri, 30 Jan 2026 00:40:19 -0500 Subject: [PATCH] refactor: convert BillingView to StatefulWidget and implement scroll listener for dynamic UI updates --- .../src/presentation/pages/billing_page.dart | 139 ++++++++++++++++-- 1 file changed, 128 insertions(+), 11 deletions(-) diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart index c51f0007..268eb7cb 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart @@ -1,3 +1,4 @@ +import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -6,7 +7,6 @@ import 'package:flutter_modular/flutter_modular.dart'; import '../blocs/billing_bloc.dart'; import '../blocs/billing_event.dart'; import '../blocs/billing_state.dart'; -import '../widgets/billing_header.dart'; import '../widgets/invoice_history_section.dart'; import '../widgets/payment_method_card.dart'; import '../widgets/pending_invoices_section.dart'; @@ -34,23 +34,136 @@ class BillingPage extends StatelessWidget { /// /// This widget displays the billing dashboard content based on the current /// state of the [BillingBloc]. -class BillingView extends StatelessWidget { +class BillingView extends StatefulWidget { /// Creates a [BillingView]. const BillingView({super.key}); + @override + State createState() => _BillingViewState(); +} + +class _BillingViewState extends State { + late ScrollController _scrollController; + bool _isScrolled = false; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + _scrollController.addListener(_onScroll); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (_scrollController.hasClients) { + if (_scrollController.offset > 140 && !_isScrolled) { + setState(() => _isScrolled = true); + } else if (_scrollController.offset <= 140 && _isScrolled) { + setState(() => _isScrolled = false); + } + } + } + @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, BillingState state) { return Scaffold( - body: Column( - children: [ - BillingHeader( - currentBill: state.currentBill, - savings: state.savings, - onBack: () => Modular.to.pop(), + body: CustomScrollView( + controller: _scrollController, + slivers: [ + SliverAppBar( + pinned: true, + expandedHeight: 200.0, + backgroundColor: UiColors.primary, + leading: Center( + child: UiIconButton.secondary( + icon: UiIcons.arrowLeft, + onTap: () => Modular.to.pop(), + ), + ), + title: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: Text( + _isScrolled + ? '\$${state.currentBill.toStringAsFixed(2)}' + : t.client_billing.title, + key: ValueKey(_isScrolled), + style: UiTypography.headline4m.copyWith( + color: UiColors.white, + ), + ), + ), + flexibleSpace: FlexibleSpaceBar( + background: Padding( + padding: const EdgeInsets.only( + top: UiConstants.space0, + left: UiConstants.space5, + right: UiConstants.space5, + bottom: UiConstants.space10, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + t.client_billing.current_period, + style: UiTypography.footnote2r.copyWith( + color: UiColors.white.withValues(alpha: 0.7), + ), + ), + const SizedBox(height: UiConstants.space1), + Text( + '\$${state.currentBill.toStringAsFixed(2)}', + style: UiTypography.display1b + .copyWith(color: UiColors.white), + ), + const SizedBox(height: UiConstants.space2), + Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space2, + vertical: UiConstants.space1, + ), + decoration: BoxDecoration( + color: UiColors.accent, + borderRadius: BorderRadius.circular(100), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + UiIcons.trendingDown, + size: 12, + color: UiColors.foreground, + ), + const SizedBox(width: UiConstants.space1), + Text( + t.client_billing.saved_amount( + amount: state.savings.toStringAsFixed(0), + ), + style: UiTypography.footnote2b.copyWith( + color: UiColors.foreground, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + SliverList( + delegate: SliverChildListDelegate( + [ + _buildContent(context, state), + ], + ), ), - Expanded(child: _buildContent(context, state)), ], ), ); @@ -60,7 +173,10 @@ class BillingView extends StatelessWidget { Widget _buildContent(BuildContext context, BillingState state) { if (state.status == BillingStatus.loading) { - return const Center(child: CircularProgressIndicator()); + return const Padding( + padding: EdgeInsets.all(UiConstants.space10), + child: Center(child: CircularProgressIndicator()), + ); } if (state.status == BillingStatus.failure) { @@ -72,7 +188,7 @@ class BillingView extends StatelessWidget { ); } - return SingleChildScrollView( + return Padding( padding: const EdgeInsets.all(UiConstants.space5), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -89,6 +205,7 @@ class BillingView extends StatelessWidget { const SizedBox(height: UiConstants.space6), InvoiceHistorySection(invoices: state.invoiceHistory), const SizedBox(height: UiConstants.space24), + SizedBox(height: MediaQuery.of(context).size.height * 0.8), ], ), );