From d03d05bc24c014c11871655c8ad4a91c27efebc8 Mon Sep 17 00:00:00 2001 From: Rodrigo Mamede <61851910+Rodrigosdk@users.noreply.github.com> Date: Sat, 24 Oct 2026 12:49:53 -0300 Subject: [PATCH 01/22] feat: implemente AppColors para o gerenciamento de temas e atualize AppTheme para utilizar os novos tokens de cor (#130) --- lib/core/themes/app_colors.dart | 118 +++++++++++++++++++++++++++ lib/core/themes/app_theme.dart | 70 ++++++++++++++-- lib/core/themes/color_tokens.dart | 3 +- lib/modules/app_widget.dart | 18 ++-- test/core/themes/app_theme_test.dart | 81 ++++++++++++++++++ 5 files changed, 275 insertions(+), 15 deletions(-) create mode 100644 lib/core/themes/app_colors.dart create mode 100644 test/core/themes/app_theme_test.dart diff --git a/lib/core/themes/app_colors.dart b/lib/core/themes/app_colors.dart new file mode 100644 index 0000000..cbccf7d --- /dev/null +++ b/lib/core/themes/app_colors.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; + +class AppColors extends ThemeExtension { + final Color background; + final Color sheet; + final Color elevated; + final Color hairline; + final Color textPrimary; + final Color textSecondary; + final Color textHint; + final Color error; + final Color scrim; + final Color bandHome; + final Color onBandSoft; + final Color brandAmber; + final Color onCalc; + + const AppColors({ + required this.background, + required this.sheet, + required this.elevated, + required this.hairline, + required this.textPrimary, + required this.textSecondary, + required this.textHint, + required this.error, + required this.scrim, + required this.bandHome, + required this.onBandSoft, + required this.brandAmber, + required this.onCalc, + }); + + static const light = AppColors( + background: Color(0xFFFFFFFF), + sheet: Color(0xFFFFFFFF), + elevated: Color(0xFFF2F2F5), + hairline: Color(0xFFE8E8EE), + textPrimary: Color(0xFF14121F), + textSecondary: Color(0xFF57546A), + textHint: Color(0xFF6E6B82), + error: Color(0xFFC92A2A), + scrim: Color(0x7314121F), + bandHome: Color(0xFF14121F), + onBandSoft: Color(0xFFB4B1C4), + brandAmber: Color(0xFFD4A657), + onCalc: Color(0xFFFFFFFF), + ); + + static const dark = AppColors( + background: Color(0xFF100E19), + sheet: Color(0xFF1B1926), + elevated: Color(0xFF262333), + hairline: Color(0x17FFFFFF), + textPrimary: Color(0xFFF5F4FA), + textSecondary: Color(0xFFB4B1C4), + textHint: Color(0xFF918DA5), + error: Color(0xFFFF8A8A), + scrim: Color(0xA805040A), + bandHome: Color(0xFF221F2E), + onBandSoft: Color(0xFFB4B1C4), + brandAmber: Color(0xFFD4A657), + onCalc: Color(0xFFFFFFFF), + ); + + @override + AppColors copyWith({ + Color? background, + Color? sheet, + Color? elevated, + Color? hairline, + Color? textPrimary, + Color? textSecondary, + Color? textHint, + Color? error, + Color? scrim, + Color? bandHome, + Color? onBandSoft, + Color? brandAmber, + Color? onCalc, + }) { + return AppColors( + background: background ?? this.background, + sheet: sheet ?? this.sheet, + elevated: elevated ?? this.elevated, + hairline: hairline ?? this.hairline, + textPrimary: textPrimary ?? this.textPrimary, + textSecondary: textSecondary ?? this.textSecondary, + textHint: textHint ?? this.textHint, + error: error ?? this.error, + scrim: scrim ?? this.scrim, + bandHome: bandHome ?? this.bandHome, + onBandSoft: onBandSoft ?? this.onBandSoft, + brandAmber: brandAmber ?? this.brandAmber, + onCalc: onCalc ?? this.onCalc, + ); + } + + @override + AppColors lerp(ThemeExtension? other, double t) { + if (other is! AppColors) return this; + return AppColors( + background: Color.lerp(background, other.background, t)!, + sheet: Color.lerp(sheet, other.sheet, t)!, + elevated: Color.lerp(elevated, other.elevated, t)!, + hairline: Color.lerp(hairline, other.hairline, t)!, + textPrimary: Color.lerp(textPrimary, other.textPrimary, t)!, + textSecondary: Color.lerp(textSecondary, other.textSecondary, t)!, + textHint: Color.lerp(textHint, other.textHint, t)!, + error: Color.lerp(error, other.error, t)!, + scrim: Color.lerp(scrim, other.scrim, t)!, + bandHome: Color.lerp(bandHome, other.bandHome, t)!, + onBandSoft: Color.lerp(onBandSoft, other.onBandSoft, t)!, + brandAmber: Color.lerp(brandAmber, other.brandAmber, t)!, + onCalc: Color.lerp(onCalc, other.onCalc, t)!, + ); + } +} diff --git a/lib/core/themes/app_theme.dart b/lib/core/themes/app_theme.dart index ad0376b..8e89bb0 100644 --- a/lib/core/themes/app_theme.dart +++ b/lib/core/themes/app_theme.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:real_calc/core/themes/extensions/help_card_theme.dart'; import 'package:real_calc/core/themes/extensions/highlight_card_theme.dart'; import 'package:real_calc/core/themes/extensions/menu_card_theme.dart'; import 'package:real_calc/core/themes/extensions/options_bottom_forms_theme.dart'; +import 'app_colors.dart'; import 'color_tokens.dart'; import 'extensions/financing_forms_theme.dart'; import 'extensions/home_page_theme.dart'; @@ -18,26 +20,33 @@ import 'text_styles.dart'; class AppTheme { static ThemeData get darkTheme { + const colors = AppColors.dark; return ThemeData( brightness: Brightness.dark, useMaterial3: true, // Cores base - scaffoldBackgroundColor: ColorTokens.background, + scaffoldBackgroundColor: colors.background, // ColorScheme do Material 3 - colorScheme: const ColorScheme.dark( - surface: ColorTokens.surface, - error: ColorTokens.error, - onPrimary: Colors.white, - onSecondary: Colors.white, - onSurface: ColorTokens.textPrimary, + colorScheme: ColorScheme.dark( + primary: colors.textPrimary, + surface: colors.sheet, + error: colors.error, + onPrimary: colors.background, + onSecondary: colors.background, + onSurface: colors.textPrimary, onError: Colors.black, ), + textSelectionTheme: TextSelectionThemeData( + cursorColor: colors.textPrimary, + selectionColor: colors.textPrimary.withValues(alpha: 0.32), + selectionHandleColor: colors.textPrimary, + ), // AppBar global appBarTheme: AppBarTheme( - backgroundColor: ColorTokens.surface, + backgroundColor: colors.sheet, elevation: 0, centerTitle: true, actionsIconTheme: const IconThemeData(color: ColorTokens.accentAmber), @@ -116,6 +125,7 @@ class AppTheme { ), extensions: [ + colors, const OptionsBottomFormsTheme( calculateButtonBackground: ColorTokens.accentAmber, calculateButtonForeground: Colors.white, @@ -292,6 +302,48 @@ class AppTheme { } static ThemeData get lightTheme { - return darkTheme; + final base = darkTheme; + const colors = AppColors.light; + return base.copyWith( + brightness: Brightness.light, + scaffoldBackgroundColor: colors.background, + colorScheme: ColorScheme.light( + primary: colors.textPrimary, + surface: colors.sheet, + error: colors.error, + onPrimary: colors.onCalc, + onSecondary: colors.onCalc, + onSurface: colors.textPrimary, + onError: colors.onCalc, + ), + textSelectionTheme: TextSelectionThemeData( + cursorColor: colors.textPrimary, + selectionColor: colors.textPrimary.withValues(alpha: 0.32), + selectionHandleColor: colors.textPrimary, + ), + appBarTheme: base.appBarTheme.copyWith(backgroundColor: colors.sheet), + inputDecorationTheme: base.inputDecorationTheme.copyWith( + fillColor: colors.background, + hintStyle: TextStyle(color: colors.textHint), + ), + extensions: [ + colors, + ...base.extensions.values.where((extension) => extension is! AppColors), + ], + ); + } + + static SystemUiOverlayStyle systemUiOverlayStyle(AppColors colors) { + return SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.light, + statusBarBrightness: Brightness.dark, + systemNavigationBarColor: colors.background, + systemNavigationBarIconBrightness: colors == AppColors.light + ? Brightness.dark + : Brightness.light, + systemNavigationBarDividerColor: colors.background, + systemNavigationBarContrastEnforced: false, + ); } } diff --git a/lib/core/themes/color_tokens.dart b/lib/core/themes/color_tokens.dart index 099e548..2443f80 100644 --- a/lib/core/themes/color_tokens.dart +++ b/lib/core/themes/color_tokens.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +@Deprecated('Use AppColors de app_colors.dart para cores adaptadas ao tema.') abstract class ColorTokens { // Superfícies static const Color background = Color(0xFF0A0D12); @@ -31,4 +32,4 @@ abstract class ColorTokens { //icons static const Color iconColor = Color(0xFFFFFFFF); -} \ No newline at end of file +} diff --git a/lib/modules/app_widget.dart b/lib/modules/app_widget.dart index b5b86c8..7f0aece 100644 --- a/lib/modules/app_widget.dart +++ b/lib/modules/app_widget.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_modular/flutter_modular.dart'; +import '../core/themes/app_colors.dart'; import '../core/themes/app_theme.dart'; class AppWidget extends StatelessWidget { @@ -10,9 +12,17 @@ class AppWidget extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp.router( - theme: AppTheme.darkTheme, - darkTheme: AppTheme.lightTheme, + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, themeMode: ThemeMode.dark, + builder: (context, child) { + final colors = + Theme.of(context).extension() ?? AppColors.dark; + return AnnotatedRegion( + value: AppTheme.systemUiOverlayStyle(colors), + child: child ?? const SizedBox.shrink(), + ); + }, routeInformationParser: Modular.routeInformationParser, routerDelegate: Modular.routerDelegate, localizationsDelegates: const [ @@ -20,9 +30,7 @@ class AppWidget extends StatelessWidget { GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - supportedLocales: const [ - Locale('pt', 'BR'), - ], + supportedLocales: const [Locale('pt', 'BR')], locale: const Locale('pt', 'BR'), ); } diff --git a/test/core/themes/app_theme_test.dart b/test/core/themes/app_theme_test.dart new file mode 100644 index 0000000..9a9ff14 --- /dev/null +++ b/test/core/themes/app_theme_test.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:real_calc/core/themes/app_colors.dart'; +import 'package:real_calc/core/themes/app_theme.dart'; + +void main() { + Widget createSut(ThemeMode themeMode) { + return MaterialApp( + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + themeMode: themeMode, + home: const Scaffold(body: SizedBox()), + ); + } + + group('AppTheme', () { + testWidgets('disponibiliza os tokens corretos no tema claro', ( + tester, + ) async { + await tester.pumpWidget(createSut(ThemeMode.light)); + + final context = tester.element(find.byType(Scaffold)); + final theme = Theme.of(context); + final colors = theme.extension()!; + + expect(colors.background, AppColors.light.background); + expect(colors.sheet, AppColors.light.sheet); + expect(colors.elevated, AppColors.light.elevated); + expect(colors.hairline, AppColors.light.hairline); + expect(colors.textPrimary, AppColors.light.textPrimary); + expect(colors.textSecondary, AppColors.light.textSecondary); + expect(colors.textHint, AppColors.light.textHint); + expect(colors.error, AppColors.light.error); + expect(colors.scrim, AppColors.light.scrim); + expect(colors.bandHome, AppColors.light.bandHome); + expect(colors.onBandSoft, AppColors.light.onBandSoft); + expect(colors.brandAmber, AppColors.light.brandAmber); + expect(colors.onCalc, AppColors.light.onCalc); + expect(theme.colorScheme.primary, AppColors.light.textPrimary); + expect(tester.takeException(), isNull); + }); + + testWidgets('disponibiliza os tokens corretos no tema escuro', ( + tester, + ) async { + await tester.pumpWidget(createSut(ThemeMode.dark)); + + final context = tester.element(find.byType(Scaffold)); + final theme = Theme.of(context); + final colors = theme.extension()!; + + expect(colors.background, AppColors.dark.background); + expect(colors.sheet, AppColors.dark.sheet); + expect(colors.elevated, AppColors.dark.elevated); + expect(colors.hairline, AppColors.dark.hairline); + expect(colors.textPrimary, AppColors.dark.textPrimary); + expect(colors.textSecondary, AppColors.dark.textSecondary); + expect(colors.textHint, AppColors.dark.textHint); + expect(colors.error, AppColors.dark.error); + expect(colors.scrim, AppColors.dark.scrim); + expect(colors.bandHome, AppColors.dark.bandHome); + expect(colors.onBandSoft, AppColors.dark.onBandSoft); + expect(colors.brandAmber, AppColors.dark.brandAmber); + expect(colors.onCalc, AppColors.dark.onCalc); + expect(theme.colorScheme.primary, AppColors.dark.textPrimary); + expect(tester.takeException(), isNull); + }); + + test('AppColors suporta copyWith e interpolação', () { + final changed = AppColors.light.copyWith(background: Colors.blue); + expect(changed.background, Colors.blue); + expect(changed.textPrimary, AppColors.light.textPrimary); + + final middle = AppColors.light.lerp(AppColors.dark, 0.5); + expect( + middle.background, + Color.lerp(AppColors.light.background, AppColors.dark.background, 0.5), + ); + }); + }); +} From c24305d5f3e1611fbc269db1532d43e3258f7526 Mon Sep 17 00:00:00 2001 From: Rodrigo Mamede <61851910+Rodrigosdk@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:32:15 -0300 Subject: [PATCH 02/22] =?UTF-8?q?fix:=20Corrigir=20in=C3=ADcio=20da=20s?= =?UTF-8?q?=C3=A9rie=20IGP-M=20para=20per=C3=ADodos=20anteriores=20a=20202?= =?UTF-8?q?1=20(#131)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: atualizar os dados e testes da série IGP-M para refletir os valores históricos a partir de junho de 1989 * feat: atualizar o código SGS do IGP-M para 28655 e aprimorar os testes relacionados para garantir a precisão --- .../domain/enum/correction_index.dart | 7 ++- .../repositories/sgs_repository_test.dart | 29 +++++++++++ .../value_correction_form_cubit_test.dart | 4 +- .../calculate_value_correction_test.dart | 48 +++++++++++++++++++ ...correction_index_series_selector_test.dart | 23 +++++++++ 5 files changed, 105 insertions(+), 6 deletions(-) diff --git a/lib/modules/value_correction/domain/enum/correction_index.dart b/lib/modules/value_correction/domain/enum/correction_index.dart index 604b089..33c7ca9 100644 --- a/lib/modules/value_correction/domain/enum/correction_index.dart +++ b/lib/modules/value_correction/domain/enum/correction_index.dart @@ -14,8 +14,8 @@ enum CorrectionIndex { ), inpc( label: 'INPC (IBGE)', - sgsCode: 188, group: CorrectionGroup.inflation, + sgsCode: 188, kind: SeriesKind.monthlyVariation, granularity: DateGranularity.month, fromYear: 1979, @@ -45,9 +45,8 @@ enum CorrectionIndex { group: CorrectionGroup.inflation, kind: SeriesKind.monthlyVariation, granularity: DateGranularity.month, - fromYear: 2021, - fromMonth: 4, - fromDay: 12, + fromYear: 1989, + fromMonth: 6, ), ipcBrasil( label: 'IPC-Brasil (FGV)', diff --git a/test/modules/value_correction/infrastructure/repositories/sgs_repository_test.dart b/test/modules/value_correction/infrastructure/repositories/sgs_repository_test.dart index e035570..aa15a51 100644 --- a/test/modules/value_correction/infrastructure/repositories/sgs_repository_test.dart +++ b/test/modules/value_correction/infrastructure/repositories/sgs_repository_test.dart @@ -48,6 +48,35 @@ void main() { }, ); + test( + 'consulta o IGP-M histórico de alta precisão pela série SGS 28655', + () async { + when(() => network.get>(any())).thenAnswer( + (_) async => SuccessResult>([ + {'data': '01/01/2015', 'valor': '0.7647618382'}, + {'data': '01/12/2020', 'valor': '0.9581082789'}, + ]), + ); + + final result = await repository.getSeries( + index: CorrectionIndex.igpM, + start: DateTime(2015, 1, 1), + end: DateTime(2020, 12, 1), + ); + + expect(result.isSuccess, isTrue); + expect(result.getOrNull(), [ + SeriesPoint(date: DateTime(2015, 1, 1), value: 0.7647618382), + SeriesPoint(date: DateTime(2020, 12, 1), value: 0.9581082789), + ]); + verify( + () => network.get>( + any(that: contains('bcdata.sgs.28655')), + ), + ).called(1); + }, + ); + test('deve ignorar pontos com valor inválido', () async { when(() => network.get>(any())).thenAnswer( (_) async => SuccessResult>([ diff --git a/test/modules/value_correction/presentation/cubit/forms/value_correction_form_cubit_test.dart b/test/modules/value_correction/presentation/cubit/forms/value_correction_form_cubit_test.dart index 1f8b44a..7f2f7ef 100644 --- a/test/modules/value_correction/presentation/cubit/forms/value_correction_form_cubit_test.dart +++ b/test/modules/value_correction/presentation/cubit/forms/value_correction_form_cubit_test.dart @@ -72,8 +72,8 @@ void main() { expect(cubit.state.canCalculate, isFalse); cubit.setIndex(CorrectionIndex.igpM); - cubit.initialDate.text = '04/2021'; - cubit.finalDate.text = '04/2021'; + cubit.initialDate.text = '01/2015'; + cubit.finalDate.text = '12/2020'; expect(cubit.state.fieldErrors[ValueCorrectionField.initialDate], isNull); expect(cubit.state.warningBannerMessage, isEmpty); diff --git a/test/modules/value_correction/use_cases/calculate_value_correction_test.dart b/test/modules/value_correction/use_cases/calculate_value_correction_test.dart index c037e04..dca14d4 100644 --- a/test/modules/value_correction/use_cases/calculate_value_correction_test.dart +++ b/test/modules/value_correction/use_cases/calculate_value_correction_test.dart @@ -156,6 +156,54 @@ void main() { expect(result.getOrNull()!.factor, 1); }); + test( + 'case de referência: IGP-M de 01/2015 a 12/2020 reproduz a Calculadora do Cidadão', + () { + const monthlyRates = ''' + 0.7647618382, 0.2705864365, 0.9808441075, 1.1656857512, 0.4063001692, 0.6715458172, + 0.6911045826, 0.2755675908, 0.9461909183, 1.8911533913, 1.5242249087, 0.4874187975, + 1.1370339879, 1.2905810339, 0.5117747748, 0.3281660945, 0.8184376735, 1.6875411382, + 0.1752114780, 0.1467980160, 0.1970707838, 0.1572552040, -0.0265986956, 0.5400211630, + 0.6408550379, 0.0836911870, 0.0147125277, -1.0956218656, -0.9318589524, -0.6662499732, + -0.7160603878, 0.0958422781, 0.4681998128, 0.1964782206, 0.5243019585, 0.8873239653, + 0.7550250130, 0.0731715412, 0.6351470125, 0.5697173435, 1.3781659311, 1.8672336349, + 0.5067165546, 0.6999138921, 1.5243480975, 0.8859122959, -0.4895058190, -1.0801688000, + 0.0066436636, 0.8846793161, 1.2553467825, 0.9186295414, 0.4454675833, 0.7952552229, + 0.3961154951, -0.6668950800, -0.0054318158, 0.6750755742, 0.3001350270, 2.0918341273, + 0.4770047108, -0.0406433182, 1.2440600559, 0.8022976831, 0.2800407659, 1.5570051776, + 2.2278336608, 2.7441632524, 4.3407948692, 3.2314190240, 3.2773938796, 0.9581082789 + '''; + final rates = monthlyRates + .split(',') + .map((value) => double.parse(value.trim())) + .toList(); + final series = List.generate( + rates.length, + (index) => SeriesPoint( + date: DateTime(2015, index + 1, 1), + value: rates[index], + ), + ); + + final result = + CalculateValueCorrection( + buildDataPreparer(), + buildRateCalculator(), + ).call( + params: buildValueCorrection( + index: CorrectionIndex.igpM.sgsCode, + initial: DateTime(2015, 1, 1), + end: DateTime(2020, 12, 1), + ), + series: series, + type: SeriesKind.monthlyVariation, + ); + + expect(result.isSuccess, isTrue); + expect(result.getOrNull()!.factor, closeTo(1.67455430, 1e-7)); + }, + ); + test( 'case de referência: IPCA de 01/2003 a 01/2003 produz fator 1,0225', () { diff --git a/test/modules/value_correction/use_cases/correction/series_selection/correction_index_series_selector_test.dart b/test/modules/value_correction/use_cases/correction/series_selection/correction_index_series_selector_test.dart index 033b4c0..3883a1a 100644 --- a/test/modules/value_correction/use_cases/correction/series_selection/correction_index_series_selector_test.dart +++ b/test/modules/value_correction/use_cases/correction/series_selection/correction_index_series_selector_test.dart @@ -17,6 +17,29 @@ void main() { }); group('CorrectionIndexSeriesSelector', () { + test('IGP-M usa a série mensal histórica desde 06/1989', () { + final igpMSeries = [ + SeriesPoint(date: DateTime(2015, 1, 1), value: 0.76), + SeriesPoint(date: DateTime(2020, 12, 1), value: 0.96), + ]; + + expect(CorrectionIndex.igpM.sgsCode, 28655); + expect(CorrectionIndex.igpM.fromYear, 1989); + expect(CorrectionIndex.igpM.fromMonth, 6); + expect(CorrectionIndex.igpM.fromDay, 1); + expect(CorrectionIndex.igpM.availableFromInputLabel, '06/1989'); + expect( + selector.selectCalculationSeries( + index: CorrectionIndex.igpM.sgsCode, + type: SeriesKind.monthlyVariation, + filteredSeries: igpMSeries, + savingsRates: const [], + trRates: const [], + ), + igpMSeries, + ); + }); + test('calcula taxa legal apenas para o índice Taxa Legal', () { final series = [SeriesPoint(date: DateTime(2024, 1, 1), value: 1)]; From 72983b15c7c6f2b050f1fdd40d71634f8e461e10 Mon Sep 17 00:00:00 2001 From: Rodrigo Mamede <61851910+Rodrigosdk@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:47:28 -0300 Subject: [PATCH 03/22] =?UTF-8?q?feat:=20tipografia=20(Bricolage=20Grotesq?= =?UTF-8?q?ue=20e=20Manrope),=20espa=C3=A7amento=20e=20raios=20(#132)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: adicionar arquivos de fonte e licenças para Bricolage Grotesque e Manrope * feat: adicionar tokens de tipografia e espaçamento, atualizar estilos do tema e remover a dependência google_fonts * fix: ajustar padding compacto no MenuCardTheme para melhor alinhamento * feat: refatorar a função principal para oferecer suporte à inicialização assíncrona e adicionar licenças de fontes * fix: atualizar descrição da tipografia no README para refletir uso offline --- README.md | 2 +- assets/fonts/Bricolage-Grotesque-OFL.txt | 93 +++++++++ assets/fonts/BricolageGrotesque-ExtraBold.ttf | Bin 0 -> 82332 bytes assets/fonts/Manrope-Bold.ttf | Bin 0 -> 94928 bytes assets/fonts/Manrope-ExtraBold.ttf | Bin 0 -> 95688 bytes assets/fonts/Manrope-Medium.ttf | Bin 0 -> 95060 bytes assets/fonts/Manrope-OFL.txt | 93 +++++++++ assets/fonts/Manrope-SemiBold.ttf | Bin 0 -> 95096 bytes lib/core/themes/app_theme.dart | 1 + .../themes/extensions/menu_card_theme.dart | 5 +- lib/core/themes/radius.dart | 7 + lib/core/themes/spacing.dart | 7 + lib/core/themes/text_styles.dart | 179 +++++++++++++++--- lib/main.dart | 17 +- .../pages/value_correction_result_page.dart | 2 +- pubspec.lock | 8 - pubspec.yaml | 18 +- test/core/widgets/help_card_test.dart | 4 +- test/flutter_test_config.dart | 3 - test/modules/app_widget_test.dart | 5 - .../components/menu_card_test.dart | 8 +- .../components/metric_card_test.dart | 4 +- 22 files changed, 395 insertions(+), 61 deletions(-) create mode 100644 assets/fonts/Bricolage-Grotesque-OFL.txt create mode 100644 assets/fonts/BricolageGrotesque-ExtraBold.ttf create mode 100644 assets/fonts/Manrope-Bold.ttf create mode 100644 assets/fonts/Manrope-ExtraBold.ttf create mode 100644 assets/fonts/Manrope-Medium.ttf create mode 100644 assets/fonts/Manrope-OFL.txt create mode 100644 assets/fonts/Manrope-SemiBold.ttf create mode 100644 lib/core/themes/radius.dart diff --git a/README.md b/README.md index e956f2f..ab3b499 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Essa lógica vive num Cubit dedicado por calculadora, reagindo em tempo real a c | `Dio` | Cliente HTTP | | `equatable` | Comparação de estados e objetos | | `intl` | Formatação de valores e datas | -| `google_fonts` | Tipografia (Manrope) | +| Manrope e Bricolage Grotesque | Tipografia empacotada no app para uso offline | | `mocktail` e `bloc_test` | Testes | **API de terceiros:** [API do Banco Central do Brasil](https://dadosabertos.bcb.gov.br/), usada como fonte de dados da taxa Selic. diff --git a/assets/fonts/Bricolage-Grotesque-OFL.txt b/assets/fonts/Bricolage-Grotesque-OFL.txt new file mode 100644 index 0000000..a397658 --- /dev/null +++ b/assets/fonts/Bricolage-Grotesque-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Bricolage Grotesque Project Authors (https://github.com/ateliertriay/bricolage) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/BricolageGrotesque-ExtraBold.ttf b/assets/fonts/BricolageGrotesque-ExtraBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..74950f209584b0a6b52fa1cc0981be41f8bb9f28 GIT binary patch literal 82332 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/Manrope-Bold.ttf b/assets/fonts/Manrope-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..746764de030c95266dd33fa1f440795ed80bbc90 GIT binary patch literal 94928 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 RxaxUe6CQz8oW<11006QG`YZqd literal 0 HcmV?d00001 diff --git a/assets/fonts/Manrope-ExtraBold.ttf b/assets/fonts/Manrope-ExtraBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..d324b62f053c0aa0061d35298d14cdbeb6274847 GIT binary patch literal 95688 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 z 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 Tsyf^)S`c*%7yeu4CpG^cmn!zY literal 0 HcmV?d00001 diff --git a/assets/fonts/Manrope-Medium.ttf b/assets/fonts/Manrope-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2a169e6b902fc2d8fe470d0da04162de933de134 GIT binary patch literal 95060 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/Manrope-OFL.txt b/assets/fonts/Manrope-OFL.txt new file mode 100644 index 0000000..e271172 --- /dev/null +++ b/assets/fonts/Manrope-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2018 The Manrope Project Authors (https://github.com/googlefonts/manrope) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/Manrope-SemiBold.ttf b/assets/fonts/Manrope-SemiBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..595e7dc699c8e2a30e765691fe70174bb388ce73 GIT binary patch literal 95096 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 Xp>5SoMg@fGC14YdgH@cx)X4w { this.iconSizeCompact = 20, this.borderRadius = 20, this.padding = const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - this.compactPadding = const EdgeInsets.all(8), + this.compactPadding = const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), this.heightThreshold = 100, }); diff --git a/lib/core/themes/radius.dart b/lib/core/themes/radius.dart new file mode 100644 index 0000000..ef114dd --- /dev/null +++ b/lib/core/themes/radius.dart @@ -0,0 +1,7 @@ +abstract class AppRadius { + static const double button = 28; + static const double sheet = 28; + static const double iconTile = 12; + static const double search = 14; + static const double pill = 999; +} \ No newline at end of file diff --git a/lib/core/themes/spacing.dart b/lib/core/themes/spacing.dart index 9fee8bd..38c5d0c 100644 --- a/lib/core/themes/spacing.dart +++ b/lib/core/themes/spacing.dart @@ -7,6 +7,13 @@ abstract class AppSpacing { static const double lg = 24; static const double xl = 32; static const double xxl = 48; + + // Vibrante minimalista layout tokens. + static const double screen = 24; + static const double rowHeight = 58; + static const double rowHeightWithSecondLine = 64; + static const double homeRowHeight = 72; + static const double sectionGap = 26; // Padding padrão de tela static const EdgeInsets screenPadding = EdgeInsets.all(md); diff --git a/lib/core/themes/text_styles.dart b/lib/core/themes/text_styles.dart index 87433e2..688bacf 100644 --- a/lib/core/themes/text_styles.dart +++ b/lib/core/themes/text_styles.dart @@ -1,13 +1,135 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'color_tokens.dart'; class AppTextStyles { - static final TextStyle _base = GoogleFonts.manrope(); + static const String manropeFamily = 'Manrope'; + static const String bricolageGrotesqueFamily = 'Bricolage Grotesque'; + + static final TextStyle _base = const TextStyle( + fontFamily: manropeFamily, + fontWeight: FontWeight.w500, + ); static final TextStyle _baseWhite = _base.copyWith(color: Colors.white); - // Headlines — mantidos, ainda usados pela TextTheme padrão do Material - // (dialogs, snackbars etc.) mesmo com o header customizado nas telas. + // Estilos de destaque e componentes da direção Vibrante minimalista. + // As cores são definidas pelo componente ou tema que utiliza cada estilo. + static final TextStyle bandStatement = const TextStyle( + fontFamily: bricolageGrotesqueFamily, + fontSize: 48, + fontWeight: FontWeight.w800, + letterSpacing: -1.8, + ); + static final TextStyle screenTitle = const TextStyle( + fontFamily: bricolageGrotesqueFamily, + fontSize: 28, + fontWeight: FontWeight.w800, + letterSpacing: -0.9, + ); + static final TextStyle sheetTitle = const TextStyle( + fontFamily: bricolageGrotesqueFamily, + fontSize: 28, + fontWeight: FontWeight.w800, + letterSpacing: -0.8, + ); + static final TextStyle exampleValue = const TextStyle( + fontFamily: bricolageGrotesqueFamily, + fontSize: 30, + fontWeight: FontWeight.w800, + letterSpacing: -0.8, + ); + static final TextStyle wordmark = const TextStyle( + fontFamily: bricolageGrotesqueFamily, + fontSize: 21, + fontWeight: FontWeight.w800, + letterSpacing: -0.5, + ); + + static final TextStyle bandKicker = const TextStyle( + fontFamily: manropeFamily, + fontSize: 15, + fontWeight: FontWeight.w600, + ); + static final TextStyle bandSub = const TextStyle( + fontFamily: manropeFamily, + fontSize: 15, + fontWeight: FontWeight.w500, + ); + static final TextStyle headerTitle = const TextStyle( + fontFamily: manropeFamily, + fontSize: 16, + fontWeight: FontWeight.w700, + ); + static final TextStyle rowLabel = const TextStyle( + fontFamily: manropeFamily, + fontSize: 16, + fontWeight: FontWeight.w500, + ); + static final TextStyle rowValue = TextStyle( + fontFamily: manropeFamily, + fontSize: 17, + fontWeight: FontWeight.w700, + fontFeatures: const [FontFeature.tabularFigures()], + ); + static final TextStyle rowUnit = const TextStyle( + fontFamily: manropeFamily, + fontSize: 14, + fontWeight: FontWeight.w500, + ); + static final TextStyle rowState = const TextStyle( + fontFamily: manropeFamily, + fontSize: 12, + fontWeight: FontWeight.w700, + ); + static final TextStyle listTitle = const TextStyle( + fontFamily: manropeFamily, + fontSize: 17, + fontWeight: FontWeight.w700, + ); + static final TextStyle listDesc = const TextStyle( + fontFamily: manropeFamily, + fontSize: 13, + fontWeight: FontWeight.w500, + ); + static final TextStyle sectionLabel = const TextStyle( + fontFamily: manropeFamily, + fontSize: 13, + fontWeight: FontWeight.w600, + ); + static final TextStyle detailLabel = const TextStyle( + fontFamily: manropeFamily, + fontSize: 14, + fontWeight: FontWeight.w500, + ); + static final TextStyle detailValue = TextStyle( + fontFamily: manropeFamily, + fontSize: 15, + fontWeight: FontWeight.w700, + fontFeatures: const [FontFeature.tabularFigures()], + ); + static final TextStyle button = const TextStyle( + fontFamily: manropeFamily, + fontSize: 16, + fontWeight: FontWeight.w800, + ); + static final TextStyle pill = const TextStyle( + fontFamily: manropeFamily, + fontSize: 13, + fontWeight: FontWeight.w800, + ); + static final TextStyle body = const TextStyle( + fontFamily: manropeFamily, + fontSize: 15, + fontWeight: FontWeight.w500, + height: 1.5, + ); + static final TextStyle caption = const TextStyle( + fontFamily: manropeFamily, + fontSize: 12, + fontWeight: FontWeight.w500, + ); + + // Títulos mantidos para uso no TextTheme padrão do Material + // (caixas de diálogo, mensagens etc.), além dos cabeçalhos personalizados. static final TextStyle headlineLarge = _base.copyWith( fontSize: 32, fontWeight: FontWeight.bold, @@ -18,8 +140,8 @@ class AppTextStyles { fontWeight: FontWeight.bold, ); - // Body — mantidos sem alteração, `bodyLarge` já é o tamanho correto - // usado nos valores de input pelo InputForms. + // Estilos de texto mantidos sem alteração. `bodyLarge` já tem o tamanho + // correto para os valores de entrada dos formulários. static final TextStyle bodyLarge = _base.copyWith(fontSize: 16); static final TextStyle bodyMedium = _base.copyWith(fontSize: 14); static final TextStyle labelLarge = _base.copyWith( @@ -28,12 +150,12 @@ class AppTextStyles { letterSpacing: 0.5, ); - // Componentes gerais — mantidos. errorBannerText já usa ColorTokens.error, - // só depende da cor em si ser atualizada (ColorTokens), não do estilo. + // Estilos gerais mantidos. `errorBannerText` já usa ColorTokens.error; + // a cor é definida em ColorTokens, sem alterar o estilo. static final TextStyle helpCardMessage = _base.copyWith( fontSize: 14, color: ColorTokens.textPrimary, - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w500, ); static final TextStyle inputHelperText = _base.copyWith( fontSize: 12, @@ -45,11 +167,9 @@ class AppTextStyles { color: ColorTokens.error, ); - // Card de métrica (Selic) — NOVO. Substitui highlightCardTitle/ - // highlightCardSubtitle/highlightCardButton, que eram específicos do - // HighlightCard antigo (removido). Cor não é fixada aqui porque muda - // conforme o estado (amber padrão, verde/vermelho em variação futura); - // aplicar via .copyWith(color: ...) no componente. + // Cartão de métrica (Selic). Substitui os estilos específicos do antigo + // cartão de destaque, que foi removido. A cor varia conforme o estado + // (âmbar, verde ou vermelho) e é aplicada no componente com `.copyWith`. static final TextStyle metricLabel = _base.copyWith( fontSize: 12, color: ColorTokens.textSecondary, @@ -61,38 +181,36 @@ class AppTextStyles { fontFeatures: const [FontFeature.tabularFigures()], ); static final TextStyle metricCaption = _base.copyWith( - fontSize: 11, + fontSize: 12, color: ColorTokens.textHint, ); - // Badge de estado do campo ("vazio" / "calculado" / "resultado") — NOVO. - // Sem cor fixa: cada estado (amber/verde/vermelho) aplica a própria cor. + // Indicador de estado do campo ("vazio", "calculado" ou "resultado"). + // Sem cor fixa: cada estado aplica sua própria cor. static final TextStyle stateBadge = _base.copyWith( - fontSize: 11, + fontSize: 12, fontWeight: FontWeight.w500, ); - // MenuCard + // Cartão do menu. static final TextStyle menuCardTitle = _baseWhite.copyWith( fontSize: 13, fontWeight: FontWeight.bold, ); static final TextStyle menuCardTitleCompact = _baseWhite.copyWith( - fontSize: 11, + fontSize: 12, fontWeight: FontWeight.bold, ); static final TextStyle menuCardDescription = _baseWhite.copyWith( - fontSize: 10, - height: 1.1, + fontSize: 12, + height: 1.2, ); - // Corrigido: estava em 9px, abaixo do mínimo legível (11px). static final TextStyle menuCardDescriptionCompact = _baseWhite.copyWith( - fontSize: 11, - height: 1.1, + fontSize: 12, + height: 1.2, ); - // MenuCard — variante featured (tile full-width, texto um pouco maior - // por ter mais espaço horizontal disponível). NOVO. + // Cartão do menu em destaque, com texto maior por ter mais espaço. static final TextStyle menuCardTitleFeatured = _baseWhite.copyWith( fontSize: 14, fontWeight: FontWeight.bold, @@ -102,14 +220,13 @@ class AppTextStyles { color: ColorTokens.textSecondary, ); - // Home + // Tela inicial. static final TextStyle appName = _baseWhite.copyWith( fontSize: 20, fontWeight: FontWeight.bold, ); - // Cor movida pra dentro do estilo (era aplicada com .withValues(alpha: 0.4) - // no call site em home_page.dart) — evita repetir o ajuste de opacidade - // toda vez que o label for usado. + // Cor definida no próprio estilo, antes aplicada no local de uso da tela + // inicial. Isso evita repetir o ajuste de opacidade em cada utilização. static final TextStyle sectionHeader = _base.copyWith( fontSize: 12, fontWeight: FontWeight.bold, diff --git a/lib/main.dart b/lib/main.dart index acc26b9..58db7a0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,25 @@ import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'modules/app_module.dart'; import 'modules/app_widget.dart'; -void main() { - initializeDateFormatting('pt_BR', null); +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + LicenseRegistry.addLicense(() async* { + yield LicenseEntryWithLineBreaks( + ['Manrope'], + await rootBundle.loadString('assets/fonts/Manrope-OFL.txt'), + ); + yield LicenseEntryWithLineBreaks( + ['Bricolage Grotesque'], + await rootBundle.loadString('assets/fonts/Bricolage-Grotesque-OFL.txt'), + ); + }); + await initializeDateFormatting('pt_BR', null); runApp( ModularApp( diff --git a/lib/modules/value_correction/presentation/pages/value_correction_result_page.dart b/lib/modules/value_correction/presentation/pages/value_correction_result_page.dart index 242cbac..caa8112 100644 --- a/lib/modules/value_correction/presentation/pages/value_correction_result_page.dart +++ b/lib/modules/value_correction/presentation/pages/value_correction_result_page.dart @@ -202,7 +202,7 @@ class _SpotlightBadge extends StatelessWidget { ), child: Text( 'calculado', - style: theme.spotlightVariationStyle.copyWith(fontSize: 10), + style: theme.spotlightVariationStyle.copyWith(fontSize: 12), ), ); } diff --git a/pubspec.lock b/pubspec.lock index 04bd3c0..3ba6073 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -285,14 +285,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" - google_fonts: - dependency: "direct main" - description: - name: google_fonts - sha256: e3cb3ee6b47fd2472c23de6da5744796a4da195137759ddb3fbcc9467b7b3c7d - url: "https://pub.dev" - source: hosted - version: "8.2.1" hooks: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1e49580..391a80e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,7 +35,6 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.9 - google_fonts: ^8.2.1 flutter_bloc: ^9.1.1 flutter_modular: ^6.4.1 equatable: ^2.1.0 @@ -71,6 +70,23 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/branding/app_icon.svg + - assets/fonts/ + + fonts: + - family: Manrope + fonts: + - asset: assets/fonts/Manrope-Medium.ttf + weight: 500 + - asset: assets/fonts/Manrope-SemiBold.ttf + weight: 600 + - asset: assets/fonts/Manrope-Bold.ttf + weight: 700 + - asset: assets/fonts/Manrope-ExtraBold.ttf + weight: 800 + - family: Bricolage Grotesque + fonts: + - asset: assets/fonts/BricolageGrotesque-ExtraBold.ttf + weight: 800 # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images diff --git a/test/core/widgets/help_card_test.dart b/test/core/widgets/help_card_test.dart index 40bb6f8..d46be76 100644 --- a/test/core/widgets/help_card_test.dart +++ b/test/core/widgets/help_card_test.dart @@ -40,7 +40,7 @@ void main() { expect(theme.messageStyle.color, ColorTokens.textPrimary); expect(theme.messageStyle.fontSize, 14); - expect(theme.messageStyle.fontWeight, FontWeight.w400); + expect(theme.messageStyle.fontWeight, FontWeight.w500); }); testWidgets('Deve exibir o texto da mensagem e o ícone de lâmpada', (tester) async { @@ -54,7 +54,7 @@ void main() { expect(resolvedStyle?.color, ColorTokens.textPrimary); expect(resolvedStyle?.fontSize, 14); - expect(resolvedStyle?.fontWeight, FontWeight.w400); + expect(resolvedStyle?.fontWeight, FontWeight.w500); expect(find.byIcon(Icons.lightbulb), findsOneWidget); }); diff --git a/test/flutter_test_config.dart b/test/flutter_test_config.dart index 9d5b0a5..c24855e 100644 --- a/test/flutter_test_config.dart +++ b/test/flutter_test_config.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:google_fonts/google_fonts.dart'; import 'package:mocktail/mocktail.dart'; import 'package:real_calc/modules/financial_calculators/domain/enum/financial_calculation_target.dart'; @@ -9,8 +8,6 @@ import 'fake_financing.dart'; Future testExecutable(FutureOr Function() testMain) async { - GoogleFonts.config.allowRuntimeFetching = false; - registerFallbackValue(FinancialCalculationTarget.initialValue); registerFallbackValue(FakeFinancing()); registerFallbackValue(FakeFinancialCalculation()); diff --git a/test/modules/app_widget_test.dart b/test/modules/app_widget_test.dart index 1d64759..408b2c3 100644 --- a/test/modules/app_widget_test.dart +++ b/test/modules/app_widget_test.dart @@ -1,15 +1,10 @@ import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:real_calc/modules/app_module.dart'; import 'package:real_calc/modules/app_widget.dart'; import 'package:real_calc/modules/home/presentation/page/page.dart'; void main() { - setUpAll(() { - GoogleFonts.config.allowRuntimeFetching = false; - }); - tearDown(() { Modular.destroy(); }); diff --git a/test/modules/home/presentation/components/menu_card_test.dart b/test/modules/home/presentation/components/menu_card_test.dart index 92d8472..b4c9815 100644 --- a/test/modules/home/presentation/components/menu_card_test.dart +++ b/test/modules/home/presentation/components/menu_card_test.dart @@ -17,14 +17,14 @@ void main() { const double heightThreshold = 100; const double normalIconSize = 24; const double compactIconSize = 20; - const double compactTitleFontSize = 11; - const double compactDescriptionFontSize = 9; + const double compactTitleFontSize = 12; + const double compactDescriptionFontSize = 12; const featuredBorderColor = Color(0xFFD4A657); const disabledLabelColor = Color(0xFF6C7480); const titleStyle = TextStyle(fontSize: 14, fontWeight: FontWeight.w500); const descriptionStyle = TextStyle(fontSize: 16, fontWeight: FontWeight.w600); - const titleStyleCompact = TextStyle(fontSize: 11, fontWeight: FontWeight.w500); - const descriptionStyleCompact = TextStyle(fontSize: 9, fontWeight: FontWeight.w600); + const titleStyleCompact = TextStyle(fontSize: 12, fontWeight: FontWeight.w500); + const descriptionStyleCompact = TextStyle(fontSize: 12, fontWeight: FontWeight.w600); const titleStyleFeatured = TextStyle(fontSize: 14, fontWeight: FontWeight.w500); const descriptionStyleFeatured = TextStyle(fontSize: 16, fontWeight: FontWeight.w600); const disabledTextStyle = TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: disabledLabelColor); diff --git a/test/modules/home/presentation/components/metric_card_test.dart b/test/modules/home/presentation/components/metric_card_test.dart index b650847..5c7ce13 100644 --- a/test/modules/home/presentation/components/metric_card_test.dart +++ b/test/modules/home/presentation/components/metric_card_test.dart @@ -16,8 +16,8 @@ void main() { labelStyle: TextStyle(fontSize: 12), valueStyle: TextStyle(fontSize: 30, fontWeight: FontWeight.w500), valueUnitStyle: TextStyle(fontSize: 15), - captionStyle: TextStyle(fontSize: 11), - variationTextStyle: TextStyle(fontSize: 11, fontWeight: FontWeight.w500), + captionStyle: TextStyle(fontSize: 12), + variationTextStyle: TextStyle(fontSize: 12, fontWeight: FontWeight.w500), variationPositiveColor: positiveColor, variationNegativeColor: negativeColor, variationNeutralColor: neutralColor, From a764e7f42cb0db0f71d9a555517ca4441999a69a Mon Sep 17 00:00:00 2001 From: Rodrigo Mamede <61851910+Rodrigosdk@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:58:46 -0300 Subject: [PATCH 04/22] =?UTF-8?q?feat:=20cor=20pr=C3=B3pria=20por=20calcul?= =?UTF-8?q?adora=20(CalculatorPalette=20e=20CalculatorScope)=20(#127)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: adicionar CalculatorPalette com a cor de cada calculadora (#127) * feat: aplicar CalculatorScope nas calculadoras e cor por calculadora na tela inicial (#127) * chore: ignorar binários locais, excluir pastas nativas do analyzer e adicionar devtools_options --- .gitignore | 3 + analysis_options.yaml | 6 ++ devtools_options.yaml | 3 + lib/core/themes/calculator_palette.dart | 54 ++++++++++++ lib/core/themes/calculator_scope.dart | 55 ++++++++++++ lib/modules/financial_calculators/module.dart | 17 +++- lib/modules/home/presentation/page/page.dart | 33 +++++-- lib/modules/value_correction/module.dart | 12 ++- test/core/themes/calculator_palette_test.dart | 87 +++++++++++++++++++ 9 files changed, 257 insertions(+), 13 deletions(-) create mode 100644 devtools_options.yaml create mode 100644 lib/core/themes/calculator_palette.dart create mode 100644 lib/core/themes/calculator_scope.dart create mode 100644 test/core/themes/calculator_palette_test.dart diff --git a/.gitignore b/.gitignore index f2fc72d..5bcb512 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ app.*.map.json /android/app/release /android/key.properties *.jks + +# Binários locais de ferramentas (ex.: act) +/bin/ diff --git a/analysis_options.yaml b/analysis_options.yaml index 0d29021..7e428b9 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,6 +7,12 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. +analyzer: + exclude: + - build/** + - android/** + - ios/** + - linux/** include: package:flutter_lints/flutter.yaml linter: diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/core/themes/calculator_palette.dart b/lib/core/themes/calculator_palette.dart new file mode 100644 index 0000000..5012a41 --- /dev/null +++ b/lib/core/themes/calculator_palette.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +enum CalculatorKind { financing, deposits, futureValue, correction } + +class CalculatorPalette { + final Color solid; + final Color onSolid; + final Color text; + + const CalculatorPalette({ + required this.solid, + required this.onSolid, + required this.text, + }); + + static CalculatorPalette of( + CalculatorKind kind, { + Brightness brightness = Brightness.light, + }) { + final isDark = brightness == Brightness.dark; + return switch (kind) { + CalculatorKind.financing => CalculatorPalette( + solid: const Color(0xFF364FC7), + onSolid: Colors.white, + text: Color(isDark ? 0xFF91A7FF : 0xFF364FC7), + ), + CalculatorKind.deposits => CalculatorPalette( + solid: const Color(0xFF087F5B), + onSolid: Colors.white, + text: Color(isDark ? 0xFF63E6BE : 0xFF0A6E50), + ), + CalculatorKind.futureValue => CalculatorPalette( + solid: const Color(0xFF6741D9), + onSolid: Colors.white, + text: Color(isDark ? 0xFFB197FC : 0xFF6741D9), + ), + CalculatorKind.correction => CalculatorPalette( + solid: const Color(0xFFB85300), + onSolid: Colors.white, + text: Color(isDark ? 0xFFFFA94D : 0xFFA04600), + ), + }; + } + + @override + bool operator ==(Object other) => + other is CalculatorPalette && + other.solid == solid && + other.onSolid == onSolid && + other.text == text; + + @override + int get hashCode => Object.hash(solid, onSolid, text); +} diff --git a/lib/core/themes/calculator_scope.dart b/lib/core/themes/calculator_scope.dart new file mode 100644 index 0000000..946184e --- /dev/null +++ b/lib/core/themes/calculator_scope.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +import 'calculator_palette.dart'; + +class CalculatorScope extends StatelessWidget { + final CalculatorKind kind; + final Widget child; + + const CalculatorScope({required this.kind, required this.child, super.key}); + + static CalculatorPalette? maybeOf(BuildContext context) => context + .dependOnInheritedWidgetOfExactType<_CalculatorPaletteScope>() + ?.palette; + + static CalculatorPalette of(BuildContext context) { + final palette = maybeOf(context); + assert( + palette != null, + 'CalculatorScope não encontrado na árvore de widgets.', + ); + return palette!; + } + + @override + Widget build(BuildContext context) { + final palette = CalculatorPalette.of( + kind, + brightness: Theme.of(context).brightness, + ); + final theme = Theme.of(context); + final localTheme = theme.copyWith( + colorScheme: theme.colorScheme.copyWith(primary: palette.solid), + textSelectionTheme: TextSelectionThemeData( + cursorColor: palette.solid, + selectionColor: palette.solid.withValues(alpha: 0.32), + selectionHandleColor: palette.solid, + ), + ); + + return Theme( + data: localTheme, + child: _CalculatorPaletteScope(palette: palette, child: child), + ); + } +} + +class _CalculatorPaletteScope extends InheritedWidget { + final CalculatorPalette palette; + + const _CalculatorPaletteScope({required this.palette, required super.child}); + + @override + bool updateShouldNotify(_CalculatorPaletteScope oldWidget) => + palette != oldWidget.palette; +} diff --git a/lib/modules/financial_calculators/module.dart b/lib/modules/financial_calculators/module.dart index 142928c..fa25c11 100644 --- a/lib/modules/financial_calculators/module.dart +++ b/lib/modules/financial_calculators/module.dart @@ -1,6 +1,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:real_calc/modules/app_module.dart'; +import 'package:real_calc/core/themes/calculator_scope.dart'; +import 'package:real_calc/core/themes/calculator_palette.dart'; import 'package:real_calc/modules/financial_calculators/presentation/pages/future_value_page.dart'; import 'package:real_calc/modules/financial_calculators/use_case/future_value/i_calculate_future_value.dart'; import 'package:real_calc/modules/financial_calculators/use_case/regular_deposits/i_calculate_deposit.dart'; @@ -39,7 +41,10 @@ class FinancialCalculatorsModule extends AppModule { BlocProvider(create: (_) => Modular.get()), BlocProvider(create: (_) => Modular.get()), ], - child: const FinancingPage(), + child: const CalculatorScope( + kind: CalculatorKind.financing, + child: FinancingPage(), + ), ), ); r.child( @@ -49,7 +54,10 @@ class FinancialCalculatorsModule extends AppModule { BlocProvider(create: (_) => Modular.get()), BlocProvider(create: (_) => Modular.get()), ], - child: const RegularDepositsPage(), + child: const CalculatorScope( + kind: CalculatorKind.deposits, + child: RegularDepositsPage(), + ), ), ); r.child( @@ -59,7 +67,10 @@ class FinancialCalculatorsModule extends AppModule { BlocProvider(create: (_) => Modular.get()), BlocProvider(create: (_) => Modular.get()), ], - child: const FutureValuePage(), + child: const CalculatorScope( + kind: CalculatorKind.futureValue, + child: FutureValuePage(), + ), ), ); } diff --git a/lib/modules/home/presentation/page/page.dart b/lib/modules/home/presentation/page/page.dart index 13b8ebe..023d0ee 100644 --- a/lib/modules/home/presentation/page/page.dart +++ b/lib/modules/home/presentation/page/page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:real_calc/core/routes/app_routes.dart'; +import 'package:real_calc/core/themes/calculator_palette.dart'; import 'package:real_calc/core/themes/spacing.dart'; import 'package:real_calc/core/widgets/title_widget.dart'; import 'package:real_calc/modules/home/domain/enum/menu_card_variant.dart'; @@ -18,8 +19,11 @@ class HomePage extends StatelessWidget { BuildContext context, ) { return BlocBuilder( - builder: (context, greeting) => - TitleWidget(title: greeting, subtitle: 'O que vamos calcular?', invert: true), + builder: (context, greeting) => TitleWidget( + title: greeting, + subtitle: 'O que vamos calcular?', + invert: true, + ), ); } @@ -55,6 +59,7 @@ class HomePage extends StatelessWidget { } Widget _buildOpitionForms(BuildContext context, HomePageTheme theme) { + final brightness = Theme.of(context).brightness; return Column( spacing: AppSpacing.md, children: [ @@ -63,7 +68,10 @@ class HomePage extends StatelessWidget { title: 'Financiamento', description: 'Prestações com juros compostos', cardColor: theme.cardColor, - iconColor: theme.iconContainerColor, + iconColor: CalculatorPalette.of( + CalculatorKind.financing, + brightness: brightness, + ).solid, onTap: () => Modular.to.pushNamed(AppRoutes.financing), variant: MenuCardVariant.featured, ), @@ -77,7 +85,10 @@ class HomePage extends StatelessWidget { title: 'Depósitos Regulares', description: 'Prestações com juros compostos', cardColor: theme.cardColor, - iconColor: theme.iconContainerColor, + iconColor: CalculatorPalette.of( + CalculatorKind.deposits, + brightness: brightness, + ).solid, onTap: () => Modular.to.pushNamed(AppRoutes.deposits), variant: MenuCardVariant.standard, ), @@ -93,7 +104,10 @@ class HomePage extends StatelessWidget { title: 'Valor Futuro', description: 'Prestações com juros compostos', cardColor: theme.cardColor, - iconColor: theme.iconContainerColor, + iconColor: CalculatorPalette.of( + CalculatorKind.futureValue, + brightness: brightness, + ).solid, onTap: () => Modular.to.pushNamed(AppRoutes.futureValue), variant: MenuCardVariant.standard, ), @@ -106,7 +120,10 @@ class HomePage extends StatelessWidget { title: 'Correção de Valores', description: 'Atualize valores por índices de inflação', cardColor: theme.cardColor, - iconColor: theme.iconContainerColor, + iconColor: CalculatorPalette.of( + CalculatorKind.correction, + brightness: brightness, + ).solid, onTap: () => Modular.to.pushNamed(AppRoutes.correction), variant: MenuCardVariant.standard, ), @@ -118,7 +135,7 @@ class HomePage extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context).extension()!; final spacing = 4; - + return Scaffold( body: SafeArea( child: SingleChildScrollView( @@ -127,7 +144,7 @@ class HomePage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, spacing: AppSpacing.xl, children: [ - SizedBox(height: AppSpacing.xs- spacing), + SizedBox(height: AppSpacing.xs - spacing), _buildGreetingWidget(context), _buildMetricCard(context), _buildOpitionForms(context, theme), diff --git a/lib/modules/value_correction/module.dart b/lib/modules/value_correction/module.dart index a65bab2..242a851 100644 --- a/lib/modules/value_correction/module.dart +++ b/lib/modules/value_correction/module.dart @@ -1,6 +1,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:real_calc/core/module.dart'; +import 'package:real_calc/core/themes/calculator_palette.dart'; +import 'package:real_calc/core/themes/calculator_scope.dart'; import '../../core/routes/app_routes.dart'; import 'domain/entites/value_correction.dart'; @@ -89,14 +91,20 @@ class ValueCorrectionModule extends Module { ), BlocProvider(create: (_) => Modular.get()), ], - child: const ValueCorrectionPage(), + child: const CalculatorScope( + kind: CalculatorKind.correction, + child: ValueCorrectionPage(), + ), ), ); r.child( '/${AppRoutes.correctionResultSegment}', child: (_) { final result = Modular.args.data as ValueCorrection; - return ValueCorrectionResultPage(result: result); + return CalculatorScope( + kind: CalculatorKind.correction, + child: ValueCorrectionResultPage(result: result), + ); }, ); } diff --git a/test/core/themes/calculator_palette_test.dart b/test/core/themes/calculator_palette_test.dart new file mode 100644 index 0000000..75dc76b --- /dev/null +++ b/test/core/themes/calculator_palette_test.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:real_calc/core/themes/calculator_palette.dart'; +import 'package:real_calc/core/themes/calculator_scope.dart'; + +void main() { + group('CalculatorPalette', () { + test('defines the expected colors for light and dark themes', () { + const expected = { + CalculatorKind.financing: (0xFF364FC7, 0xFF364FC7, 0xFF91A7FF), + CalculatorKind.deposits: (0xFF087F5B, 0xFF0A6E50, 0xFF63E6BE), + CalculatorKind.futureValue: (0xFF6741D9, 0xFF6741D9, 0xFFB197FC), + CalculatorKind.correction: (0xFFB85300, 0xFFA04600, 0xFFFFA94D), + }; + + for (final entry in expected.entries) { + final light = CalculatorPalette.of( + entry.key, + brightness: Brightness.light, + ); + final dark = CalculatorPalette.of( + entry.key, + brightness: Brightness.dark, + ); + expect(light.solid.toARGB32(), entry.value.$1); + expect(light.text.toARGB32(), entry.value.$2); + expect(dark.solid, light.solid); + expect(dark.text.toARGB32(), entry.value.$3); + expect(light.onSolid, Colors.white); + expect(dark.onSolid, Colors.white); + } + }); + + test('compares palettes by value', () { + expect( + CalculatorPalette.of(CalculatorKind.deposits), + CalculatorPalette.of(CalculatorKind.deposits), + ); + expect( + CalculatorPalette.of(CalculatorKind.deposits).hashCode, + CalculatorPalette.of(CalculatorKind.deposits).hashCode, + ); + expect( + CalculatorPalette.of(CalculatorKind.deposits), + isNot( + CalculatorPalette.of( + CalculatorKind.deposits, + brightness: Brightness.dark, + ), + ), + ); + }); + + test('white text has at least 4.5:1 contrast on every solid color', () { + for (final kind in CalculatorKind.values) { + final palette = CalculatorPalette.of(kind); + expect( + (palette.onSolid.computeLuminance() + 0.05) / + (palette.solid.computeLuminance() + 0.05), + greaterThanOrEqualTo(4.5), + reason: '$kind must meet WCAG AA contrast', + ); + } + }); + }); + + testWidgets('correction scope applies its solid color to the text cursor', ( + tester, + ) async { + await tester.pumpWidget( + MaterialApp( + theme: ThemeData.light(), + home: const CalculatorScope( + kind: CalculatorKind.correction, + child: Scaffold(body: TextField()), + ), + ), + ); + + final editableText = tester.widget(find.byType(EditableText)); + expect(editableText.cursorColor, const Color(0xFFB85300)); + expect( + CalculatorScope.of(tester.element(find.byType(TextField))).text, + const Color(0xFFA04600), + ); + }); +} From ab2d3d9a5f1bd1782abc3f47b19569c7d15ef2b6 Mon Sep 17 00:00:00 2001 From: Rodrigo Mamede <61851910+Rodrigosdk@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:41:33 -0300 Subject: [PATCH 05/22] feat: alternar e persistir o tema do app (#108) * feat: adicionar ThemeCubit para alternar e persistir o tema com shared_preferences (#108) * feat: aplicar o ThemeCubit no AppWidget e ler a escolha antes do primeiro frame (#108) * feat: adicionar ThemeToggleButton e exibi-lo na tela inicial em modo debug (#108) --- lib/core/themes/theme_cubit.dart | 33 ++++++++ lib/core/widgets/theme_toggle_button.dart | 42 ++++++++++ lib/main.dart | 26 ++++--- lib/modules/app_widget.dart | 46 ++++++----- lib/modules/home/presentation/page/page.dart | 20 ++++- pubspec.lock | 60 +++++++++++++- pubspec.yaml | 1 + test/core/themes/theme_cubit_test.dart | 65 ++++++++++++++++ .../widgets/theme_toggle_button_test.dart | 78 +++++++++++++++++++ test/modules/app_widget_test.dart | 56 +++++++++---- 10 files changed, 376 insertions(+), 51 deletions(-) create mode 100644 lib/core/themes/theme_cubit.dart create mode 100644 lib/core/widgets/theme_toggle_button.dart create mode 100644 test/core/themes/theme_cubit_test.dart create mode 100644 test/core/widgets/theme_toggle_button_test.dart diff --git a/lib/core/themes/theme_cubit.dart b/lib/core/themes/theme_cubit.dart new file mode 100644 index 0000000..53ca4c1 --- /dev/null +++ b/lib/core/themes/theme_cubit.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Guarda e persiste o [ThemeMode] escolhido pelo usuário. +class ThemeCubit extends Cubit { + static const storageKey = 'theme_mode'; + + /// Usado enquanto as telas são migradas para os dois temas. A #125 troca + /// para [ThemeMode.system] quando todas estiverem prontas. + static const initialMode = ThemeMode.dark; + + final SharedPreferences _preferences; + + ThemeCubit(this._preferences) : super(_read(_preferences)); + + static ThemeMode _read(SharedPreferences preferences) { + final stored = preferences.getString(storageKey); + return ThemeMode.values.firstWhere( + (mode) => mode.name == stored, + orElse: () => initialMode, + ); + } + + Future setMode(ThemeMode mode) async { + emit(mode); + await _preferences.setString(storageKey, mode.name); + } + + /// Alterna entre claro e escuro a partir do brilho que está na tela. + Future toggle(Brightness current) => + setMode(current == Brightness.dark ? ThemeMode.light : ThemeMode.dark); +} diff --git a/lib/core/widgets/theme_toggle_button.dart b/lib/core/widgets/theme_toggle_button.dart new file mode 100644 index 0000000..a9f2919 --- /dev/null +++ b/lib/core/widgets/theme_toggle_button.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:real_calc/core/themes/theme_cubit.dart'; + +/// Botão que alterna entre o tema claro e o escuro. +/// +/// Mostra a lua no tema claro e o sol no escuro, sem fundo. A cor padrão é +/// branca, pensada para a faixa colorida da tela inicial (#119). +class ThemeToggleButton extends StatelessWidget { + static const double touchSize = 44; + + final Color color; + + const ThemeToggleButton({super.key, this.color = Colors.white}); + + @override + Widget build(BuildContext context) { + final brightness = Theme.of(context).brightness; + final isDark = brightness == Brightness.dark; + final label = isDark + ? 'Mudar para o modo claro' + : 'Mudar para o modo escuro'; + + return MergeSemantics( + child: Semantics( + label: label, + child: SizedBox.square( + dimension: touchSize, + child: IconButton( + onPressed: () => context.read().toggle(brightness), + padding: EdgeInsets.zero, + color: color, + icon: Icon( + isDark ? Icons.light_mode_outlined : Icons.dark_mode_outlined, + size: 22, + ), + ), + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 58db7a0..5ed19b3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:intl/date_symbol_data_local.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'core/themes/theme_cubit.dart'; import 'modules/app_module.dart'; import 'modules/app_widget.dart'; @@ -10,21 +14,21 @@ import 'modules/app_widget.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); LicenseRegistry.addLicense(() async* { - yield LicenseEntryWithLineBreaks( - ['Manrope'], - await rootBundle.loadString('assets/fonts/Manrope-OFL.txt'), - ); - yield LicenseEntryWithLineBreaks( - ['Bricolage Grotesque'], - await rootBundle.loadString('assets/fonts/Bricolage-Grotesque-OFL.txt'), - ); + yield LicenseEntryWithLineBreaks([ + 'Manrope', + ], await rootBundle.loadString('assets/fonts/Manrope-OFL.txt')); + yield LicenseEntryWithLineBreaks([ + 'Bricolage Grotesque', + ], await rootBundle.loadString('assets/fonts/Bricolage-Grotesque-OFL.txt')); }); await initializeDateFormatting('pt_BR', null); + // Lido antes do primeiro frame para o app já abrir no tema escolhido. + final preferences = await SharedPreferences.getInstance(); runApp( - ModularApp( - module: AppModule(), - child: const AppWidget(), + BlocProvider( + create: (_) => ThemeCubit(preferences), + child: ModularApp(module: AppModule(), child: const AppWidget()), ), ); } diff --git a/lib/modules/app_widget.dart b/lib/modules/app_widget.dart index 7f0aece..0b82b8a 100644 --- a/lib/modules/app_widget.dart +++ b/lib/modules/app_widget.dart @@ -1,37 +1,41 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_modular/flutter_modular.dart'; import '../core/themes/app_colors.dart'; import '../core/themes/app_theme.dart'; +import '../core/themes/theme_cubit.dart'; class AppWidget extends StatelessWidget { const AppWidget({super.key}); @override Widget build(BuildContext context) { - return MaterialApp.router( - theme: AppTheme.lightTheme, - darkTheme: AppTheme.darkTheme, - themeMode: ThemeMode.dark, - builder: (context, child) { - final colors = - Theme.of(context).extension() ?? AppColors.dark; - return AnnotatedRegion( - value: AppTheme.systemUiOverlayStyle(colors), - child: child ?? const SizedBox.shrink(), - ); - }, - routeInformationParser: Modular.routeInformationParser, - routerDelegate: Modular.routerDelegate, - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [Locale('pt', 'BR')], - locale: const Locale('pt', 'BR'), + return BlocBuilder( + builder: (context, themeMode) => MaterialApp.router( + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + themeMode: themeMode, + builder: (context, child) { + final colors = + Theme.of(context).extension() ?? AppColors.dark; + return AnnotatedRegion( + value: AppTheme.systemUiOverlayStyle(colors), + child: child ?? const SizedBox.shrink(), + ); + }, + routeInformationParser: Modular.routeInformationParser, + routerDelegate: Modular.routerDelegate, + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('pt', 'BR')], + locale: const Locale('pt', 'BR'), + ), ); } } diff --git a/lib/modules/home/presentation/page/page.dart b/lib/modules/home/presentation/page/page.dart index 023d0ee..b5ea448 100644 --- a/lib/modules/home/presentation/page/page.dart +++ b/lib/modules/home/presentation/page/page.dart @@ -1,9 +1,11 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:real_calc/core/routes/app_routes.dart'; import 'package:real_calc/core/themes/calculator_palette.dart'; import 'package:real_calc/core/themes/spacing.dart'; +import 'package:real_calc/core/widgets/theme_toggle_button.dart'; import 'package:real_calc/core/widgets/title_widget.dart'; import 'package:real_calc/modules/home/domain/enum/menu_card_variant.dart'; import '../../../../core/themes/extensions/home_page_theme.dart'; @@ -15,16 +17,26 @@ import '../cubit/selic_cubit.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); - BlocBuilder _buildGreetingWidget( - BuildContext context, - ) { - return BlocBuilder( + Widget _buildGreetingWidget(BuildContext context) { + final greeting = BlocBuilder( builder: (context, greeting) => TitleWidget( title: greeting, subtitle: 'O que vamos calcular?', invert: true, ), ); + + // Provisório até a #119 levar o botão para a faixa da tela inicial: + // só aparece em debug, para testar as telas já migradas. + if (!kDebugMode) return greeting; + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: greeting), + ThemeToggleButton(color: Theme.of(context).colorScheme.onSurface), + ], + ); } BlocBuilder _buildMetricCard(BuildContext context) { diff --git a/pubspec.lock b/pubspec.lock index 3ba6073..9ce21f3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -621,6 +621,62 @@ packages: url: "https://pub.dev" source: hosted version: "7.2.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "1e85e0dccf5b4a016077fea7dfbb4a7cb39c759ac1596fcf60c06deadb7da91f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "1e12aafe408aa50da80edfd679a2a6bf63ba7ab37c7fa98286da459a757b3399" + url: "https://pub.dev" + source: hosted + version: "2.4.28" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "2ec3934efa51e46117f23031cc141b8fc878e8525b94ec1ea4f7f586cf1b47ea" + url: "https://pub.dev" + source: hosted + version: "2.5.7" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" shelf: dependency: transitive description: @@ -899,5 +955,5 @@ packages: source: hosted version: "3.1.4" sdks: - dart: ">=3.11.0 <4.0.0" - flutter: ">=3.38.4" + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml index 391a80e..e493a8c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: dio: ^5.11.1 flutter_svg: ^2.3.0 share_plus: ^13.3.1 + shared_preferences: ^2.5.6 dev_dependencies: flutter_test: diff --git a/test/core/themes/theme_cubit_test.dart b/test/core/themes/theme_cubit_test.dart new file mode 100644 index 0000000..0881b3c --- /dev/null +++ b/test/core/themes/theme_cubit_test.dart @@ -0,0 +1,65 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:real_calc/core/themes/theme_cubit.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + Future preferencesWith(Map values) { + SharedPreferences.setMockInitialValues(values); + return SharedPreferences.getInstance(); + } + + group('ThemeCubit', () { + test('sem escolha gravada, começa no valor inicial (escuro)', () async { + final cubit = ThemeCubit(await preferencesWith({})); + + expect(cubit.state, ThemeCubit.initialMode); + expect(cubit.state, ThemeMode.dark); + }); + + test('lê a escolha gravada', () async { + final cubit = ThemeCubit( + await preferencesWith({ThemeCubit.storageKey: 'light'}), + ); + + expect(cubit.state, ThemeMode.light); + }); + + test('valor gravado desconhecido volta para o valor inicial', () async { + final cubit = ThemeCubit( + await preferencesWith({ThemeCubit.storageKey: 'sepia'}), + ); + + expect(cubit.state, ThemeCubit.initialMode); + }); + + test('grava a escolha e ela continua ao recriar o cubit', () async { + final preferences = await preferencesWith({}); + + await ThemeCubit(preferences).setMode(ThemeMode.system); + + expect(preferences.getString(ThemeCubit.storageKey), 'system'); + expect(ThemeCubit(preferences).state, ThemeMode.system); + }); + + late SharedPreferences preferences; + + blocTest( + 'toggle no escuro muda para o claro', + setUp: () async => preferences = await preferencesWith({}), + build: () => ThemeCubit(preferences), + act: (cubit) => cubit.toggle(Brightness.dark), + expect: () => [ThemeMode.light], + ); + + blocTest( + 'toggle no claro muda para o escuro', + setUp: () async => + preferences = await preferencesWith({ThemeCubit.storageKey: 'light'}), + build: () => ThemeCubit(preferences), + act: (cubit) => cubit.toggle(Brightness.light), + expect: () => [ThemeMode.dark], + ); + }); +} diff --git a/test/core/widgets/theme_toggle_button_test.dart b/test/core/widgets/theme_toggle_button_test.dart new file mode 100644 index 0000000..0cdf20d --- /dev/null +++ b/test/core/widgets/theme_toggle_button_test.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:real_calc/core/themes/theme_cubit.dart'; +import 'package:real_calc/core/widgets/theme_toggle_button.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + late ThemeCubit cubit; + + setUp(() async { + SharedPreferences.setMockInitialValues({}); + cubit = ThemeCubit(await SharedPreferences.getInstance()); + }); + + tearDown(() => cubit.close()); + + Widget app() => BlocProvider.value( + value: cubit, + child: BlocBuilder( + builder: (context, mode) => MaterialApp( + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + themeMode: mode, + home: const Scaffold(body: Center(child: ThemeToggleButton())), + ), + ), + ); + + group('ThemeToggleButton', () { + testWidgets('no escuro mostra o sol e o rótulo para o modo claro', ( + tester, + ) async { + final semantics = tester.ensureSemantics(); + await tester.pumpWidget(app()); + + expect(find.byIcon(Icons.light_mode_outlined), findsOneWidget); + expect( + tester.getSemantics(find.byType(ThemeToggleButton)), + isSemantics( + label: 'Mudar para o modo claro', + isButton: true, + hasTapAction: true, + ), + ); + semantics.dispose(); + }); + + testWidgets('o toque troca o tema na hora e grava a escolha', ( + tester, + ) async { + await tester.pumpWidget(app()); + + await tester.tap(find.byType(ThemeToggleButton)); + await tester.pumpAndSettle(); + + expect(cubit.state, ThemeMode.light); + expect( + Theme.of(tester.element(find.byType(IconButton))).brightness, + Brightness.light, + ); + expect(find.byIcon(Icons.dark_mode_outlined), findsOneWidget); + expect(find.bySemanticsLabel('Mudar para o modo escuro'), findsOneWidget); + + final preferences = await SharedPreferences.getInstance(); + expect(preferences.getString(ThemeCubit.storageKey), 'light'); + }); + + testWidgets('tem área de toque de 44x44', (tester) async { + await tester.pumpWidget(app()); + + expect( + tester.getSize(find.byType(ThemeToggleButton)), + const Size.square(44), + ); + }); + }); +} diff --git a/test/modules/app_widget_test.dart b/test/modules/app_widget_test.dart index 408b2c3..5ec2b4f 100644 --- a/test/modules/app_widget_test.dart +++ b/test/modules/app_widget_test.dart @@ -1,27 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:real_calc/core/themes/theme_cubit.dart'; import 'package:real_calc/modules/app_module.dart'; import 'package:real_calc/modules/app_widget.dart'; import 'package:real_calc/modules/home/presentation/page/page.dart'; +import 'package:shared_preferences/shared_preferences.dart'; void main() { + late ThemeCubit themeCubit; + + Future buildApp() async { + SharedPreferences.setMockInitialValues({}); + themeCubit = ThemeCubit(await SharedPreferences.getInstance()); + return BlocProvider.value( + value: themeCubit, + child: ModularApp(module: AppModule(), child: const AppWidget()), + ); + } + tearDown(() { Modular.destroy(); + themeCubit.close(); }); group('AppWidget & AppModule - Integração de Rotas', () { - testWidgets('Deve inicializar o AppWidget e carregar a HomePage na rota padrão', (tester) async { - await tester.pumpWidget( - ModularApp( - module: AppModule(), - child: const AppWidget(), - ), - ); - - // Aguarda a resolução assíncrona das rotas e animações iniciais - await tester.pumpAndSettle(); - - expect(find.byType(HomePage), findsOneWidget); - }); + testWidgets( + 'Deve inicializar o AppWidget e carregar a HomePage na rota padrão', + (tester) async { + await tester.pumpWidget(await buildApp()); + + // Aguarda a resolução assíncrona das rotas e animações iniciais + await tester.pumpAndSettle(); + + expect(find.byType(HomePage), findsOneWidget); + + // O tema fica no mesmo teste: o Modular não reinicializa bem dois + // ModularApp no mesmo arquivo. + ThemeMode appThemeMode() => + tester.widget(find.byType(MaterialApp)).themeMode!; + + expect(appThemeMode(), ThemeMode.dark); + + await themeCubit.setMode(ThemeMode.light); + await tester.pumpAndSettle(); + + expect(appThemeMode(), ThemeMode.light); + expect( + Theme.of(tester.element(find.byType(HomePage))).brightness, + Brightness.light, + ); + }, + ); }); } From 72b1fe5516bbde7eb856dfa29415942116cf643d Mon Sep 17 00:00:00 2001 From: Rodrigo Mamede <61851910+Rodrigosdk@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:44:09 -0300 Subject: [PATCH 06/22] feat: criar lista de campos (FieldList e FieldRow) (#110) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: renomear InputFieldState.highlighted para unknown (#110) * feat: adicionar FieldList e FieldRow com os estados neutro, edição, a descobrir, calculado, erro e seleção (#110) * test: cobrir estados, toque, teclado, fonte em 130% e Semantics do FieldRow (#110) --- lib/core/themes/app_colors.dart | 7 + lib/core/widgets/field_list/field_list.dart | 26 ++ lib/core/widgets/field_list/field_row.dart | 380 +++++++++++++++++ lib/core/widgets/input_forms_result_card.dart | 12 +- lib/core/widgets/types/input_field_state.dart | 2 +- .../cubit/forms/financing_form_cubit.dart | 2 +- .../presentation/pages/financing_page.dart | 2 +- .../presentation/pages/future_value_page.dart | 2 +- .../pages/regular_deposits_page.dart | 2 +- .../widgets/field_list/field_row_test.dart | 402 ++++++++++++++++++ .../widgets/input_forms_result_card_test.dart | 22 +- .../forms/financing_form_cubit_test.dart | 2 +- .../presentation/widgets/forms_view_test.dart | 2 +- 13 files changed, 839 insertions(+), 24 deletions(-) create mode 100644 lib/core/widgets/field_list/field_list.dart create mode 100644 lib/core/widgets/field_list/field_row.dart create mode 100644 test/core/widgets/field_list/field_row_test.dart diff --git a/lib/core/themes/app_colors.dart b/lib/core/themes/app_colors.dart index cbccf7d..423a093 100644 --- a/lib/core/themes/app_colors.dart +++ b/lib/core/themes/app_colors.dart @@ -63,6 +63,13 @@ class AppColors extends ThemeExtension { onCalc: Color(0xFFFFFFFF), ); + /// Cores do tema atual; sem a extensão registrada, escolhe pelo brilho. + static AppColors of(BuildContext context) { + final theme = Theme.of(context); + return theme.extension() ?? + (theme.brightness == Brightness.dark ? dark : light); + } + @override AppColors copyWith({ Color? background, diff --git a/lib/core/widgets/field_list/field_list.dart b/lib/core/widgets/field_list/field_list.dart new file mode 100644 index 0000000..2d81655 --- /dev/null +++ b/lib/core/widgets/field_list/field_list.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:real_calc/core/themes/app_colors.dart'; + +/// Lista de campos: uma coluna com linha `hairline` no topo, sem cartão, +/// sombra ou ícones. Cada [FieldRow] desenha a própria linha de baixo. +class FieldList extends StatelessWidget { + final List children; + + const FieldList({super.key, required this.children}); + + @override + Widget build(BuildContext context) { + final colors = AppColors.of(context); + + return DecoratedBox( + decoration: BoxDecoration( + border: Border(top: BorderSide(color: colors.hairline)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: children, + ), + ); + } +} diff --git a/lib/core/widgets/field_list/field_row.dart b/lib/core/widgets/field_list/field_row.dart new file mode 100644 index 0000000..e5dbfa6 --- /dev/null +++ b/lib/core/widgets/field_list/field_row.dart @@ -0,0 +1,380 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:real_calc/core/themes/app_colors.dart'; +import 'package:real_calc/core/themes/calculator_palette.dart'; +import 'package:real_calc/core/themes/calculator_scope.dart'; +import 'package:real_calc/core/themes/radius.dart'; +import 'package:real_calc/core/themes/spacing.dart'; +import 'package:real_calc/core/themes/text_styles.dart'; + +import '../types/input_field_state.dart'; + +/// Linha de um campo da [FieldList]. +/// +/// À esquerda ficam o rótulo e uma segunda linha opcional; à direita, sem +/// quebra, o prefixo, o valor e a unidade. A cor de destaque vem do +/// [CalculatorScope] mais próximo. +/// +/// Estados: +/// - neutro; +/// - em edição (com foco): linha de 2px e cursor na cor da calculadora; +/// - [InputFieldState.unknown]: "a descobrir" e pílula "Toque para calcular"; +/// - [InputFieldState.calculated]: "calculado para você", ✓ e valor em +/// destaque; +/// - [InputFieldState.error]: linha, valor e segunda linha em `error`; +/// - seleção ([FieldRow.selection]): valor e chevron, abre um seletor. +class FieldRow extends StatefulWidget { + static const unknownText = 'a descobrir'; + static const calculatedText = 'calculado para você'; + static const calculateText = 'Toque para calcular'; + + final String label; + + /// Segunda linha opcional, como "todo mês" ou "opcional". Nos estados + /// a descobrir e calculado é trocada pelo texto do estado. + final String? secondLine; + + /// Texto da segunda linha no estado de erro (ex.: "fora do período"). + final String? errorText; + + final InputFieldState state; + final TextEditingController? controller; + final FocusNode? focusNode; + final String? prefix; + final String? unit; + final String hint; + final TextInputType keyboardType; + final List? inputFormatters; + final ValueChanged? onChanged; + + /// Chamado ao tocar na linha no estado a descobrir. + final VoidCallback? onCalculate; + + /// Espaço mantido entre o campo em foco e o teclado. + final EdgeInsets scrollPadding; + + final String? _selectionValue; + final VoidCallback? _onSelect; + + const FieldRow({ + super.key, + required this.label, + required TextEditingController this.controller, + this.secondLine, + this.errorText, + this.state = InputFieldState.neutral, + this.focusNode, + this.prefix, + this.unit, + this.hint = '0', + this.keyboardType = const TextInputType.numberWithOptions(decimal: true), + this.inputFormatters, + this.onChanged, + this.onCalculate, + this.scrollPadding = const EdgeInsets.fromLTRB(20, 20, 20, 120), + }) : _selectionValue = null, + _onSelect = null; + + /// Linha de seleção (ex.: Índice): mostra o valor e um chevron e chama + /// [onTap] ao ser tocada. + const FieldRow.selection({ + super.key, + required this.label, + required String value, + required VoidCallback onTap, + this.secondLine, + this.errorText, + this.state = InputFieldState.neutral, + }) : _selectionValue = value, + _onSelect = onTap, + controller = null, + focusNode = null, + prefix = null, + unit = null, + hint = '', + keyboardType = TextInputType.none, + inputFormatters = null, + onChanged = null, + onCalculate = null, + scrollPadding = EdgeInsets.zero; + + bool get isSelection => _onSelect != null; + + @override + State createState() => _FieldRowState(); +} + +class _FieldRowState extends State { + /// Parte máxima da linha ocupada pelo bloco da direita; o resto fica para + /// o rótulo e a segunda linha, que quebram. + static const _maxTrailingFraction = 0.65; + + FocusNode? _ownFocusNode; + + FocusNode get _focusNode => + widget.focusNode ?? (_ownFocusNode ??= FocusNode()); + + @override + void initState() { + super.initState(); + _focusNode.addListener(_rebuild); + widget.controller?.addListener(_rebuild); + } + + @override + void didUpdateWidget(FieldRow oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.focusNode != widget.focusNode) { + (oldWidget.focusNode ?? _ownFocusNode)?.removeListener(_rebuild); + _focusNode.addListener(_rebuild); + } + if (oldWidget.controller != widget.controller) { + oldWidget.controller?.removeListener(_rebuild); + widget.controller?.addListener(_rebuild); + } + } + + @override + void dispose() { + _focusNode.removeListener(_rebuild); + widget.controller?.removeListener(_rebuild); + _ownFocusNode?.dispose(); + super.dispose(); + } + + void _rebuild() => setState(() {}); + + bool get _isUnknown => widget.state == InputFieldState.unknown; + bool get _isCalculated => widget.state == InputFieldState.calculated; + bool get _isError => widget.state == InputFieldState.error; + bool get _isEditing => + !widget.isSelection && !_isUnknown && _focusNode.hasFocus; + + void _handleTap() { + if (widget.isSelection) { + widget._onSelect!(); + } else if (_isUnknown) { + widget.onCalculate?.call(); + } else { + _focusNode.requestFocus(); + } + } + + CalculatorPalette _palette(BuildContext context) { + final scoped = CalculatorScope.maybeOf(context); + if (scoped != null) return scoped; + final scheme = Theme.of(context).colorScheme; + return CalculatorPalette( + solid: scheme.primary, + onSolid: scheme.onPrimary, + text: scheme.primary, + ); + } + + String? get _stateText { + if (_isError) return widget.errorText ?? widget.secondLine; + if (_isUnknown) return FieldRow.unknownText; + if (_isCalculated) return FieldRow.calculatedText; + return widget.secondLine; + } + + @override + Widget build(BuildContext context) { + final colors = AppColors.of(context); + final palette = _palette(context); + final secondText = _stateText; + + final (lineColor, lineWidth) = switch (true) { + _ when _isError => (colors.error, 1.5), + _ when _isUnknown || _isEditing => (palette.solid, 2.0), + _ => (colors.hairline, 1.0), + }; + + final secondStyle = (_isError || _isUnknown || _isCalculated) + ? AppTextStyles.rowState.copyWith( + color: _isError ? colors.error : palette.text, + ) + : AppTextStyles.caption.copyWith(color: colors.textSecondary); + + final row = GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _handleTap, + child: Container( + constraints: BoxConstraints( + minHeight: secondText == null + ? AppSpacing.rowHeight + : AppSpacing.rowHeightWithSecondLine, + ), + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: lineColor, width: lineWidth), + ), + ), + child: LayoutBuilder( + builder: (context, constraints) => Row( + children: [ + Expanded( + child: ExcludeSemantics( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.label, + style: AppTextStyles.rowLabel.copyWith( + color: colors.textPrimary, + ), + ), + if (secondText != null) ...[ + const SizedBox(height: 2), + Text(secondText, style: secondStyle), + ], + ], + ), + ), + ), + const SizedBox(width: AppSpacing.md), + // Valor sem quebra: se não couber (tela estreita e fonte grande), + // diminui inteiro em vez de ser cortado. + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: constraints.maxWidth * _maxTrailingFraction, + ), + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerRight, + child: _buildTrailing(context, colors, palette), + ), + ), + ], + ), + ), + ), + ); + + if (widget.isSelection || _isUnknown) { + final value = widget.isSelection + ? widget._selectionValue + : FieldRow.unknownText; + return Semantics( + container: true, + button: true, + label: [ + widget.label, + value, + if (widget.isSelection && secondText != null) secondText, + ].join(', '), + onTap: _handleTap, + excludeSemantics: true, + child: row, + ); + } + + // O TextField contribui com o valor; o rótulo junta nome, unidade e + // estado, na ordem em que o TalkBack deve ler. + return MergeSemantics( + child: Semantics( + label: [widget.label, ?widget.unit, ?secondText].join(', '), + child: row, + ), + ); + } + + Widget _buildTrailing( + BuildContext context, + AppColors colors, + CalculatorPalette palette, + ) { + if (widget.isSelection) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget._selectionValue!, + style: AppTextStyles.rowValue.copyWith( + color: _isError ? colors.error : colors.textPrimary, + ), + ), + const SizedBox(width: AppSpacing.xs), + Icon(Icons.chevron_right, size: 20, color: colors.textSecondary), + ], + ); + } + + if (_isUnknown) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7), + decoration: BoxDecoration( + color: palette.solid, + borderRadius: BorderRadius.circular(AppRadius.pill), + ), + child: Text( + FieldRow.calculateText, + style: AppTextStyles.pill.copyWith(color: palette.onSolid), + ), + ); + } + + final valueStyle = AppTextStyles.rowValue.copyWith( + color: switch (widget.state) { + InputFieldState.error => colors.error, + InputFieldState.calculated => palette.text, + _ => colors.textPrimary, + }, + fontWeight: _isCalculated ? FontWeight.w800 : null, + ); + final unitStyle = AppTextStyles.rowUnit.copyWith( + color: _isError ? colors.error : colors.textSecondary, + ); + + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + if (_isCalculated) ...[ + Icon(Icons.check, size: 16, color: palette.text), + const SizedBox(width: 6), + ], + if (widget.prefix != null) ...[ + ExcludeSemantics(child: Text(widget.prefix!, style: unitStyle)), + const SizedBox(width: AppSpacing.xs), + ], + // Largura do conteúdo (ou do hint), para prefixo, valor e unidade + // ficarem colados. + IntrinsicWidth( + child: TextField( + controller: widget.controller, + focusNode: _focusNode, + keyboardType: widget.keyboardType, + inputFormatters: widget.inputFormatters, + onChanged: widget.onChanged, + textAlign: TextAlign.end, + textInputAction: TextInputAction.done, + style: valueStyle, + cursorColor: palette.solid, + scrollPadding: widget.scrollPadding, + decoration: InputDecoration( + hintText: widget.hint, + hintStyle: valueStyle.copyWith(color: colors.textHint), + isCollapsed: true, + filled: false, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + ), + ), + ), + if (widget.unit != null) ...[ + const SizedBox(width: AppSpacing.xs), + ExcludeSemantics(child: Text(widget.unit!, style: unitStyle)), + ], + ], + ); + } +} diff --git a/lib/core/widgets/input_forms_result_card.dart b/lib/core/widgets/input_forms_result_card.dart index b8792e6..f2a2cc2 100644 --- a/lib/core/widgets/input_forms_result_card.dart +++ b/lib/core/widgets/input_forms_result_card.dart @@ -29,8 +29,8 @@ class InputFormsResultCard extends StatelessWidget { this.readOnly = false, this.trailingBadge, }); - bool get _isHighlightedAndEmptyText { - if (state == InputFieldState.highlighted && controller.text.isEmpty) { + bool get _isUnknownAndEmptyText { + if (state == InputFieldState.unknown && controller.text.isEmpty) { return true; } return false; @@ -39,14 +39,14 @@ class InputFormsResultCard extends StatelessWidget { Color _accentColor(InputFormsResultCardTheme theme) { return switch (state) { InputFieldState.neutral => theme.neutralIconColor, - InputFieldState.highlighted => theme.highlightedColor, + InputFieldState.unknown => theme.highlightedColor, InputFieldState.calculated => theme.calculatedColor, InputFieldState.error => theme.errorColor, }; } String? get _badgeText => switch (state) { - InputFieldState.highlighted => 'vazio', + InputFieldState.unknown => 'vazio', InputFieldState.calculated => 'calculado', InputFieldState.neutral || InputFieldState.error => null, }; @@ -54,7 +54,7 @@ class InputFormsResultCard extends StatelessWidget { String? get _resolvedBadgeText => trailingBadge ?? _badgeText; String get _effectiveHint { - if (_isHighlightedAndEmptyText) { + if (_isUnknownAndEmptyText) { return 'toque para calcular'; } return hint; @@ -68,7 +68,7 @@ class InputFormsResultCard extends StatelessWidget { final String? badgeText = _resolvedBadgeText; final String? errorText = validator?.call(controller.text); - final bool isHintMessage = _isHighlightedAndEmptyText == true; + final bool isHintMessage = _isUnknownAndEmptyText == true; return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), diff --git a/lib/core/widgets/types/input_field_state.dart b/lib/core/widgets/types/input_field_state.dart index aa2d812..5f46ffe 100644 --- a/lib/core/widgets/types/input_field_state.dart +++ b/lib/core/widgets/types/input_field_state.dart @@ -1 +1 @@ -enum InputFieldState { neutral, highlighted, calculated, error } \ No newline at end of file +enum InputFieldState { neutral, unknown, calculated, error } \ No newline at end of file diff --git a/lib/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit.dart b/lib/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit.dart index ed1498f..af46179 100644 --- a/lib/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit.dart +++ b/lib/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit.dart @@ -86,7 +86,7 @@ class FinancingFormCubit extends Cubit { return InputFieldState.neutral; } - return InputFieldState.highlighted; + return InputFieldState.unknown; } void _setFormattedValue( diff --git a/lib/modules/financial_calculators/presentation/pages/financing_page.dart b/lib/modules/financial_calculators/presentation/pages/financing_page.dart index e4bd7d6..d37afe4 100644 --- a/lib/modules/financial_calculators/presentation/pages/financing_page.dart +++ b/lib/modules/financial_calculators/presentation/pages/financing_page.dart @@ -80,7 +80,7 @@ class FinancingPage extends StatelessWidget { onCalculate: calculate, onClear: formCubit.clear, onFieldTap: (field) { - if (formState.stateOf(field) == InputFieldState.highlighted) { + if (formState.stateOf(field) == InputFieldState.unknown) { calculate(); } }, diff --git a/lib/modules/financial_calculators/presentation/pages/future_value_page.dart b/lib/modules/financial_calculators/presentation/pages/future_value_page.dart index cae86aa..8f7e679 100644 --- a/lib/modules/financial_calculators/presentation/pages/future_value_page.dart +++ b/lib/modules/financial_calculators/presentation/pages/future_value_page.dart @@ -80,7 +80,7 @@ class FutureValuePage extends StatelessWidget { onCalculate: calculate, onClear: formCubit.clear, onFieldTap: (field) { - if (formState.stateOf(field) == InputFieldState.highlighted) { + if (formState.stateOf(field) == InputFieldState.unknown) { calculate(); } }, diff --git a/lib/modules/financial_calculators/presentation/pages/regular_deposits_page.dart b/lib/modules/financial_calculators/presentation/pages/regular_deposits_page.dart index 489d62a..161f7a4 100644 --- a/lib/modules/financial_calculators/presentation/pages/regular_deposits_page.dart +++ b/lib/modules/financial_calculators/presentation/pages/regular_deposits_page.dart @@ -81,7 +81,7 @@ class RegularDepositsPage extends StatelessWidget { onCalculate: calculate, onClear: formCubit.clear, onFieldTap: (field) { - if (formState.stateOf(field) == InputFieldState.highlighted) { + if (formState.stateOf(field) == InputFieldState.unknown) { calculate(); } }, diff --git a/test/core/widgets/field_list/field_row_test.dart b/test/core/widgets/field_list/field_row_test.dart new file mode 100644 index 0000000..238bbd9 --- /dev/null +++ b/test/core/widgets/field_list/field_row_test.dart @@ -0,0 +1,402 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:real_calc/core/themes/app_colors.dart'; +import 'package:real_calc/core/themes/app_theme.dart'; +import 'package:real_calc/core/themes/calculator_palette.dart'; +import 'package:real_calc/core/themes/calculator_scope.dart'; +import 'package:real_calc/core/widgets/field_list/field_list.dart'; +import 'package:real_calc/core/widgets/field_list/field_row.dart'; +import 'package:real_calc/core/widgets/types/input_field_state.dart'; + +void main() { + const colors = AppColors.light; + final palette = CalculatorPalette.of(CalculatorKind.correction); + + late TextEditingController controller; + + setUp(() => controller = TextEditingController()); + tearDown(() => controller.dispose()); + + Future pump( + WidgetTester tester, + Widget row, { + double width = 400, + double textScale = 1, + }) async { + tester.view.physicalSize = Size(width, 800); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.lightTheme, + home: MediaQuery( + data: MediaQueryData( + size: Size(width, 800), + textScaler: TextScaler.linear(textScale), + ), + child: Scaffold( + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: CalculatorScope( + kind: CalculatorKind.correction, + child: FieldList(children: [row]), + ), + ), + ), + ), + ), + ); + } + + BorderSide bottomLine(WidgetTester tester) { + final container = tester.widget( + find + .descendant( + of: find.byType(FieldRow), + matching: find.byType(Container), + ) + .first, + ); + return ((container.decoration! as BoxDecoration).border! as Border).bottom; + } + + TextStyle valueStyle(WidgetTester tester) => + tester.widget(find.byType(EditableText)).style; + + group('FieldRow', () { + testWidgets('neutro: rótulo, segunda linha e linha hairline', ( + tester, + ) async { + controller.text = '1.200,00'; + await pump( + tester, + FieldRow( + label: 'Depósito', + secondLine: 'todo mês', + prefix: 'R\$', + controller: controller, + ), + ); + + expect(find.text('Depósito'), findsOneWidget); + expect(find.text('todo mês'), findsOneWidget); + expect(find.text('R\$'), findsOneWidget); + expect(bottomLine(tester).color, colors.hairline); + expect(bottomLine(tester).width, 1); + expect(valueStyle(tester).color, colors.textPrimary); + expect(find.byIcon(Icons.check), findsNothing); + expect( + tester.getSize(find.byType(FieldRow)).height, + greaterThanOrEqualTo(64), + ); + }); + + testWidgets('sem segunda linha, tem altura mínima de 58', (tester) async { + await pump(tester, FieldRow(label: 'Taxa', controller: controller)); + + expect( + tester.getSize(find.byType(FieldRow)).height, + greaterThanOrEqualTo(58), + ); + // Vazio, o campo ocupa a largura do hint "0". + expect(find.text('0'), findsOneWidget); + expect( + tester.getSize(find.byType(TextField)).width, + greaterThanOrEqualTo(tester.getSize(find.text('0')).width), + ); + }); + + testWidgets('em edição: tocar na linha foca o campo, linha de 2px e ' + 'cursor na cor da calculadora', (tester) async { + await pump( + tester, + FieldRow(label: 'Valor', prefix: 'R\$', controller: controller), + ); + + // Toca no rótulo, longe do campo: a linha inteira é área de toque. + await tester.tap(find.text('Valor')); + await tester.pump(); + + final editable = tester.widget(find.byType(EditableText)); + expect(editable.focusNode.hasFocus, isTrue); + expect(editable.cursorColor, palette.solid); + expect(bottomLine(tester).color, palette.solid); + expect(bottomLine(tester).width, 2); + }); + + testWidgets('a descobrir: texto, pílula, linha e toque para calcular', ( + tester, + ) async { + var calculated = 0; + await pump( + tester, + FieldRow( + label: 'Prestação', + controller: controller, + state: InputFieldState.unknown, + onCalculate: () => calculated++, + ), + ); + + expect(find.text(FieldRow.unknownText), findsOneWidget); + expect(find.text(FieldRow.calculateText), findsOneWidget); + expect(find.byType(TextField), findsNothing); + expect(bottomLine(tester).color, palette.solid); + expect(bottomLine(tester).width, 2); + + final pill = tester.widget(find.text(FieldRow.calculateText)); + expect(pill.style!.color, palette.onSolid); + final pillBox = tester.widget( + find + .ancestor( + of: find.text(FieldRow.calculateText), + matching: find.byType(Container), + ) + .first, + ); + expect((pillBox.decoration! as BoxDecoration).color, palette.solid); + + await tester.tap(find.text('Prestação')); + expect(calculated, 1); + }); + + testWidgets('calculado: texto, ✓ e valor em destaque com peso 800', ( + tester, + ) async { + controller.text = '1.523,42'; + await pump( + tester, + FieldRow( + label: 'Prestação', + prefix: 'R\$', + controller: controller, + state: InputFieldState.calculated, + ), + ); + + expect(find.text(FieldRow.calculatedText), findsOneWidget); + expect( + tester.widget(find.text(FieldRow.calculatedText)).style!.color, + palette.text, + ); + expect(tester.widget(find.byIcon(Icons.check)).color, palette.text); + expect(valueStyle(tester).color, palette.text); + expect(valueStyle(tester).fontWeight, FontWeight.w800); + expect(bottomLine(tester).color, colors.hairline); + }); + + testWidgets('erro: linha de 1,5px, valor e segunda linha em error', ( + tester, + ) async { + controller.text = '01/1900'; + await pump( + tester, + FieldRow( + label: 'Data inicial', + controller: controller, + state: InputFieldState.error, + errorText: 'fora do período', + ), + ); + + expect(find.text('fora do período'), findsOneWidget); + expect( + tester.widget(find.text('fora do período')).style!.color, + colors.error, + ); + expect(valueStyle(tester).color, colors.error); + expect(bottomLine(tester).color, colors.error); + expect(bottomLine(tester).width, 1.5); + }); + + testWidgets('seleção: valor, chevron e toque abre o seletor', ( + tester, + ) async { + var opened = 0; + await pump( + tester, + FieldRow.selection( + label: 'Índice', + value: 'IPCA', + onTap: () => opened++, + ), + ); + + expect(find.text('IPCA'), findsOneWidget); + expect(find.byIcon(Icons.chevron_right), findsOneWidget); + expect(find.byType(TextField), findsNothing); + + await tester.tap(find.text('Índice')); + expect(opened, 1); + }); + + testWidgets('prefixo, valor e unidade ficam colados e na borda', ( + tester, + ) async { + controller.text = '12'; + await pump( + tester, + FieldRow(label: 'Prazo', unit: 'meses', controller: controller), + ); + + final field = tester.getRect(find.byType(TextField)); + final unit = tester.getRect(find.text('meses')); + final row = tester.getRect(find.byType(FieldRow)); + + expect(unit.left - field.right, 4); + expect(unit.right, row.right); + }); + + testWidgets('usa o teclado informado e mantém espaço acima do teclado', ( + tester, + ) async { + await pump( + tester, + FieldRow( + label: 'Prazo', + controller: controller, + keyboardType: TextInputType.number, + ), + ); + + final field = tester.widget(find.byType(TextField)); + expect(field.keyboardType, TextInputType.number); + expect(field.scrollPadding.bottom, greaterThanOrEqualTo(100)); + + await pump(tester, FieldRow(label: 'Taxa', controller: controller)); + expect( + tester.widget(find.byType(TextField)).keyboardType, + const TextInputType.numberWithOptions(decimal: true), + ); + }); + + testWidgets('em 360dp e fonte em 130%, a segunda linha quebra e o valor ' + 'não é cortado', (tester) async { + controller.text = '1.234.567,89'; + await pump( + tester, + FieldRow( + label: 'Valor corrigido', + prefix: 'R\$', + controller: controller, + state: InputFieldState.error, + errorText: 'fora do período disponível para o índice escolhido', + ), + width: 360, + textScale: 1.3, + ); + + expect(tester.takeException(), isNull); + + final secondLine = tester.getSize( + find.text('fora do período disponível para o índice escolhido'), + ); + final singleLine = tester.getSize(find.text('Valor corrigido')); + expect(secondLine.height, greaterThan(singleLine.height)); + + final painter = TextPainter( + text: TextSpan(text: controller.text, style: valueStyle(tester)), + textDirection: TextDirection.ltr, + textScaler: const TextScaler.linear(1.3), + )..layout(); + // O campo tem a largura do texto inteiro e fica dentro da linha. + expect( + tester.getSize(find.byType(TextField)).width, + greaterThanOrEqualTo(painter.width), + ); + painter.dispose(); + final row = tester.getRect(find.byType(FieldRow)); + final field = tester.getRect(find.byType(TextField)); + expect(field.left, greaterThanOrEqualTo(row.left)); + expect(field.right, lessThanOrEqualTo(row.right)); + }); + + group('Semantics', () { + testWidgets('a descobrir é anunciado como botão', (tester) async { + final handle = tester.ensureSemantics(); + await pump( + tester, + FieldRow( + label: 'Prestação', + controller: controller, + state: InputFieldState.unknown, + onCalculate: () {}, + ), + ); + + expect( + tester.getSemantics(find.byType(FieldRow)), + isSemantics( + label: 'Prestação, a descobrir', + isButton: true, + hasTapAction: true, + ), + ); + handle.dispose(); + }); + + testWidgets('campo anuncia rótulo, unidade, estado e valor', ( + tester, + ) async { + final handle = tester.ensureSemantics(); + controller.text = '12'; + await pump( + tester, + FieldRow( + label: 'Prazo', + unit: 'meses', + controller: controller, + state: InputFieldState.calculated, + ), + ); + + expect( + tester.getSemantics(find.byType(FieldRow)), + isSemantics( + label: 'Prazo, meses, calculado para você', + value: '12', + isTextField: true, + ), + ); + handle.dispose(); + }); + + testWidgets('seleção anuncia rótulo e valor como botão', (tester) async { + final handle = tester.ensureSemantics(); + await pump( + tester, + FieldRow.selection(label: 'Índice', value: 'IPCA', onTap: () {}), + ); + + expect( + tester.getSemantics(find.byType(FieldRow)), + isSemantics( + label: 'Índice, IPCA', + isButton: true, + hasTapAction: true, + ), + ); + handle.dispose(); + }); + }); + }); + + group('FieldList', () { + testWidgets('desenha a linha hairline no topo', (tester) async { + await pump(tester, FieldRow(label: 'Taxa', controller: controller)); + + final box = tester.widget( + find + .descendant( + of: find.byType(FieldList), + matching: find.byType(DecoratedBox), + ) + .first, + ); + final border = (box.decoration as BoxDecoration).border! as Border; + expect(border.top.color, colors.hairline); + expect(find.byType(Card), findsNothing); + }); + }); +} diff --git a/test/core/widgets/input_forms_result_card_test.dart b/test/core/widgets/input_forms_result_card_test.dart index d7aba3c..38b47f9 100644 --- a/test/core/widgets/input_forms_result_card_test.dart +++ b/test/core/widgets/input_forms_result_card_test.dart @@ -83,13 +83,13 @@ void main() { expect(find.text('calculado'), findsNothing); }); - testWidgets('exibe badge "vazio" quando o estado é highlighted', ( + testWidgets('exibe badge "vazio" quando o estado é unknown', ( tester, ) async { await tester.pumpWidget( buildTestWidget( controller: controller, - state: InputFieldState.highlighted, + state: InputFieldState.unknown, ), ); @@ -282,13 +282,13 @@ void main() { group("hint dinâmico", () { testWidgets( - 'exibe hint "toque para calcular" quando estado é highlighted e campo vazio', + 'exibe hint "toque para calcular" quando estado é unknown e campo vazio', (tester) async { await tester.pumpWidget( buildTestWidget( controller: controller, hint: 'Digite o valor', - state: InputFieldState.highlighted, + state: InputFieldState.unknown, ), ); @@ -298,7 +298,7 @@ void main() { ); testWidgets( - 'mantém o hint original quando estado é highlighted mas o campo tem texto', + 'mantém o hint original quando estado é unknown mas o campo tem texto', (tester) async { controller.text = '1500'; @@ -306,7 +306,7 @@ void main() { buildTestWidget( controller: controller, hint: 'Digite o valor', - state: InputFieldState.highlighted, + state: InputFieldState.unknown, ), ); @@ -316,7 +316,7 @@ void main() { ); testWidgets( - 'mantém o hint original quando o campo está vazio mas o estado não é highlighted', + 'mantém o hint original quando o campo está vazio mas o estado não é unknown', (tester) async { await tester.pumpWidget( buildTestWidget( @@ -338,7 +338,7 @@ void main() { buildTestWidget( controller: controller, hint: 'Digite o valor', - state: InputFieldState.highlighted, + state: InputFieldState.unknown, ), ); @@ -368,7 +368,7 @@ void main() { ); testWidgets( - 'aplica a cor neutra no hint quando o estado é highlighted mas o campo tem texto', + 'aplica a cor neutra no hint quando o estado é unknown mas o campo tem texto', (tester) async { controller.text = '1500'; @@ -376,7 +376,7 @@ void main() { buildTestWidget( controller: controller, hint: 'Digite o valor', - state: InputFieldState.highlighted, + state: InputFieldState.unknown, ), ); @@ -390,7 +390,7 @@ void main() { testWidgets('o hint muda quando onTap altera o estado para calculado', ( tester, ) async { - InputFieldState state = InputFieldState.highlighted; + InputFieldState state = InputFieldState.unknown; await tester.pumpWidget( MaterialApp( diff --git a/test/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit_test.dart b/test/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit_test.dart index c36b257..40f93be 100644 --- a/test/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit_test.dart +++ b/test/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit_test.dart @@ -41,7 +41,7 @@ void main() { ); expect( cubit.state.stateOf(FinancialCalculationTarget.interestRate), - InputFieldState.highlighted, + InputFieldState.unknown, ); }); } diff --git a/test/modules/financial_calculators/presentation/widgets/forms_view_test.dart b/test/modules/financial_calculators/presentation/widgets/forms_view_test.dart index cb6252b..8762914 100644 --- a/test/modules/financial_calculators/presentation/widgets/forms_view_test.dart +++ b/test/modules/financial_calculators/presentation/widgets/forms_view_test.dart @@ -357,7 +357,7 @@ void main() { onClear: formCubit.clear, onFieldTap: (field) { if (formState.stateOf(field) == - InputFieldState.highlighted) { + InputFieldState.unknown) { calculate(); } }, From 639e924ea68f1ebe1d3a089342d2fbffe7794018 Mon Sep 17 00:00:00 2001 From: Rodrigo Mamede <61851910+Rodrigosdk@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:07:19 -0300 Subject: [PATCH 07/22] feat: criar detalhes do resultado (ResultDetails) (#111) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: adicionar ResultSummary e formatadores de moeda e taxa anual equivalente (#111) * feat: montar o ResultSummary de cada calculadora em funções puras a partir dos valores arredondados (#111) * feat: adicionar ResultDetails com barra de composição, legenda, linhas de detalhe e goldens (#111) --- .gitignore | 3 + lib/core/utils/br_number_format.dart | 24 ++ .../result_details/result_details.dart | 192 +++++++++++++++ .../result_details/result_summary.dart | 81 +++++++ .../presentation/utils/result_summaries.dart | 79 ++++++ .../utils/correction_result_summary.dart | 40 +++ .../goldens/correction_dark.png | Bin 0 -> 19383 bytes .../goldens/correction_light.png | Bin 0 -> 19286 bytes .../result_details/goldens/financing_dark.png | Bin 0 -> 20876 bytes .../goldens/financing_light.png | Bin 0 -> 20676 bytes .../result_details/result_details_test.dart | 227 ++++++++++++++++++ .../utils/result_summaries_test.dart | 130 ++++++++++ .../utils/correction_result_summary_test.dart | 60 +++++ 13 files changed, 836 insertions(+) create mode 100644 lib/core/utils/br_number_format.dart create mode 100644 lib/core/widgets/result_details/result_details.dart create mode 100644 lib/core/widgets/result_details/result_summary.dart create mode 100644 lib/modules/financial_calculators/presentation/utils/result_summaries.dart create mode 100644 lib/modules/value_correction/presentation/utils/correction_result_summary.dart create mode 100644 test/core/widgets/result_details/goldens/correction_dark.png create mode 100644 test/core/widgets/result_details/goldens/correction_light.png create mode 100644 test/core/widgets/result_details/goldens/financing_dark.png create mode 100644 test/core/widgets/result_details/goldens/financing_light.png create mode 100644 test/core/widgets/result_details/result_details_test.dart create mode 100644 test/modules/financial_calculators/presentation/utils/result_summaries_test.dart create mode 100644 test/modules/value_correction/presentation/utils/correction_result_summary_test.dart diff --git a/.gitignore b/.gitignore index 5bcb512..5e82815 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ app.*.map.json # Binários locais de ferramentas (ex.: act) /bin/ + +# Imagens geradas quando um golden falha +**/failures/ diff --git a/lib/core/utils/br_number_format.dart b/lib/core/utils/br_number_format.dart new file mode 100644 index 0000000..c17809e --- /dev/null +++ b/lib/core/utils/br_number_format.dart @@ -0,0 +1,24 @@ +import 'dart:math'; + +import 'package:intl/intl.dart'; + +final _decimal = NumberFormat('#,##0.00', 'pt_BR'); + +/// Arredonda para centavos e devolve o valor em centavos inteiros, para as +/// contas derivadas fecharem com o que a pessoa vê na tela. +int toCents(double value) => (value * 100).round(); + +/// "R$ 2.836,80", com espaço não separável entre o símbolo e o número. +/// Negativos ficam com o sinal antes do símbolo: "-R$ 10,00". +String formatCents(int cents) { + final sign = cents < 0 ? '-' : ''; + return '${sign}R\$\u00A0${_decimal.format(cents.abs() / 100)}'; +} + +/// "26,82%". +String formatPercent(double percent) => '${_decimal.format(percent)}%'; + +/// Taxa anual equivalente, em %, a partir da taxa mensal em %: +/// (1 + i)¹² − 1. +double annualEquivalentRate(double monthlyPercent) => + (pow(1 + monthlyPercent / 100, 12) - 1) * 100; diff --git a/lib/core/widgets/result_details/result_details.dart b/lib/core/widgets/result_details/result_details.dart new file mode 100644 index 0000000..7cac1a5 --- /dev/null +++ b/lib/core/widgets/result_details/result_details.dart @@ -0,0 +1,192 @@ +import 'package:flutter/material.dart'; +import 'package:real_calc/core/themes/app_colors.dart'; +import 'package:real_calc/core/themes/calculator_scope.dart'; +import 'package:real_calc/core/themes/spacing.dart'; +import 'package:real_calc/core/themes/text_styles.dart'; + +import 'result_summary.dart'; + +/// Detalhes do resultado, abaixo da lista de campos: título, barra de +/// composição com legenda e linhas de detalhe. Sem cartão, sombra ou borda. +/// +/// A cor de juros, rendimento ou correção vem do [CalculatorScope]. +class ResultDetails extends StatelessWidget { + static const double barHeight = 6; + static const double lineHeight = 32; + + final ResultSummary summary; + + const ResultDetails({super.key, required this.summary}); + + @override + Widget build(BuildContext context) { + final colors = AppColors.of(context); + final otherColor = + CalculatorScope.maybeOf(context)?.solid ?? + Theme.of(context).colorScheme.primary; + final composition = summary.composition; + + return Padding( + padding: const EdgeInsets.only(top: AppSpacing.sectionGap), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + summary.title, + style: AppTextStyles.sectionLabel.copyWith( + color: colors.textSecondary, + ), + ), + if (composition != null) ...[ + const SizedBox(height: 12), + _CompositionBar( + composition: composition, + ownColor: colors.textPrimary, + otherColor: otherColor, + trackColor: colors.hairline, + ), + const SizedBox(height: 10), + ExcludeSemantics( + child: Wrap( + spacing: AppSpacing.md, + runSpacing: AppSpacing.xs, + children: [ + _LegendItem( + segment: composition.own, + color: colors.textPrimary, + textColor: colors.textSecondary, + ), + _LegendItem( + segment: composition.other, + color: otherColor, + textColor: colors.textSecondary, + ), + ], + ), + ), + ], + const SizedBox(height: AppSpacing.sm), + for (final line in summary.lines) + _DetailRow(line: line, colors: colors), + ], + ), + ); + } +} + +class _CompositionBar extends StatelessWidget { + final ResultComposition composition; + final Color ownColor; + final Color otherColor; + final Color trackColor; + + const _CompositionBar({ + required this.composition, + required this.ownColor, + required this.otherColor, + required this.trackColor, + }); + + @override + Widget build(BuildContext context) { + final own = composition.own.percent; + final other = composition.other.percent; + + return Semantics( + container: true, + label: composition.semanticsLabel, + child: ClipRRect( + borderRadius: BorderRadius.circular(ResultDetails.barHeight / 2), + child: SizedBox( + height: ResultDetails.barHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (own > 0) + Expanded( + flex: own, + child: ColoredBox(color: ownColor), + ), + if (own > 0 && other > 0) const SizedBox(width: 2), + if (other > 0) + Expanded( + flex: other, + child: ColoredBox(color: otherColor), + ), + if (own == 0 && other == 0) + Expanded(child: ColoredBox(color: trackColor)), + ], + ), + ), + ), + ); + } +} + +class _LegendItem extends StatelessWidget { + final ResultSegment segment; + final Color color; + final Color textColor; + + const _LegendItem({ + required this.segment, + required this.color, + required this.textColor, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + const SizedBox(width: 6), + Text( + '${segment.label} ${segment.percent}%', + style: AppTextStyles.caption.copyWith(color: textColor), + ), + ], + ); + } +} + +class _DetailRow extends StatelessWidget { + final ResultDetailLine line; + final AppColors colors; + + const _DetailRow({required this.line, required this.colors}); + + @override + Widget build(BuildContext context) { + return MergeSemantics( + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: ResultDetails.lineHeight), + child: Row( + children: [ + Expanded( + child: Text( + line.label, + style: AppTextStyles.detailLabel.copyWith( + color: colors.textSecondary, + ), + ), + ), + const SizedBox(width: AppSpacing.md), + Text( + line.value, + softWrap: false, + style: AppTextStyles.detailValue.copyWith( + color: colors.textPrimary, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/core/widgets/result_details/result_summary.dart b/lib/core/widgets/result_details/result_summary.dart new file mode 100644 index 0000000..5e43e4f --- /dev/null +++ b/lib/core/widgets/result_details/result_summary.dart @@ -0,0 +1,81 @@ +/// Um lado da barra de composição: rótulo e percentual inteiro. +class ResultSegment { + final String label; + final int percent; + + const ResultSegment(this.label, this.percent); + + @override + bool operator ==(Object other) => + other is ResultSegment && + other.label == label && + other.percent == percent; + + @override + int get hashCode => Object.hash(label, percent); + + @override + String toString() => '$label $percent%'; +} + +/// Linha de detalhe: rótulo à esquerda, valor já formatado à direita. +class ResultDetailLine { + final String label; + final String value; + + const ResultDetailLine(this.label, this.value); + + @override + bool operator ==(Object other) => + other is ResultDetailLine && other.label == label && other.value == value; + + @override + int get hashCode => Object.hash(label, value); + + @override + String toString() => '$label: $value'; +} + +/// Composição do resultado: o que é da pessoa ([own]) e o que é juros, +/// rendimento ou correção ([other]). Os percentuais somam 100. +class ResultComposition { + final ResultSegment own; + final ResultSegment other; + + const ResultComposition._(this.own, this.other); + + /// Calcula os percentuais a partir dos valores em centavos. O lado [own] é + /// arredondado e o [other] fica com o restante, para a soma dar 100. + /// Devolve `null` se o total não for positivo ou se alguma parte for + /// negativa (ex.: deflação), casos em que a barra não faz sentido. + static ResultComposition? fromCents({ + required String ownLabel, + required int ownCents, + required String otherLabel, + required int otherCents, + }) { + final total = ownCents + otherCents; + if (total <= 0 || ownCents < 0 || otherCents < 0) return null; + final ownPercent = (ownCents * 100 / total).round(); + return ResultComposition._( + ResultSegment(ownLabel, ownPercent), + ResultSegment(otherLabel, 100 - ownPercent), + ); + } + + /// Texto lido pelo TalkBack, ex.: "Emprestado 88%, Juros 12%". + String get semanticsLabel => '$own, $other'; +} + +/// Tudo o que o [ResultDetails] mostra abaixo da lista de campos. +class ResultSummary { + final String title; + final ResultComposition? composition; + final List lines; + + const ResultSummary({ + required this.title, + required this.composition, + required this.lines, + }); +} diff --git a/lib/modules/financial_calculators/presentation/utils/result_summaries.dart b/lib/modules/financial_calculators/presentation/utils/result_summaries.dart new file mode 100644 index 0000000..4733c77 --- /dev/null +++ b/lib/modules/financial_calculators/presentation/utils/result_summaries.dart @@ -0,0 +1,79 @@ +import 'package:real_calc/core/utils/br_number_format.dart'; +import 'package:real_calc/core/widgets/result_details/result_summary.dart'; + +import '../../domain/entities/financial_calculation.dart'; + +// Os valores derivados saem dos valores arredondados que a pessoa vê na tela +// (em centavos), para a conta fechar: 236,40 × 12 = 2.836,80. + +/// Financiamento: [FinancialCalculation.initialValue] é o valor emprestado e +/// [FinancialCalculation.finalValue], a prestação. +ResultSummary financingResultSummary(FinancialCalculation calculation) { + final borrowed = toCents(calculation.initialValue); + final total = toCents(calculation.finalValue) * calculation.periods; + final interest = total - borrowed; + + return ResultSummary( + title: 'Do total que você vai pagar', + composition: ResultComposition.fromCents( + ownLabel: 'Emprestado', + ownCents: borrowed, + otherLabel: 'Juros', + otherCents: interest, + ), + lines: [ + ResultDetailLine('Total pago', formatCents(total)), + ResultDetailLine('Total de juros', formatCents(interest)), + _annualRateLine(calculation.rate), + ], + ); +} + +/// Depósitos Regulares: [FinancialCalculation.initialValue] é o depósito +/// mensal e [FinancialCalculation.finalValue], o valor final. +ResultSummary regularDepositsResultSummary(FinancialCalculation calculation) { + final deposited = toCents(calculation.initialValue) * calculation.periods; + final yield_ = toCents(calculation.finalValue) - deposited; + + return ResultSummary( + title: 'Do valor final', + composition: ResultComposition.fromCents( + ownLabel: 'Depositado', + ownCents: deposited, + otherLabel: 'Rendimento', + otherCents: yield_, + ), + lines: [ + ResultDetailLine('Total depositado', formatCents(deposited)), + ResultDetailLine('Rendimento', formatCents(yield_)), + _annualRateLine(calculation.rate), + ], + ); +} + +/// Valor Futuro: [FinancialCalculation.initialValue] é o capital e +/// [FinancialCalculation.finalValue], o valor final. +ResultSummary futureValueResultSummary(FinancialCalculation calculation) { + final capital = toCents(calculation.initialValue); + final yield_ = toCents(calculation.finalValue) - capital; + + return ResultSummary( + title: 'Do valor final', + composition: ResultComposition.fromCents( + ownLabel: 'Capital', + ownCents: capital, + otherLabel: 'Rendimento', + otherCents: yield_, + ), + lines: [ + ResultDetailLine('Capital investido', formatCents(capital)), + ResultDetailLine('Rendimento', formatCents(yield_)), + _annualRateLine(calculation.rate), + ], + ); +} + +ResultDetailLine _annualRateLine(double monthlyPercent) => ResultDetailLine( + 'Taxa anual equivalente', + '${formatPercent(annualEquivalentRate(monthlyPercent))} a.a.', +); diff --git a/lib/modules/value_correction/presentation/utils/correction_result_summary.dart b/lib/modules/value_correction/presentation/utils/correction_result_summary.dart new file mode 100644 index 0000000..3d99188 --- /dev/null +++ b/lib/modules/value_correction/presentation/utils/correction_result_summary.dart @@ -0,0 +1,40 @@ +import 'package:intl/intl.dart'; +import 'package:real_calc/core/utils/br_number_format.dart'; +import 'package:real_calc/core/widgets/result_details/result_summary.dart'; + +import '../../domain/entites/value_correction.dart'; + +final _factorFormat = NumberFormat('#,##0.0000000', 'pt_BR'); + +/// Correção de Valores. Sem valor original (só o fator), não há o que +/// detalhar e devolve `null`. +/// +/// A correção sai dos valores arredondados: corrigido − original, em +/// centavos. Com deflação a correção é negativa e a barra é omitida. +ResultSummary? correctionResultSummary(ValueCorrection correction) { + final originalValue = correction.originalValue; + final adjustedValue = correction.adjustedValue; + if (originalValue == null || adjustedValue == null) return null; + + final original = toCents(originalValue); + final adjusted = toCents(adjustedValue); + final difference = adjusted - original; + + return ResultSummary( + title: 'Do valor corrigido', + composition: ResultComposition.fromCents( + ownLabel: 'Valor original', + ownCents: original, + otherLabel: 'Correção', + otherCents: difference, + ), + lines: [ + ResultDetailLine('Valor original', formatCents(original)), + ResultDetailLine('Correção no período', formatCents(difference)), + ResultDetailLine( + 'Fator de correção', + _factorFormat.format(correction.factor), + ), + ], + ); +} diff --git a/test/core/widgets/result_details/goldens/correction_dark.png b/test/core/widgets/result_details/goldens/correction_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..d6cae0db855a77f2cffce88cffcb344c3b56a4ec GIT binary patch literal 19383 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 literal 0 HcmV?d00001 diff --git a/test/core/widgets/result_details/goldens/correction_light.png b/test/core/widgets/result_details/goldens/correction_light.png new file mode 100644 index 0000000000000000000000000000000000000000..85b4a34de3267d982b8faa6a40c46e622116c246 GIT binary patch literal 19286 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 literal 0 HcmV?d00001 diff --git a/test/core/widgets/result_details/result_details_test.dart b/test/core/widgets/result_details/result_details_test.dart new file mode 100644 index 0000000..eae2917 --- /dev/null +++ b/test/core/widgets/result_details/result_details_test.dart @@ -0,0 +1,227 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:real_calc/core/themes/app_colors.dart'; +import 'package:real_calc/core/themes/app_theme.dart'; +import 'package:real_calc/core/themes/calculator_palette.dart'; +import 'package:real_calc/core/themes/calculator_scope.dart'; +import 'package:real_calc/core/widgets/result_details/result_details.dart'; +import 'package:real_calc/core/widgets/result_details/result_summary.dart'; +import 'package:real_calc/modules/financial_calculators/domain/entities/financial_calculation.dart'; +import 'package:real_calc/modules/financial_calculators/presentation/utils/result_summaries.dart'; +import 'package:real_calc/modules/value_correction/domain/entites/period.dart'; +import 'package:real_calc/modules/value_correction/domain/entites/value_correction.dart'; +import 'package:real_calc/modules/value_correction/presentation/utils/correction_result_summary.dart'; + +final financing = financingResultSummary( + FinancialCalculation( + initialValue: 2500, + rate: 2, + periods: 12, + finalValue: 236.3990, + ), +); + +final correction = correctionResultSummary( + ValueCorrection( + index: 433, + period: Period(initial: DateTime(2022), end: DateTime(2025)), + percentage: 100, + originalValue: 1000, + factor: 1.2345678, + adjustedValue: 1234.5678, + variation: 23.45678, + ), +)!; + +/// Carrega a Manrope para os goldens mostrarem o texto de verdade, e não os +/// blocos da fonte padrão de teste. +Future loadManrope() async { + final loader = FontLoader('Manrope'); + for (final weight in ['Medium', 'SemiBold', 'Bold', 'ExtraBold']) { + loader.addFont(rootBundle.load('assets/fonts/Manrope-$weight.ttf')); + } + await loader.load(); +} + +void main() { + setUpAll(loadManrope); + + Future pump( + WidgetTester tester, + ResultSummary summary, { + CalculatorKind kind = CalculatorKind.financing, + Brightness brightness = Brightness.light, + double width = 360, + }) async { + tester.view.physicalSize = Size(width, 480); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + + await tester.pumpWidget( + MaterialApp( + debugShowCheckedModeBanner: false, + theme: brightness == Brightness.light + ? AppTheme.lightTheme + : AppTheme.darkTheme, + home: Scaffold( + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: CalculatorScope( + kind: kind, + child: Align( + alignment: Alignment.topCenter, + child: ResultDetails(summary: summary), + ), + ), + ), + ), + ), + ); + } + + group('ResultDetails', () { + testWidgets('mostra título, legenda e linhas de detalhe', (tester) async { + await pump(tester, financing); + + expect(find.text('Do total que você vai pagar'), findsOneWidget); + expect(find.text('Emprestado 88%'), findsOneWidget); + expect(find.text('Juros 12%'), findsOneWidget); + for (final line in financing.lines) { + expect(find.text(line.label), findsOneWidget); + expect(find.text(line.value), findsOneWidget); + } + expect(find.text(financing.lines.first.value), findsOneWidget); + expect(financing.lines.first.value, contains('2.836,80')); + }); + + testWidgets('barra de 6px com a cor da pessoa e a da calculadora', ( + tester, + ) async { + await pump(tester, financing); + + final boxes = tester + .widgetList( + find.descendant( + of: find.byType(ResultDetails), + matching: find.byType(ColoredBox), + ), + ) + .map((box) => box.color) + .toList(); + expect(boxes, [ + AppColors.light.textPrimary, + CalculatorPalette.of(CalculatorKind.financing).solid, + ]); + + final bar = find.descendant( + of: find.byType(ResultDetails), + matching: find.byType(ClipRRect), + ); + expect(tester.getSize(bar).height, ResultDetails.barHeight); + // Cada segmento ocupa a altura toda da barra, na proporção 88/12. + final segments = find.descendant( + of: bar, + matching: find.byType(ColoredBox), + ); + final own = tester.getSize(segments.at(0)); + final other = tester.getSize(segments.at(1)); + expect(own.height, ResultDetails.barHeight); + expect(other.height, ResultDetails.barHeight); + expect(own.width / (own.width + other.width), closeTo(0.88, 0.01)); + }); + + testWidgets('linhas de detalhe têm pelo menos 32px', (tester) async { + await pump(tester, financing); + + final row = find + .ancestor( + of: find.text('Total pago'), + matching: find.byType(ConstrainedBox), + ) + .first; + expect( + tester.getSize(row).height, + greaterThanOrEqualTo(ResultDetails.lineHeight), + ); + }); + + testWidgets('a barra tem Semantics com a composição', (tester) async { + final handle = tester.ensureSemantics(); + await pump(tester, financing); + + expect( + find.bySemanticsLabel('Emprestado 88%, Juros 12%'), + findsOneWidget, + ); + handle.dispose(); + }); + + testWidgets('sem composição, mostra só título e linhas', (tester) async { + await pump( + tester, + const ResultSummary( + title: 'Do valor corrigido', + composition: null, + lines: [ResultDetailLine('Fator de correção', '0,9900000')], + ), + ); + + expect( + find.descendant( + of: find.byType(ResultDetails), + matching: find.byType(ClipRRect), + ), + findsNothing, + ); + expect(find.text('Fator de correção'), findsOneWidget); + }); + + testWidgets('valores com 7 dígitos cabem em 360dp', (tester) async { + final summary = financingResultSummary( + FinancialCalculation( + initialValue: 1500000, + rate: 1.5, + periods: 360, + finalValue: 22839.53, + ), + ); + expect(summary.lines.first.value, contains('8.222.230,80')); + + await pump(tester, summary); + + expect(tester.takeException(), isNull); + for (final line in summary.lines) { + final value = tester.getRect(find.text(line.value)); + expect(value.right, lessThanOrEqualTo(360 - 24)); + } + }); + }); + + group('goldens', () { + for (final brightness in Brightness.values) { + testWidgets('financiamento no tema ${brightness.name}', (tester) async { + await pump(tester, financing, brightness: brightness); + await expectLater( + find.byType(Scaffold), + matchesGoldenFile('goldens/financing_${brightness.name}.png'), + ); + }); + + testWidgets('correção no tema ${brightness.name}', (tester) async { + await pump( + tester, + correction, + kind: CalculatorKind.correction, + brightness: brightness, + ); + expect(find.text('Valor original 81%'), findsOneWidget); + expect(find.text('Correção 19%'), findsOneWidget); + await expectLater( + find.byType(Scaffold), + matchesGoldenFile('goldens/correction_${brightness.name}.png'), + ); + }); + } + }); +} diff --git a/test/modules/financial_calculators/presentation/utils/result_summaries_test.dart b/test/modules/financial_calculators/presentation/utils/result_summaries_test.dart new file mode 100644 index 0000000..9304104 --- /dev/null +++ b/test/modules/financial_calculators/presentation/utils/result_summaries_test.dart @@ -0,0 +1,130 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:real_calc/core/widgets/result_details/result_summary.dart'; +import 'package:real_calc/modules/financial_calculators/domain/entities/financial_calculation.dart'; +import 'package:real_calc/modules/financial_calculators/presentation/utils/result_summaries.dart'; + +void main() { + group('financingResultSummary', () { + // R$ 2.500,00 a 2% a.m. em 12 meses: prestação de R$ 236,399 → 236,40. + final calculation = FinancialCalculation( + initialValue: 2500, + rate: 2, + periods: 12, + finalValue: 236.3990, + ); + + test('o total sai da prestação arredondada: 236,40 × 12', () { + final summary = financingResultSummary(calculation); + + expect(summary.title, 'Do total que você vai pagar'); + expect(summary.lines, const [ + ResultDetailLine('Total pago', 'R\$\u00A02.836,80'), + ResultDetailLine('Total de juros', 'R\$\u00A0336,80'), + ResultDetailLine('Taxa anual equivalente', '26,82% a.a.'), + ]); + }); + + test('composição: emprestado 88%, juros 12%', () { + final composition = financingResultSummary(calculation).composition!; + + expect(composition.own, const ResultSegment('Emprestado', 88)); + expect(composition.other, const ResultSegment('Juros', 12)); + expect(composition.semanticsLabel, 'Emprestado 88%, Juros 12%'); + }); + }); + + group('regularDepositsResultSummary', () { + test('R\$ 100,00 por mês a 1% em 12 meses', () { + final summary = regularDepositsResultSummary( + FinancialCalculation( + initialValue: 100, + rate: 1, + periods: 12, + finalValue: 1280.9328, + ), + ); + + expect(summary.title, 'Do valor final'); + expect(summary.lines, const [ + ResultDetailLine('Total depositado', 'R\$\u00A01.200,00'), + ResultDetailLine('Rendimento', 'R\$\u00A080,93'), + ResultDetailLine('Taxa anual equivalente', '12,68% a.a.'), + ]); + expect(summary.composition!.own, const ResultSegment('Depositado', 94)); + expect(summary.composition!.other, const ResultSegment('Rendimento', 6)); + }); + }); + + group('futureValueResultSummary', () { + test('R\$ 1.000,00 a 1% em 12 meses', () { + final summary = futureValueResultSummary( + FinancialCalculation( + initialValue: 1000, + rate: 1, + periods: 12, + finalValue: 1126.8250, + ), + ); + + expect(summary.lines, const [ + ResultDetailLine('Capital investido', 'R\$\u00A01.000,00'), + ResultDetailLine('Rendimento', 'R\$\u00A0126,83'), + ResultDetailLine('Taxa anual equivalente', '12,68% a.a.'), + ]); + expect(summary.composition!.own, const ResultSegment('Capital', 89)); + expect(summary.composition!.other, const ResultSegment('Rendimento', 11)); + }); + + test('taxa zero: rendimento 0% e a barra continua somando 100', () { + final composition = futureValueResultSummary( + FinancialCalculation( + initialValue: 1000, + rate: 0, + periods: 12, + finalValue: 1000, + ), + ).composition!; + + expect(composition.own.percent, 100); + expect(composition.other.percent, 0); + }); + }); + + group('ResultComposition', () { + test('os percentuais sempre somam 100', () { + for (final (own, other) in [ + (250000, 33680), + (1, 2), + (1, 1), + (333, 667), + (99950, 50), + (123456789, 98765), + ]) { + final composition = ResultComposition.fromCents( + ownLabel: 'a', + ownCents: own, + otherLabel: 'b', + otherCents: other, + )!; + expect( + composition.own.percent + composition.other.percent, + 100, + reason: '$own + $other', + ); + } + }); + + test('sem total positivo ou com parte negativa, não há barra', () { + ResultComposition? of(int own, int other) => ResultComposition.fromCents( + ownLabel: 'a', + ownCents: own, + otherLabel: 'b', + otherCents: other, + ); + + expect(of(0, 0), isNull); + expect(of(1000, -50), isNull); + expect(of(-10, 50), isNull); + }); + }); +} diff --git a/test/modules/value_correction/presentation/utils/correction_result_summary_test.dart b/test/modules/value_correction/presentation/utils/correction_result_summary_test.dart new file mode 100644 index 0000000..f9be79c --- /dev/null +++ b/test/modules/value_correction/presentation/utils/correction_result_summary_test.dart @@ -0,0 +1,60 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:real_calc/core/widgets/result_details/result_summary.dart'; +import 'package:real_calc/modules/value_correction/domain/entites/period.dart'; +import 'package:real_calc/modules/value_correction/domain/entites/value_correction.dart'; +import 'package:real_calc/modules/value_correction/presentation/utils/correction_result_summary.dart'; + +ValueCorrection correctionOf({ + double? original = 1000, + double? adjusted = 1234.5678, + double factor = 1.2345678, +}) => ValueCorrection( + index: 433, + period: Period(initial: DateTime(2022), end: DateTime(2025)), + percentage: 100, + originalValue: original, + factor: factor, + adjustedValue: adjusted, + variation: (factor - 1) * 100, +); + +void main() { + group('correctionResultSummary', () { + test('valor original 81%, correção 19%', () { + final summary = correctionResultSummary(correctionOf())!; + + expect(summary.title, 'Do valor corrigido'); + expect( + summary.composition!.own, + const ResultSegment('Valor original', 81), + ); + expect(summary.composition!.other, const ResultSegment('Correção', 19)); + }); + + test('a correção sai dos valores arredondados', () { + final summary = correctionResultSummary(correctionOf())!; + + expect(summary.lines, const [ + ResultDetailLine('Valor original', 'R\$\u00A01.000,00'), + ResultDetailLine('Correção no período', 'R\$\u00A0234,57'), + ResultDetailLine('Fator de correção', '1,2345678'), + ]); + }); + + test('com deflação, mostra as linhas mas omite a barra', () { + final summary = correctionResultSummary( + correctionOf(adjusted: 990, factor: 0.99), + )!; + + expect(summary.composition, isNull); + expect(summary.lines[1].value, '-R\$\u00A010,00'); + }); + + test('sem valor original, não há detalhes', () { + expect( + correctionResultSummary(correctionOf(original: null, adjusted: null)), + isNull, + ); + }); + }); +} From 53d7cf062a461cbdfe9f47da5270234b4337c61f Mon Sep 17 00:00:00 2001 From: Rodrigo Mamede <61851910+Rodrigosdk@users.noreply.github.com> Date: Sun, 25 Oct 2026 16:55:31 -0300 Subject: [PATCH 08/22] =?UTF-8?q?feat:=20faixa=20da=20calculadora=20com=20?= =?UTF-8?q?pergunta=20e=20resposta,=20e=20barra=20de=20a=C3=A7=C3=B5es=20(?= =?UTF-8?q?#121)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: adicionar CalculatorBand com cabeçalho, frase por estado e barra de status clara (#121) * feat: adicionar BottomActionBar e SnackBar de campos limpos com desfazer (#121) --- lib/core/widgets/bottom_action_bar.dart | 106 ++++++++ .../calculator_band/band_statement.dart | 30 +++ .../calculator_band/calculator_band.dart | 254 ++++++++++++++++++ test/core/widgets/bottom_action_bar_test.dart | 222 +++++++++++++++ .../calculator_band/calculator_band_test.dart | 225 ++++++++++++++++ 5 files changed, 837 insertions(+) create mode 100644 lib/core/widgets/bottom_action_bar.dart create mode 100644 lib/core/widgets/calculator_band/band_statement.dart create mode 100644 lib/core/widgets/calculator_band/calculator_band.dart create mode 100644 test/core/widgets/bottom_action_bar_test.dart create mode 100644 test/core/widgets/calculator_band/calculator_band_test.dart diff --git a/lib/core/widgets/bottom_action_bar.dart b/lib/core/widgets/bottom_action_bar.dart new file mode 100644 index 0000000..c842648 --- /dev/null +++ b/lib/core/widgets/bottom_action_bar.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:real_calc/core/themes/app_colors.dart'; +import 'package:real_calc/core/themes/calculator_scope.dart'; +import 'package:real_calc/core/themes/text_styles.dart'; + +/// Deve ficar no `body`, abaixo da rolagem, e não em `bottomNavigationBar`, +/// para subir junto com o teclado. +class BottomActionBar extends StatelessWidget { + static const double buttonHeight = 56; + static const EdgeInsets padding = EdgeInsets.fromLTRB(24, 12, 24, 30); + static const shareLabel = 'Compartilhar resultado'; + + final String primaryLabel; + final VoidCallback? onPrimary; + + final VoidCallback? onShare; + + const BottomActionBar({ + super.key, + required this.primaryLabel, + required this.onPrimary, + this.onShare, + }); + + @override + Widget build(BuildContext context) { + final colors = AppColors.of(context); + final scheme = Theme.of(context).colorScheme; + final palette = CalculatorScope.maybeOf(context); + final solid = palette?.solid ?? scheme.primary; + final onSolid = palette?.onSolid ?? scheme.onPrimary; + + return ColoredBox( + color: colors.background, + child: SafeArea( + top: false, + minimum: padding, + child: Row( + children: [ + Opacity( + opacity: onShare == null ? 0.4 : 1, + child: SizedBox.square( + dimension: buttonHeight, + child: Tooltip( + message: shareLabel, + excludeFromSemantics: true, + child: Semantics( + label: shareLabel, + button: true, + enabled: onShare != null, + child: OutlinedButton( + onPressed: onShare, + style: OutlinedButton.styleFrom( + padding: EdgeInsets.zero, + shape: const CircleBorder(), + side: BorderSide(color: colors.hairline, width: 1.5), + foregroundColor: colors.textPrimary, + disabledForegroundColor: colors.textPrimary, + ), + child: const Icon(Icons.ios_share, size: 22), + ), + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: SizedBox( + height: buttonHeight, + child: FilledButton( + onPressed: onPrimary, + style: FilledButton.styleFrom( + backgroundColor: solid, + foregroundColor: onSolid, + disabledBackgroundColor: solid.withValues(alpha: 0.4), + disabledForegroundColor: onSolid, + shape: const StadiumBorder(), + textStyle: AppTextStyles.button, + ), + child: Text(primaryLabel), + ), + ), + ), + ], + ), + ), + ); + } +} + +ScaffoldFeatureController +showFieldsClearedSnackBar( + BuildContext context, { + required VoidCallback onUndo, +}) { + final messenger = ScaffoldMessenger.of(context)..hideCurrentSnackBar(); + return messenger.showSnackBar( + SnackBar( + content: const Text('Campos limpos'), + duration: const Duration(seconds: 4), + // Com ação, o SnackBar não some sozinho por padrão. + persist: false, + action: SnackBarAction(label: 'Desfazer', onPressed: onUndo), + ), + ); +} diff --git a/lib/core/widgets/calculator_band/band_statement.dart b/lib/core/widgets/calculator_band/band_statement.dart new file mode 100644 index 0000000..ca58f84 --- /dev/null +++ b/lib/core/widgets/calculator_band/band_statement.dart @@ -0,0 +1,30 @@ +class BandStatement { + final String? kicker; + + final String value; + + final bool isMoney; + + final String? sub; + + const BandStatement({ + this.kicker, + required this.value, + this.isMoney = false, + this.sub, + }); + + String get semanticsLabel => + [?kicker, isMoney ? 'R\$ $value' : value, ?sub].join(' '); + + @override + bool operator ==(Object other) => + other is BandStatement && + other.kicker == kicker && + other.value == value && + other.isMoney == isMoney && + other.sub == sub; + + @override + int get hashCode => Object.hash(kicker, value, isMoney, sub); +} diff --git a/lib/core/widgets/calculator_band/calculator_band.dart b/lib/core/widgets/calculator_band/calculator_band.dart new file mode 100644 index 0000000..6ae19e8 --- /dev/null +++ b/lib/core/widgets/calculator_band/calculator_band.dart @@ -0,0 +1,254 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:real_calc/core/themes/calculator_palette.dart'; +import 'package:real_calc/core/themes/calculator_scope.dart'; +import 'package:real_calc/core/themes/spacing.dart'; +import 'package:real_calc/core/themes/text_styles.dart'; + +import 'band_statement.dart'; + +class CalculatorBand extends StatelessWidget { + static const double iconTouchSize = 44; + static const double moneySymbolSize = 22; + static const double minValueSize = 36; + + static const backLabel = 'Voltar'; + static const helpLabel = 'Como funciona'; + static const clearLabel = 'Limpar campos'; + + static const statusBarStyle = SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.light, + statusBarBrightness: Brightness.dark, + ); + + final String title; + final BandStatement statement; + final VoidCallback? onBack; + final VoidCallback? onHelp; + final VoidCallback? onClear; + + const CalculatorBand({ + super.key, + required this.title, + required this.statement, + this.onBack, + this.onHelp, + this.onClear, + }); + + @override + Widget build(BuildContext context) { + final palette = + CalculatorScope.maybeOf(context) ?? + CalculatorPalette( + solid: Theme.of(context).colorScheme.primary, + onSolid: Theme.of(context).colorScheme.onPrimary, + text: Theme.of(context).colorScheme.primary, + ); + final onSolid = palette.onSolid; + + return AnnotatedRegion( + value: statusBarStyle, + child: ColoredBox( + color: palette.solid, + child: SafeArea( + bottom: false, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Row( + children: [ + _BandIconButton( + icon: Icons.arrow_back, + label: backLabel, + color: onSolid, + onPressed: + onBack ?? () => Navigator.of(context).maybePop(), + ), + const SizedBox(width: 2), + Expanded( + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTextStyles.headerTitle.copyWith( + color: onSolid, + ), + ), + ), + if (onHelp != null) + _BandIconButton( + icon: Icons.help_outline, + label: helpLabel, + color: onSolid, + onPressed: onHelp!, + ), + if (onClear != null) + _BandIconButton( + icon: Icons.refresh, + label: clearLabel, + color: onSolid, + onPressed: onClear!, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.screen, + AppSpacing.sm, + AppSpacing.screen, + AppSpacing.lg + 4, + ), + child: _Statement(statement: statement, color: onSolid), + ), + ], + ), + ), + ), + ); + } +} + +class _Statement extends StatelessWidget { + final BandStatement statement; + final Color color; + + const _Statement({required this.statement, required this.color}); + + @override + Widget build(BuildContext context) { + final soft = color.withValues(alpha: 0.82); + + return Semantics( + container: true, + liveRegion: true, + label: statement.semanticsLabel, + child: ExcludeSemantics( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (statement.kicker != null) + Text( + statement.kicker!, + style: AppTextStyles.bandKicker.copyWith(color: soft), + ), + _StatementValue(statement: statement, color: color), + if (statement.sub != null) + Text( + statement.sub!, + style: AppTextStyles.bandSub.copyWith(color: soft), + ), + ], + ), + ), + ); + } +} + +class _StatementValue extends StatelessWidget { + final BandStatement statement; + final Color color; + + const _StatementValue({required this.statement, required this.color}); + + @override + Widget build(BuildContext context) { + final textScaler = MediaQuery.textScalerOf(context); + final baseStyle = AppTextStyles.bandStatement.copyWith(color: color); + final symbolStyle = baseStyle.copyWith( + fontSize: CalculatorBand.moneySymbolSize, + letterSpacing: -0.4, + ); + + return LayoutBuilder( + builder: (context, constraints) { + final symbolWidth = statement.isMoney + ? _measure('R\$', symbolStyle, textScaler) + 6 + : 0.0; + final available = constraints.maxWidth - symbolWidth; + final naturalWidth = _measure(statement.value, baseStyle, textScaler); + final baseSize = baseStyle.fontSize!; + final fontSize = naturalWidth <= available + ? baseSize + : math.max( + CalculatorBand.minValueSize, + (baseSize * available / naturalWidth).floorToDouble(), + ); + + return FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + if (statement.isMoney) ...[ + Text('R\$', style: symbolStyle), + const SizedBox(width: 6), + ], + Text( + statement.value, + maxLines: 1, + softWrap: false, + style: baseStyle.copyWith(fontSize: fontSize), + ), + ], + ), + ); + }, + ); + } + + static double _measure(String text, TextStyle style, TextScaler scaler) { + final painter = TextPainter( + text: TextSpan(text: text, style: style), + textDirection: TextDirection.ltr, + textScaler: scaler, + maxLines: 1, + )..layout(); + final width = painter.width; + painter.dispose(); + return width; + } +} + +class _BandIconButton extends StatelessWidget { + final IconData icon; + final String label; + final Color color; + final VoidCallback onPressed; + + const _BandIconButton({ + required this.icon, + required this.label, + required this.color, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return MergeSemantics( + child: Semantics( + label: label, + child: SizedBox.square( + dimension: CalculatorBand.iconTouchSize, + child: IconButton( + onPressed: onPressed, + padding: EdgeInsets.zero, + color: color, + icon: Icon(icon, size: 22), + ), + ), + ), + ); + } +} diff --git a/test/core/widgets/bottom_action_bar_test.dart b/test/core/widgets/bottom_action_bar_test.dart new file mode 100644 index 0000000..c3c9815 --- /dev/null +++ b/test/core/widgets/bottom_action_bar_test.dart @@ -0,0 +1,222 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:real_calc/core/themes/app_colors.dart'; +import 'package:real_calc/core/themes/app_theme.dart'; +import 'package:real_calc/core/themes/calculator_palette.dart'; +import 'package:real_calc/core/themes/calculator_scope.dart'; +import 'package:real_calc/core/widgets/bottom_action_bar.dart'; + +void main() { + const screen = Size(400, 800); + + Future pump( + WidgetTester tester, + Widget bar, { + EdgeInsets viewPadding = EdgeInsets.zero, + EdgeInsets viewInsets = EdgeInsets.zero, + Widget? content, + }) async { + tester.view.physicalSize = screen; + tester.view.devicePixelRatio = 1; + tester.view.padding = FakeViewPadding(bottom: viewPadding.bottom); + tester.view.viewInsets = FakeViewPadding(bottom: viewInsets.bottom); + addTearDown(tester.view.reset); + + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.lightTheme, + home: CalculatorScope( + kind: CalculatorKind.deposits, + child: Scaffold( + body: Column( + children: [ + Expanded(child: content ?? const SizedBox.expand()), + bar, + ], + ), + ), + ), + ), + ); + } + + group('BottomActionBar', () { + testWidgets('sem resultado, compartilhar fica apagado e desabilitado', ( + tester, + ) async { + final handle = tester.ensureSemantics(); + await pump( + tester, + BottomActionBar(primaryLabel: 'Calcular', onPrimary: () {}), + ); + + final opacity = tester.widget( + find.ancestor( + of: find.byType(OutlinedButton), + matching: find.byType(Opacity), + ), + ); + expect(opacity.opacity, 0.4); + expect( + tester.widget(find.byType(OutlinedButton)).onPressed, + isNull, + ); + expect( + tester.getSemantics(find.bySemanticsLabel(BottomActionBar.shareLabel)), + isSemantics(isButton: true, hasEnabledState: true, isEnabled: false), + ); + handle.dispose(); + }); + + testWidgets('com resultado, compartilhar fica ativo', (tester) async { + var shared = 0; + await pump( + tester, + BottomActionBar( + primaryLabel: 'Calcular', + onPrimary: () {}, + onShare: () => shared++, + ), + ); + + final opacity = tester.widget( + find.ancestor( + of: find.byType(OutlinedButton), + matching: find.byType(Opacity), + ), + ); + expect(opacity.opacity, 1); + await tester.tap(find.bySemanticsLabel(BottomActionBar.shareLabel)); + expect(shared, 1); + }); + + testWidgets('botões de 56px, principal em pílula na cor da calculadora ' + 'e compartilhar com contorno hairline', (tester) async { + var calculated = 0; + await pump( + tester, + BottomActionBar( + primaryLabel: 'Calcular', + onPrimary: () => calculated++, + ), + ); + + expect(tester.getSize(find.byType(FilledButton)).height, 56); + expect(tester.getSize(find.byType(OutlinedButton)), const Size(56, 56)); + + final primary = tester.widget(find.byType(FilledButton)); + final palette = CalculatorPalette.of(CalculatorKind.deposits); + expect(primary.style!.backgroundColor!.resolve({}), palette.solid); + expect(primary.style!.shape!.resolve({}), isA()); + + final share = tester.widget(find.byType(OutlinedButton)); + expect(share.style!.side!.resolve({})!.color, AppColors.light.hairline); + + await tester.tap(find.text('Calcular')); + expect(calculated, 1); + }); + + testWidgets('fundo background e padding 12/24/30', (tester) async { + await pump( + tester, + BottomActionBar(primaryLabel: 'Calcular', onPrimary: () {}), + ); + + final bar = tester.getRect(find.byType(BottomActionBar)); + final share = tester.getRect(find.byType(OutlinedButton)); + final primary = tester.getRect(find.byType(FilledButton)); + + expect(share.left - bar.left, 24); + expect(bar.right - primary.right, 24); + expect(share.top - bar.top, 12); + expect(bar.bottom - share.bottom, 30); + expect( + tester + .widget( + find + .descendant( + of: find.byType(BottomActionBar), + matching: find.byType(ColoredBox), + ) + .first, + ) + .color, + AppColors.light.background, + ); + }); + + testWidgets('respeita a área segura de baixo', (tester) async { + await pump( + tester, + BottomActionBar(primaryLabel: 'Calcular', onPrimary: () {}), + viewPadding: const EdgeInsets.only(bottom: 48), + ); + + final bar = tester.getRect(find.byType(BottomActionBar)); + final share = tester.getRect(find.byType(OutlinedButton)); + expect(bar.bottom, screen.height); + expect(bar.bottom - share.bottom, 48); + }); + + testWidgets('sobe com o teclado sem cobrir o campo em foco', ( + tester, + ) async { + await pump( + tester, + BottomActionBar(primaryLabel: 'Calcular', onPrimary: () {}), + viewInsets: const EdgeInsets.only(bottom: 300), + content: ListView( + children: [ + const SizedBox(height: 600), + TextField( + key: const Key('campo'), + autofocus: true, + scrollPadding: const EdgeInsets.only(bottom: 120), + ), + ], + ), + ); + await tester.pumpAndSettle(); + + final bar = tester.getRect(find.byType(BottomActionBar)); + final field = tester.getRect(find.byKey(const Key('campo'))); + expect(bar.bottom, screen.height - 300); + expect(field.bottom, lessThanOrEqualTo(bar.top)); + }); + }); + + group('showFieldsClearedSnackBar', () { + testWidgets('mostra "Campos limpos" com "Desfazer" por 4s', (tester) async { + var undone = 0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => TextButton( + onPressed: () => + showFieldsClearedSnackBar(context, onUndo: () => undone++), + child: const Text('limpar'), + ), + ), + ), + ), + ); + + await tester.tap(find.text('limpar')); + await tester.pumpAndSettle(); + expect(find.text('Campos limpos'), findsOneWidget); + + await tester.tap(find.text('Desfazer')); + await tester.pumpAndSettle(); + expect(undone, 1); + + await tester.tap(find.text('limpar')); + await tester.pumpAndSettle(); + await tester.pump(const Duration(seconds: 3)); + expect(find.text('Campos limpos'), findsOneWidget); + await tester.pump(const Duration(seconds: 2)); + await tester.pumpAndSettle(); + expect(find.text('Campos limpos'), findsNothing); + }); + }); +} diff --git a/test/core/widgets/calculator_band/calculator_band_test.dart b/test/core/widgets/calculator_band/calculator_band_test.dart new file mode 100644 index 0000000..d5ce7f8 --- /dev/null +++ b/test/core/widgets/calculator_band/calculator_band_test.dart @@ -0,0 +1,225 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:real_calc/core/themes/app_theme.dart'; +import 'package:real_calc/core/themes/calculator_palette.dart'; +import 'package:real_calc/core/themes/calculator_scope.dart'; +import 'package:real_calc/core/widgets/calculator_band/band_statement.dart'; +import 'package:real_calc/core/widgets/calculator_band/calculator_band.dart'; + +const initial = BandStatement( + kicker: 'Descubra quanto fica', + value: 'a prestação', + sub: 'de um financiamento', +); +const question = BandStatement( + kicker: 'Vamos descobrir', + value: 'o prazo?', + sub: 'Preencha os outros três campos', +); +const answer = BandStatement( + kicker: 'Sua prestação fica em', + value: '236,40', + isMoney: true, + sub: 'por 12 meses', +); +const warning = BandStatement( + kicker: 'Para calcular, falta', + value: 'a taxa', + sub: 'Preencha a taxa de juros ao mês', +); + +void main() { + Future pump( + WidgetTester tester, { + BandStatement statement = initial, + CalculatorKind kind = CalculatorKind.financing, + VoidCallback? onBack, + VoidCallback? onHelp, + VoidCallback? onClear, + double width = 400, + double textScale = 1, + }) async { + tester.view.physicalSize = Size(width, 800); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.lightTheme, + home: MediaQuery( + data: MediaQueryData( + size: Size(width, 800), + padding: const EdgeInsets.only(top: 24), + textScaler: TextScaler.linear(textScale), + ), + child: Scaffold( + body: CalculatorScope( + kind: kind, + child: Align( + alignment: Alignment.topCenter, + child: CalculatorBand( + title: 'Financiamento', + statement: statement, + onBack: onBack, + onHelp: onHelp, + onClear: onClear, + ), + ), + ), + ), + ), + ), + ); + } + + double valueFontSize(WidgetTester tester, String value) => + tester.widget(find.text(value)).style!.fontSize!; + + group('CalculatorBand', () { + testWidgets('a frase muda entre inicial, pergunta, resposta e aviso', ( + tester, + ) async { + for (final statement in [initial, question, answer, warning]) { + await pump(tester, statement: statement); + + expect(find.text(statement.kicker!), findsOneWidget); + expect(find.text(statement.value), findsOneWidget); + expect(find.text(statement.sub!), findsOneWidget); + expect( + find.text('R\$'), + statement.isMoney ? findsOneWidget : findsNothing, + ); + } + }); + + testWidgets('a frase é uma liveRegion com o texto completo', ( + tester, + ) async { + final handle = tester.ensureSemantics(); + await pump(tester, statement: answer); + + expect( + find.bySemanticsLabel('Sua prestação fica em R\$ 236,40 por 12 meses'), + findsOneWidget, + ); + final node = tester.getSemantics( + find.bySemanticsLabel(RegExp('Sua prestação')), + ); + expect(node, isSemantics(isLiveRegion: true)); + handle.dispose(); + }); + + testWidgets('fundo na cor da calculadora, ícones em onSolid e barra de ' + 'status com ícones claros nas quatro cores', (tester) async { + for (final kind in CalculatorKind.values) { + await pump(tester, kind: kind, onHelp: () {}, onClear: () {}); + final palette = CalculatorPalette.of(kind); + + final background = tester.widget( + find + .descendant( + of: find.byType(CalculatorBand), + matching: find.byType(ColoredBox), + ) + .first, + ); + expect(background.color, palette.solid); + + final region = tester.widget>( + find.descendant( + of: find.byType(CalculatorBand), + matching: find.byType(AnnotatedRegion), + ), + ); + expect(region.value.statusBarIconBrightness, Brightness.light); + expect(region.value.statusBarBrightness, Brightness.dark); + + for (final button in tester.widgetList( + find.byType(IconButton), + )) { + expect(button.color, palette.onSolid); + } + } + }); + + testWidgets('fica atrás da barra de status', (tester) async { + await pump(tester); + + expect(tester.getTopLeft(find.byType(CalculatorBand)).dy, 0); + expect( + tester.getTopLeft(find.text('Financiamento')).dy, + greaterThanOrEqualTo(24), + ); + }); + + testWidgets('botões com rótulos, área de 44px e ações', (tester) async { + final handle = tester.ensureSemantics(); + final taps = []; + await pump( + tester, + onBack: () => taps.add('voltar'), + onHelp: () => taps.add('ajuda'), + onClear: () => taps.add('limpar'), + ); + + for (final label in [ + CalculatorBand.backLabel, + CalculatorBand.helpLabel, + CalculatorBand.clearLabel, + ]) { + final button = find.bySemanticsLabel(label); + expect(button, findsOneWidget); + expect( + tester.getSemantics(button), + isSemantics(isButton: true, hasTapAction: true), + ); + expect(tester.getSize(button), const Size.square(44)); + await tester.tap(button); + } + expect(taps, ['voltar', 'ajuda', 'limpar']); + handle.dispose(); + }); + + testWidgets('sem onHelp e onClear, só mostra voltar', (tester) async { + await pump(tester); + + expect(find.byType(IconButton), findsOneWidget); + expect(find.byIcon(Icons.arrow_back), findsOneWidget); + }); + + testWidgets('valor curto fica em 48px', (tester) async { + await pump(tester, statement: answer); + + expect(valueFontSize(tester, '236,40'), 48); + }); + + testWidgets('em 360dp e fonte em 130%, as frases quebram e o número ' + 'encolhe sem cortar', (tester) async { + const long = BandStatement( + kicker: 'O total que você vai pagar ao fim do financiamento é de', + value: '1.234.567,89', + isMoney: true, + sub: 'somando todas as 360 prestações mensais com os juros', + ); + await pump(tester, statement: long, width: 360, textScale: 1.3); + + expect(tester.takeException(), isNull); + + final kickerHeight = tester.getSize(find.text(long.kicker!)).height; + final subHeight = tester.getSize(find.text(long.sub!)).height; + final oneLine = 15 * 1.3; + expect(kickerHeight, greaterThan(oneLine * 1.5)); + expect(subHeight, greaterThan(oneLine * 1.5)); + + final fontSize = valueFontSize(tester, long.value); + expect(fontSize, lessThan(48)); + expect(fontSize, greaterThanOrEqualTo(CalculatorBand.minValueSize)); + + final band = tester.getRect(find.byType(CalculatorBand)); + final value = tester.getRect(find.text(long.value)); + expect(value.left, greaterThanOrEqualTo(band.left)); + expect(value.right, lessThanOrEqualTo(band.right)); + }); + }); +} From 48e6fa6202acd52f2f688dff17135c442ee194b8 Mon Sep 17 00:00:00 2001 From: Rodrigo Mamede <61851910+Rodrigosdk@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:03:57 -0300 Subject: [PATCH 09/22] feat: sheet de ajuda contextual para calculadoras (#114) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: adicionar AppSheet, folha modal com alça, título, fechar e rolagem interna (#114) * feat: adicionar HelpSheet com o conteúdo e as fórmulas das quatro calculadoras (#114) --- lib/core/widgets/app_sheet.dart | 136 +++++++++++ lib/core/widgets/help_sheet/help_content.dart | 128 +++++++++++ lib/core/widgets/help_sheet/help_sheet.dart | 164 +++++++++++++ .../widgets/help_sheet/help_sheet_test.dart | 217 ++++++++++++++++++ 4 files changed, 645 insertions(+) create mode 100644 lib/core/widgets/app_sheet.dart create mode 100644 lib/core/widgets/help_sheet/help_content.dart create mode 100644 lib/core/widgets/help_sheet/help_sheet.dart create mode 100644 test/core/widgets/help_sheet/help_sheet_test.dart diff --git a/lib/core/widgets/app_sheet.dart b/lib/core/widgets/app_sheet.dart new file mode 100644 index 0000000..2f96ca9 --- /dev/null +++ b/lib/core/widgets/app_sheet.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:real_calc/core/themes/app_colors.dart'; +import 'package:real_calc/core/themes/calculator_palette.dart'; +import 'package:real_calc/core/themes/calculator_scope.dart'; +import 'package:real_calc/core/themes/radius.dart'; +import 'package:real_calc/core/themes/spacing.dart'; +import 'package:real_calc/core/themes/text_styles.dart'; + +Future showAppSheet({ + required BuildContext context, + required String title, + required WidgetBuilder bodyBuilder, + WidgetBuilder? footerBuilder, + CalculatorKind? kind, +}) { + final colors = AppColors.of(context); + + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: colors.sheet, + barrierColor: colors.scrim, + clipBehavior: Clip.antiAlias, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(AppRadius.sheet), + ), + ), + constraints: BoxConstraints( + maxHeight: MediaQuery.sizeOf(context).height - AppSheet.topGap, + ), + builder: (context) { + final sheet = AppSheet( + title: title, + body: Builder(builder: bodyBuilder), + footer: footerBuilder == null ? null : Builder(builder: footerBuilder), + ); + return kind == null ? sheet : CalculatorScope(kind: kind, child: sheet); + }, + ); +} + +class AppSheet extends StatelessWidget { + static const double topGap = 96; + static const closeLabel = 'Fechar'; + + final String title; + final Widget body; + final Widget? footer; + + const AppSheet({ + super.key, + required this.title, + required this.body, + this.footer, + }); + + @override + Widget build(BuildContext context) { + final colors = AppColors.of(context); + final bottomInset = MediaQuery.viewPaddingOf(context).bottom; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.only(top: 10), + child: Center( + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: colors.textHint.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(AppSpacing.screen, 8, 12, 8), + child: Row( + children: [ + Expanded( + child: Semantics( + header: true, + child: Text( + title, + style: AppTextStyles.sheetTitle.copyWith( + color: colors.textPrimary, + ), + ), + ), + ), + MergeSemantics( + child: Semantics( + label: closeLabel, + child: SizedBox.square( + dimension: 44, + child: IconButton( + onPressed: () => Navigator.of(context).maybePop(), + padding: EdgeInsets.zero, + color: colors.textSecondary, + icon: const Icon(Icons.close, size: 22), + ), + ), + ), + ), + ], + ), + ), + Flexible( + child: SingleChildScrollView( + padding: EdgeInsets.fromLTRB( + AppSpacing.screen, + AppSpacing.sm, + AppSpacing.screen, + footer == null ? AppSpacing.lg + bottomInset : AppSpacing.md, + ), + child: body, + ), + ), + if (footer != null) + Padding( + padding: EdgeInsets.fromLTRB( + AppSpacing.screen, + AppSpacing.sm, + AppSpacing.screen, + AppSpacing.md + bottomInset, + ), + child: footer, + ), + ], + ); + } +} diff --git a/lib/core/widgets/help_sheet/help_content.dart b/lib/core/widgets/help_sheet/help_content.dart new file mode 100644 index 0000000..981496f --- /dev/null +++ b/lib/core/widgets/help_sheet/help_content.dart @@ -0,0 +1,128 @@ +import 'package:real_calc/core/themes/calculator_palette.dart'; + +class HelpField { + final String name; + final String description; + + const HelpField(this.name, this.description); +} + +class HelpContent { + final String intro; + final List fields; + final String example; + final String exampleResult; + final String methodology; + final String formula; + + const HelpContent({ + required this.intro, + required this.fields, + required this.example, + required this.exampleResult, + required this.methodology, + required this.formula, + }); + + static HelpContent of(CalculatorKind kind) => switch (kind) { + CalculatorKind.financing => financing, + CalculatorKind.deposits => deposits, + CalculatorKind.futureValue => futureValue, + CalculatorKind.correction => correction, + }; + + static const _rate = HelpField( + 'Taxa de juros', + 'Os juros de cada mês, em porcentagem.', + ); + + static const financing = HelpContent( + intro: + 'Descubra a prestação, o valor financiado, o prazo ou a taxa. ' + 'Preencha três campos e o app calcula o que falta.', + fields: [ + HelpField( + 'Valor financiado', + 'Quanto você vai pegar emprestado, sem os juros.', + ), + HelpField('Prazo', 'Em quantos meses a dívida será paga.'), + _rate, + HelpField('Prestação', 'O valor fixo pago todo mês.'), + ], + example: 'R\$ 2.500,00 a 2% ao mês, em 12 meses, dão uma prestação de', + exampleResult: 'R\$ 236,40', + methodology: + 'Usamos a tabela Price, com prestações iguais. V é o valor ' + 'financiado, i a taxa mensal e n o prazo.', + formula: 'P = V × i ÷ (1 − (1 + i)⁻ⁿ)', + ); + + static const deposits = HelpContent( + intro: + 'Veja quanto junta guardando o mesmo valor todo mês, ou descubra o ' + 'depósito, o prazo ou a taxa para chegar a um objetivo.', + fields: [ + HelpField( + 'Depósito mensal', + 'O valor guardado todo mês, sempre no início do mês.', + ), + HelpField('Prazo', 'Por quantos meses você vai depositar.'), + _rate, + HelpField('Valor final', 'Quanto você terá no fim do prazo.'), + ], + example: 'R\$ 100,00 por mês a 1% ao mês, durante 12 meses, viram', + exampleResult: 'R\$ 1.280,93', + methodology: + 'O depósito é feito no início de cada mês e já rende no próprio ' + 'mês. D é o depósito, i a taxa mensal e n o prazo.', + formula: 'VF = D × (1 + i) × ((1 + i)ⁿ − 1) ÷ i', + ); + + static const futureValue = HelpContent( + intro: + 'Veja quanto um valor aplicado hoje rende com juros compostos, ou ' + 'descubra o capital, o prazo ou a taxa.', + fields: [ + HelpField('Capital', 'O valor aplicado hoje, uma única vez.'), + HelpField('Prazo', 'Por quantos meses o dinheiro fica aplicado.'), + _rate, + HelpField('Valor final', 'Quanto o capital vale no fim do prazo.'), + ], + example: 'R\$ 1.000,00 a 1% ao mês, durante 12 meses, viram', + exampleResult: 'R\$ 1.126,83', + methodology: + 'Juros compostos: os juros de cada mês também passam a render. VP ' + 'é o capital, i a taxa mensal e n o prazo.', + formula: 'VF = VP × (1 + i)ⁿ', + ); + + static const correction = HelpContent( + intro: + 'Atualize um valor pela inflação ou por outro índice entre duas ' + 'datas, com os dados oficiais do Banco Central.', + fields: [ + HelpField( + 'Índice de correção', + 'A série usada na correção, como IPCA, IGP-M ou CDI.', + ), + HelpField('Data inicial', 'De quando o valor parte.'), + HelpField('Data final', 'Até quando o valor é corrigido.'), + HelpField( + 'Percentual', + 'Só no CDI: quanto do CDI aplicar, como 100% ou 110%.', + ), + HelpField( + 'Valor a ser corrigido', + 'O valor na data inicial. Sem ele, mostramos só o fator.', + ), + ], + example: + 'Se o índice subiu 0,5% em um mês e 0,4% no seguinte, ' + 'R\$ 1.000,00 viram', + exampleResult: 'R\$ 1.009,02', + methodology: + 'Multiplicamos as variações de cada período e aplicamos o fator ao ' + 'valor. i₁ … iₙ são as variações do índice no período.', + formula: 'fator = (1 + i₁) × … × (1 + iₙ)', + ); +} diff --git a/lib/core/widgets/help_sheet/help_sheet.dart b/lib/core/widgets/help_sheet/help_sheet.dart new file mode 100644 index 0000000..c63ac05 --- /dev/null +++ b/lib/core/widgets/help_sheet/help_sheet.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import 'package:real_calc/core/themes/app_colors.dart'; +import 'package:real_calc/core/themes/calculator_palette.dart'; +import 'package:real_calc/core/themes/calculator_scope.dart'; +import 'package:real_calc/core/themes/spacing.dart'; +import 'package:real_calc/core/themes/text_styles.dart'; + +import '../app_sheet.dart'; +import 'help_content.dart'; + +const helpSheetTitle = 'Como funciona'; + +Future showHelpSheet(BuildContext context, CalculatorKind kind) { + return showAppSheet( + context: context, + title: helpSheetTitle, + kind: kind, + bodyBuilder: (_) => HelpSheetBody(content: HelpContent.of(kind)), + footerBuilder: (context) => const _UnderstoodButton(), + ); +} + +class HelpSheetBody extends StatelessWidget { + final HelpContent content; + + const HelpSheetBody({super.key, required this.content}); + + @override + Widget build(BuildContext context) { + final colors = AppColors.of(context); + final palette = CalculatorScope.of(context); + final body = AppTextStyles.body.copyWith(color: colors.textPrimary); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(content.intro, style: body), + _Section(title: 'O que cada campo significa', colors: colors), + DecoratedBox( + decoration: BoxDecoration( + border: Border(top: BorderSide(color: colors.hairline)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + for (final field in content.fields) + _FieldDescription(field: field, colors: colors), + ], + ), + ), + _Section(title: 'Exemplo', colors: colors), + Text(content.example, style: body), + const SizedBox(height: AppSpacing.xs), + Text( + content.exampleResult, + style: AppTextStyles.exampleValue.copyWith(color: palette.text), + ), + _Section(title: 'Como calculamos', colors: colors), + Text( + content.methodology, + style: AppTextStyles.body.copyWith(color: colors.textSecondary), + ), + const SizedBox(height: AppSpacing.sm), + Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + color: colors.elevated, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + content.formula, + style: AppTextStyles.detailValue.copyWith( + color: colors.textPrimary, + ), + ), + ), + ], + ); + } +} + +class _Section extends StatelessWidget { + final String title; + final AppColors colors; + + const _Section({required this.title, required this.colors}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only( + top: AppSpacing.sectionGap, + bottom: AppSpacing.sm, + ), + child: Semantics( + header: true, + child: Text( + title, + style: AppTextStyles.sectionLabel.copyWith( + color: colors.textSecondary, + ), + ), + ), + ); + } +} + +class _FieldDescription extends StatelessWidget { + final HelpField field; + final AppColors colors; + + const _FieldDescription({required this.field, required this.colors}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: colors.hairline)), + ), + child: MergeSemantics( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + field.name, + style: AppTextStyles.rowLabel.copyWith(color: colors.textPrimary), + ), + const SizedBox(height: 2), + Text( + field.description, + style: AppTextStyles.listDesc.copyWith( + color: colors.textSecondary, + ), + ), + ], + ), + ), + ); + } +} + +class _UnderstoodButton extends StatelessWidget { + const _UnderstoodButton(); + + @override + Widget build(BuildContext context) { + final palette = CalculatorScope.of(context); + + return SizedBox( + height: 56, + child: FilledButton( + onPressed: () => Navigator.of(context).pop(), + style: FilledButton.styleFrom( + backgroundColor: palette.solid, + foregroundColor: palette.onSolid, + shape: const StadiumBorder(), + textStyle: AppTextStyles.button, + ), + child: const Text('Entendi'), + ), + ); + } +} diff --git a/test/core/widgets/help_sheet/help_sheet_test.dart b/test/core/widgets/help_sheet/help_sheet_test.dart new file mode 100644 index 0000000..0bfc136 --- /dev/null +++ b/test/core/widgets/help_sheet/help_sheet_test.dart @@ -0,0 +1,217 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:real_calc/core/themes/app_theme.dart'; +import 'package:real_calc/core/themes/calculator_palette.dart'; +import 'package:real_calc/core/themes/calculator_scope.dart'; +import 'package:real_calc/core/utils/br_number_format.dart'; +import 'package:real_calc/core/widgets/app_sheet.dart'; +import 'package:real_calc/core/widgets/calculator_band/band_statement.dart'; +import 'package:real_calc/core/widgets/calculator_band/calculator_band.dart'; +import 'package:real_calc/core/widgets/help_sheet/help_content.dart'; +import 'package:real_calc/core/widgets/help_sheet/help_sheet.dart'; + +void main() { + Future pump( + WidgetTester tester, { + CalculatorKind kind = CalculatorKind.financing, + Size size = const Size(400, 800), + }) async { + tester.view.physicalSize = size; + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.lightTheme, + home: Scaffold( + body: CalculatorScope( + kind: kind, + child: Builder( + builder: (context) => Align( + alignment: Alignment.topCenter, + child: CalculatorBand( + title: 'Calculadora', + statement: const BandStatement(value: 'x'), + onHelp: () => showHelpSheet(context, kind), + ), + ), + ), + ), + ), + ), + ); + } + + Future open(WidgetTester tester) async { + await tester.tap(find.byIcon(Icons.help_outline)); + await tester.pumpAndSettle(); + } + + final sheet = find.byType(AppSheet); + + group('HelpSheet', () { + testWidgets('abre pelo "?" com o conteúdo da calculadora', (tester) async { + await pump(tester); + await open(tester); + + const content = HelpContent.financing; + expect(find.text(helpSheetTitle), findsOneWidget); + expect(find.text(content.intro), findsOneWidget); + for (final field in content.fields) { + expect(find.text(field.name), findsOneWidget); + expect(find.text(field.description), findsOneWidget); + } + expect(find.text(content.exampleResult), findsOneWidget); + expect(find.text(content.formula), findsOneWidget); + expect(find.text('Entendi'), findsOneWidget); + expect(find.byType(Icon).evaluate().length, lessThanOrEqualTo(3)); + }); + + testWidgets('topo a 96px', (tester) async { + await pump(tester, size: const Size(400, 640)); + await open(tester); + + expect(tester.getTopLeft(sheet).dy, greaterThanOrEqualTo(96)); + }); + + testWidgets('exemplo e "Entendi" na cor da calculadora', (tester) async { + for (final kind in CalculatorKind.values) { + await pump(tester, kind: kind); + await open(tester); + final palette = CalculatorPalette.of(kind); + + final example = tester.widget( + find.text(HelpContent.of(kind).exampleResult), + ); + expect(example.style!.color, palette.text); + final button = tester.widget(find.byType(FilledButton)); + expect(button.style!.backgroundColor!.resolve({}), palette.solid); + + await tester.tap(find.text('Entendi')); + await tester.pumpAndSettle(); + } + }); + + group('fecha', () { + Future expectCloses( + WidgetTester tester, + Future Function() action, + ) async { + await pump(tester); + await open(tester); + expect(sheet, findsOneWidget); + await action(); + await tester.pumpAndSettle(); + expect(sheet, findsNothing); + } + + testWidgets('no X', (tester) async { + await expectCloses( + tester, + () => tester.tap(find.bySemanticsLabel(AppSheet.closeLabel)), + ); + }); + + testWidgets('em "Entendi"', (tester) async { + await expectCloses(tester, () => tester.tap(find.text('Entendi'))); + }); + + testWidgets('tocando fora', (tester) async { + await expectCloses(tester, () => tester.tapAt(const Offset(20, 20))); + }); + + testWidgets('arrastando para baixo', (tester) async { + await expectCloses( + tester, + () => tester.fling( + find.text(helpSheetTitle), + const Offset(0, 500), + 2000, + ), + ); + }); + + testWidgets('com o voltar do Android', (tester) async { + await expectCloses(tester, () => tester.binding.handlePopRoute()); + }); + }); + + testWidgets('em 640dp, o conteúdo rola e "Entendi" continua visível', ( + tester, + ) async { + await pump(tester, size: const Size(360, 640)); + await open(tester); + + Rect understood() => tester.getRect(find.text('Entendi')); + expect(understood().bottom, lessThanOrEqualTo(640)); + + final scrollable = find.descendant( + of: sheet, + matching: find.byType(Scrollable), + ); + expect( + tester.state(scrollable).position.maxScrollExtent, + greaterThan(0), + ); + + await tester.scrollUntilVisible( + find.text(HelpContent.financing.formula), + 100, + scrollable: scrollable, + ); + expect(find.text(HelpContent.financing.formula).hitTestable(), findsOne); + expect(understood().bottom, lessThanOrEqualTo(640)); + }); + + testWidgets('é anunciada como diálogo', (tester) async { + final handle = tester.ensureSemantics(); + await pump(tester); + await open(tester); + + expect( + tester.getSemantics(find.bySemanticsLabel('Dialog')), + isSemantics(namesRoute: true, scopesRoute: true), + ); + handle.dispose(); + }); + + testWidgets('devolve o foco ao "?" ao fechar', (tester) async { + await pump(tester); + final helpFocus = Focus.of( + tester.element(find.byIcon(Icons.help_outline)), + ); + helpFocus.requestFocus(); + await tester.pump(); + + await open(tester); + expect(helpFocus.hasFocus, isFalse); + + await tester.tap(find.text('Entendi')); + await tester.pumpAndSettle(); + expect(helpFocus.hasFocus, isTrue); + }); + }); + + group('HelpContent', () { + double price(double v, double i, int n) => v * i / (1 - pow(1 + i, -n)); + + String money(num value) => formatCents( + toCents(value.toDouble()), + ).replaceAll(String.fromCharCode(0xA0), ' '); + + test('os exemplos batem com as fórmulas', () { + expect(HelpContent.financing.exampleResult, money(price(2500, 0.02, 12))); + expect( + HelpContent.deposits.exampleResult, + money(100 * 1.01 * (pow(1.01, 12) - 1) / 0.01), + ); + expect( + HelpContent.futureValue.exampleResult, + money(1000 * pow(1.01, 12)), + ); + expect(HelpContent.correction.exampleResult, money(1000 * 1.005 * 1.004)); + }); + }); +} From e736e503acfb6f55e70a3ce695ba0f48b20c55f2 Mon Sep 17 00:00:00 2001 From: Rodrigo Mamede <61851910+Rodrigosdk@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:02:37 -0300 Subject: [PATCH 10/22] feat: migrar Financiamento para a Vibrante minimalista (#123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: adicionar FinancingStatement para montar a frase da faixa do Financiamento (#123) * fix: escrever a taxa calculada com duas casas no FinancingFormCubit (#123) * feat: migrar a FinancingPage para faixa, lista de campos, detalhes e barra de ações (#123) --- lib/core/widgets/form_notice.dart | 61 ++++ lib/modules/financial_calculators/module.dart | 5 +- .../cubit/forms/financing_form_cubit.dart | 2 +- .../presentation/pages/financing_page.dart | 263 +++++++++++----- .../utils/financing_statement.dart | 109 +++++++ .../forms/financing_form_cubit_test.dart | 15 + .../pages/financing_page_test.dart | 280 ++++++++++++++---- .../utils/financing_statement_test.dart | 136 +++++++++ 8 files changed, 733 insertions(+), 138 deletions(-) create mode 100644 lib/core/widgets/form_notice.dart create mode 100644 lib/modules/financial_calculators/presentation/utils/financing_statement.dart create mode 100644 test/modules/financial_calculators/presentation/utils/financing_statement_test.dart diff --git a/lib/core/widgets/form_notice.dart b/lib/core/widgets/form_notice.dart new file mode 100644 index 0000000..273b0fa --- /dev/null +++ b/lib/core/widgets/form_notice.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:real_calc/core/themes/app_colors.dart'; +import 'package:real_calc/core/themes/text_styles.dart'; + +class FormNotice extends StatelessWidget { + final String title; + final String message; + final bool isError; + + const FormNotice({ + super.key, + required this.title, + required this.message, + this.isError = false, + }); + + @override + Widget build(BuildContext context) { + final colors = AppColors.of(context); + final accent = isError ? colors.error : colors.textSecondary; + + return MergeSemantics( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 1), + child: Icon( + isError ? Icons.error_outline : Icons.info_outline, + size: 20, + color: accent, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: AppTextStyles.rowLabel.copyWith( + color: isError ? colors.error : colors.textPrimary, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 2), + Text( + message, + style: AppTextStyles.body.copyWith( + color: colors.textSecondary, + fontSize: 14, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/financial_calculators/module.dart b/lib/modules/financial_calculators/module.dart index fa25c11..d75033a 100644 --- a/lib/modules/financial_calculators/module.dart +++ b/lib/modules/financial_calculators/module.dart @@ -41,10 +41,7 @@ class FinancialCalculatorsModule extends AppModule { BlocProvider(create: (_) => Modular.get()), BlocProvider(create: (_) => Modular.get()), ], - child: const CalculatorScope( - kind: CalculatorKind.financing, - child: FinancingPage(), - ), + child: const FinancingPage(), ), ); r.child( diff --git a/lib/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit.dart b/lib/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit.dart index af46179..1798533 100644 --- a/lib/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit.dart +++ b/lib/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit.dart @@ -118,7 +118,7 @@ class FinancingFormCubit extends Cubit { _suppressRecompute = true; _setFormattedValue(initialValue, result.initialValue); months.text = result.periods > 0 ? result.periods.toString() : ''; - _setFormattedValue(rate, result.rate, fractionDigits: 4); + _setFormattedValue(rate, result.rate); _setFormattedValue(finalValue, result.finalValue); _suppressRecompute = false; diff --git a/lib/modules/financial_calculators/presentation/pages/financing_page.dart b/lib/modules/financial_calculators/presentation/pages/financing_page.dart index d37afe4..e209648 100644 --- a/lib/modules/financial_calculators/presentation/pages/financing_page.dart +++ b/lib/modules/financial_calculators/presentation/pages/financing_page.dart @@ -1,122 +1,223 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:real_calc/core/themes/calculator_palette.dart'; +import 'package:real_calc/core/themes/calculator_scope.dart'; import 'package:real_calc/core/themes/spacing.dart'; -import 'package:real_calc/core/widgets/page_header.dart'; -import 'package:real_calc/core/widgets/title_widget.dart'; -import 'package:real_calc/modules/financial_calculators/presentation/models/field_spec.dart'; -import 'package:real_calc/modules/financial_calculators/presentation/widgets/forms_view.dart'; -import 'package:real_calc/core/widgets/help_card.dart'; +import 'package:real_calc/core/utils/decimal_input_formatter.dart'; +import 'package:real_calc/core/widgets/bottom_action_bar.dart'; +import 'package:real_calc/core/widgets/calculator_band/calculator_band.dart'; +import 'package:real_calc/core/widgets/field_list/field_list.dart'; +import 'package:real_calc/core/widgets/field_list/field_row.dart'; +import 'package:real_calc/core/widgets/form_notice.dart'; +import 'package:real_calc/core/widgets/help_sheet/help_sheet.dart'; +import 'package:real_calc/core/widgets/result_details/result_details.dart'; -import '../../../../core/utils/decimal_input_formatter.dart'; import '../../domain/enum/financial_calculation_target.dart'; -import '../../../../core/widgets/types/input_field_state.dart'; import '../cubit/financing/financing_cubit.dart'; import '../cubit/forms/financing_form_cubit.dart'; +import '../utils/financing_statement.dart'; +import '../utils/result_summaries.dart'; + +class _FieldConfig { + final FinancialCalculationTarget target; + final String label; + final String? prefix; + final String? unit; + final String? secondLine; + final String hint; + final TextInputType keyboardType; + final List formatters; + + const _FieldConfig({ + required this.target, + required this.label, + required this.hint, + required this.keyboardType, + required this.formatters, + this.prefix, + this.unit, + this.secondLine, + }); +} class FinancingPage extends StatelessWidget { - static final List> _fieldSpecs = [ - FieldSpec( - field: FinancialCalculationTarget.initialValue, - label: 'Valor financiado (R\$)', + static const title = 'Financiamento'; + + static final _fields = [ + _FieldConfig( + target: FinancialCalculationTarget.initialValue, + label: 'Valor financiado', + prefix: 'R\$', hint: '0,00', - icon: Icons.attach_money, + keyboardType: const TextInputType.numberWithOptions(decimal: true), formatters: [DecimalInputFormatter()], ), - FieldSpec( - field: FinancialCalculationTarget.periods, - label: 'Prazo (meses)', + _FieldConfig( + target: FinancialCalculationTarget.periods, + label: 'Prazo', + unit: 'meses', hint: '0', - icon: Icons.calendar_today, + keyboardType: TextInputType.number, formatters: [FilteringTextInputFormatter.digitsOnly], ), - FieldSpec( - field: FinancialCalculationTarget.interestRate, - label: 'Taxa de juros (% ao mês)', + _FieldConfig( + target: FinancialCalculationTarget.interestRate, + label: 'Taxa de juros', + unit: '% a.m.', hint: '0,00', - icon: Icons.percent, + keyboardType: const TextInputType.numberWithOptions(decimal: true), formatters: [DecimalInputFormatter()], ), - FieldSpec( - field: FinancialCalculationTarget.finalValue, - label: 'Valor da prestação', + _FieldConfig( + target: FinancialCalculationTarget.finalValue, + label: 'Prestação', + prefix: 'R\$', + secondLine: 'todo mês', hint: '0,00', - icon: Icons.payments, + keyboardType: const TextInputType.numberWithOptions(decimal: true), formatters: [DecimalInputFormatter()], ), ]; const FinancingPage({super.key}); - BlocListener _buildForms() { - return BlocListener( - listener: (context, state) { - final formCubit = context.read(); - if (state is FinancingCalculated) { - formCubit.applyResult(state.value, state.calculatedField); - } else if (state is FinancingError) { - formCubit.invalidateCalculatedField(); - } - }, - child: BlocBuilder( - builder: (context, financingState) { - final formCubit = context.watch(); - final formState = formCubit.state; - - void calculate() { - if (formState.canCalculate) { - context.read().calculate(formCubit.financing); - } + @override + Widget build(BuildContext context) { + return CalculatorScope( + kind: CalculatorKind.financing, + child: BlocListener( + listener: (context, state) { + final formCubit = context.read(); + if (state is FinancingCalculated) { + formCubit.applyResult(state.value, state.calculatedField); + } else if (state is FinancingError) { + formCubit.invalidateCalculatedField(); } - - return FormsView( - fieldSpecs: _fieldSpecs, - controllerFor: formCubit.controllerFor, - stateOf: formState.stateOf, - isLoading: financingState is FinancingLoading, - errorMessage: financingState is FinancingError - ? financingState.message ?? 'Erro ao calcular.' - : null, - onCalculate: calculate, - onClear: formCubit.clear, - onFieldTap: (field) { - if (formState.stateOf(field) == InputFieldState.unknown) { - calculate(); - } - }, - ); }, + child: const _FinancingView(), ), ); } +} + +class _FinancingView extends StatelessWidget { + const _FinancingView(); @override Widget build(BuildContext context) { + final formCubit = context.watch(); + final formState = formCubit.state; + final financingState = context.watch().state; + + final emptyFields = { + for (final field in FinancialCalculationTarget.values) + if (formCubit.controllerFor(field).text.trim().isEmpty) field, + }; + final calculatedField = formState.calculatedField; + final values = formCubit.financing; + final filled = + FinancialCalculationTarget.values.length - emptyFields.length; + final error = financingState is FinancingError ? financingState : null; + + void calculate() { + if (formCubit.state.canCalculate) { + context.read().calculate(formCubit.financing); + } + } + + void clear() { + final snapshot = { + for (final field in FinancialCalculationTarget.values) + field: formCubit.controllerFor(field).text, + }; + final previouslyCalculated = formCubit.state.calculatedField; + FocusScope.of(context).unfocus(); + formCubit.clear(); + showFieldsClearedSnackBar( + context, + onUndo: () { + for (final MapEntry(key: field, value: text) in snapshot.entries) { + if (field != previouslyCalculated) { + formCubit.controllerFor(field).text = text; + } + } + if (previouslyCalculated != null) calculate(); + }, + ); + } + return Scaffold( - appBar: const PageHeader(title: 'Financiamento'), - body: SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md + 4, - vertical: AppSpacing.md + 4, + body: Column( + children: [ + CalculatorBand( + title: FinancingPage.title, + statement: financingStatement( + emptyFields: emptyFields, + values: values, + calculatedField: calculatedField, + ), + onHelp: () => showHelpSheet(context, CalculatorKind.financing), + onClear: clear, ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: AppSpacing.lg, - children: [ - const TitleWidget( - title: 'Financiamento', - subtitle: 'Calcule o valor da prestação do seu financiamento.', + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB( + AppSpacing.screen, + AppSpacing.sm, + AppSpacing.screen, + AppSpacing.lg, ), - const HelpCard( - message: - 'Preencha 3 campos e toque no 4° para calcular automaticamente', + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FieldList( + children: [ + for (final field in FinancingPage._fields) + FieldRow( + key: ValueKey(field.target), + label: field.label, + controller: formCubit.controllerFor(field.target), + state: formState.stateOf(field.target), + prefix: field.prefix, + unit: field.unit, + secondLine: field.secondLine, + hint: field.hint, + keyboardType: field.keyboardType, + inputFormatters: field.formatters, + onCalculate: calculate, + ), + ], + ), + if (error != null) + Padding( + padding: const EdgeInsets.only(top: AppSpacing.md), + child: FormNotice( + title: 'Não foi possível calcular', + message: error.message ?? 'Confira os valores.', + isError: true, + ), + ) + else if (filled > 0 && filled < 3) + Padding( + padding: const EdgeInsets.only(top: AppSpacing.md), + child: FormNotice( + title: 'Faltam dados para calcular', + message: financingMissingFieldsMessage(emptyFields), + ), + ), + if (calculatedField != null) + ResultDetails(summary: financingResultSummary(values)), + ], ), - _buildForms(), - ], + ), + ), + BottomActionBar( + primaryLabel: 'Calcular', + onPrimary: formState.canCalculate ? calculate : null, ), - ), + ], ), ); } -} \ No newline at end of file +} diff --git a/lib/modules/financial_calculators/presentation/utils/financing_statement.dart b/lib/modules/financial_calculators/presentation/utils/financing_statement.dart new file mode 100644 index 0000000..8d74195 --- /dev/null +++ b/lib/modules/financial_calculators/presentation/utils/financing_statement.dart @@ -0,0 +1,109 @@ +import 'package:intl/intl.dart'; +import 'package:real_calc/core/utils/br_number_format.dart'; +import 'package:real_calc/core/widgets/calculator_band/band_statement.dart'; + +import '../../domain/entities/financial_calculation.dart'; +import '../../domain/enum/financial_calculation_target.dart'; + +const _fieldOrder = [ + FinancialCalculationTarget.initialValue, + FinancialCalculationTarget.periods, + FinancialCalculationTarget.interestRate, + FinancialCalculationTarget.finalValue, +]; + +String financingFieldName(FinancialCalculationTarget field) => switch (field) { + FinancialCalculationTarget.initialValue => 'o valor financiado', + FinancialCalculationTarget.periods => 'o prazo', + FinancialCalculationTarget.interestRate => 'a taxa de juros', + FinancialCalculationTarget.finalValue => 'a prestação', +}; + +BandStatement financingStatement({ + required Set emptyFields, + required FinancialCalculation values, + FinancialCalculationTarget? calculatedField, +}) { + if (calculatedField != null) return _answer(calculatedField, values); + + if (emptyFields.length == _fieldOrder.length) { + return const BandStatement( + kicker: 'Informe três valores', + value: 'e descubra o quarto', + sub: 'Qualquer campo pode ser o resultado.', + ); + } + + if (emptyFields.length == 1) { + return BandStatement( + kicker: 'Você quer descobrir', + value: financingFieldName(emptyFields.single), + sub: 'Os outros três campos já estão preenchidos.', + ); + } + + if (emptyFields.isEmpty) { + return const BandStatement( + kicker: 'Para calcular, deixe', + value: '1 campo em branco', + sub: 'Apague o valor que você quer descobrir.', + ); + } + + final missing = emptyFields.length - 1; + return BandStatement( + kicker: 'Para calcular, ainda', + value: missing == 1 ? 'falta 1 campo' : 'faltam $missing campos', + sub: 'Deixe em branco só o que você quer descobrir.', + ); +} + +String financingMissingFieldsMessage(Set empty) { + final names = [ + for (final field in _fieldOrder) + if (empty.contains(field)) financingFieldName(field), + ]; + final list = names.length == 1 + ? names.single + : '${names.sublist(0, names.length - 1).join(', ')} ou ${names.last}'; + return 'Preencha $list. O campo que ficar em branco é calculado.'; +} + +final _money = NumberFormat('#,##0.00', 'pt_BR'); + +BandStatement _answer( + FinancialCalculationTarget field, + FinancialCalculation values, +) { + final installment = formatCents(toCents(values.finalValue)); + final periods = values.periods; + + return switch (field) { + FinancialCalculationTarget.finalValue => BandStatement( + kicker: 'Sua prestação vai ser', + value: _money.format(toCents(values.finalValue) / 100), + isMoney: true, + sub: periods == 1 + ? '1 parcela, pela Tabela Price' + : '$periods parcelas iguais, pela Tabela Price', + ), + FinancialCalculationTarget.initialValue => BandStatement( + kicker: 'Você pode financiar', + value: _money.format(toCents(values.initialValue) / 100), + isMoney: true, + sub: periods == 1 + ? 'em 1 parcela de $installment' + : 'em $periods parcelas de $installment', + ), + FinancialCalculationTarget.periods => BandStatement( + kicker: 'Você quita em', + value: periods == 1 ? '1 mês' : '$periods meses', + sub: 'com parcelas de $installment', + ), + FinancialCalculationTarget.interestRate => BandStatement( + kicker: 'A taxa de juros é de', + value: '${formatPercent(values.rate)} ao mês', + sub: '${formatPercent(annualEquivalentRate(values.rate))} ao ano', + ), + }; +} diff --git a/test/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit_test.dart b/test/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit_test.dart index 40f93be..dd97e5c 100644 --- a/test/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit_test.dart +++ b/test/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit_test.dart @@ -44,4 +44,19 @@ void main() { InputFieldState.unknown, ); }); + + test('escreve a taxa calculada com duas casas', () { + cubit.applyResult( + FinancialCalculation( + initialValue: 2500, + periods: 12, + rate: 2.0000694603100793, + finalValue: 236.40, + ), + FinancialCalculationTarget.interestRate, + ); + + expect(cubit.rate.text, '2,00'); + expect(cubit.financing.rate, 2); + }); } diff --git a/test/modules/financial_calculators/presentation/pages/financing_page_test.dart b/test/modules/financial_calculators/presentation/pages/financing_page_test.dart index 018f159..5646d4b 100644 --- a/test/modules/financial_calculators/presentation/pages/financing_page_test.dart +++ b/test/modules/financial_calculators/presentation/pages/financing_page_test.dart @@ -1,82 +1,258 @@ -import 'package:flutter/rendering.dart'; -import 'package:flutter_test/flutter_test.dart'; - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:real_calc/core/themes/app_theme.dart'; -import 'package:real_calc/core/widgets/page_header.dart'; -import 'package:real_calc/core/widgets/title_widget.dart'; +import 'package:real_calc/core/widgets/bottom_action_bar.dart'; +import 'package:real_calc/core/widgets/calculator_band/calculator_band.dart'; +import 'package:real_calc/core/widgets/field_list/field_row.dart'; +import 'package:real_calc/core/widgets/form_notice.dart'; +import 'package:real_calc/core/widgets/result_details/result_details.dart'; import 'package:real_calc/modules/financial_calculators/domain/enum/financial_calculation_target.dart'; import 'package:real_calc/modules/financial_calculators/presentation/cubit/financing/financing_cubit.dart'; import 'package:real_calc/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit.dart'; import 'package:real_calc/modules/financial_calculators/presentation/pages/financing_page.dart'; -import 'package:real_calc/modules/financial_calculators/presentation/widgets/forms_view.dart'; -import 'package:real_calc/core/widgets/help_card.dart'; - -import '../../../../test_harness.dart'; - +import 'package:real_calc/modules/financial_calculators/use_case/financing/calculate_financing.dart'; void main() { - late TestHarness harness; - + late FinancingCubit financingCubit; + late FinancingFormCubit formCubit; + setUp(() { - harness = TestHarness()..setUpDefaults(); + financingCubit = FinancingCubit(CalculateFinancing()); + formCubit = FinancingFormCubit(); }); - - tearDown(() => harness.dispose()); - - Widget createSut() { - return MaterialApp( - theme: AppTheme.lightTheme, - darkTheme: AppTheme.darkTheme, - themeMode: ThemeMode.dark, - home: MultiBlocProvider( - providers: [ - BlocProvider.value(value: harness.financingCubit), - BlocProvider.value( - value: harness.financingFormCubit, - ), - ], - child: const FinancingPage(), + + tearDown(() async { + await financingCubit.close(); + await formCubit.close(); + }); + + Future pump( + WidgetTester tester, { + ThemeMode themeMode = ThemeMode.light, + Size size = const Size(400, 900), + double textScale = 1, + }) async { + tester.view.physicalSize = size; + tester.view.devicePixelRatio = 1; + tester.platformDispatcher.textScaleFactorTestValue = textScale; + addTearDown(tester.view.reset); + addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue); + + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + themeMode: themeMode, + home: MultiBlocProvider( + providers: [ + BlocProvider.value(value: financingCubit), + BlocProvider.value(value: formCubit), + ], + child: const FinancingPage(), + ), ), ); } - group('FinancingPage - Testes de Integração da Tela', () { - testWidgets('Deve renderizar a árvore de componentes completa com sucesso', (tester) async { - await tester.pumpWidget(createSut()); + Finder row(FinancialCalculationTarget field) => find.byKey(ValueKey(field)); - // 1. Verifica se a estrutura de componentes customizados está presente - expect(find.byType(PageHeader), findsOneWidget); - expect(find.byType(TitleWidget), findsOneWidget); - expect(find.byType(HelpCard), findsOneWidget); - expect(find.byType(FormsView), findsOneWidget); + Future type( + WidgetTester tester, + FinancialCalculationTarget field, + String text, + ) async { + await tester.enterText( + find.descendant(of: row(field), matching: find.byType(TextField)), + text, + ); + await tester.pump(); + } + + Future fillExample(WidgetTester tester) async { + await type(tester, FinancialCalculationTarget.initialValue, '250000'); + await type(tester, FinancialCalculationTarget.periods, '12'); + await type(tester, FinancialCalculationTarget.interestRate, '200'); + } + + Finder inBand(String text) => find.descendant( + of: find.byType(CalculatorBand), + matching: find.text(text), + ); + + group('FinancingPage', () { + testWidgets('monta faixa, lista de campos e barra de ações', ( + tester, + ) async { + await pump(tester); + expect(find.byType(CalculatorBand), findsOneWidget); + expect(find.byType(FieldRow), findsNWidgets(4)); + expect(find.byType(BottomActionBar), findsOneWidget); + expect(find.text('Financiamento'), findsOneWidget); + for (final label in [ + 'Valor financiado', + 'Prazo', + 'Taxa de juros', + 'Prestação', + ]) { + expect(find.text(label), findsOneWidget); + } + expect(find.text('meses'), findsOneWidget); + expect(find.text('% a.m.'), findsOneWidget); + expect(inBand('e descubra o quarto'), findsOneWidget); + expect(find.byType(ResultDetails), findsNothing); + expect(find.byType(FormNotice), findsNothing); }); - testWidgets('Deve garantir que a página inteira possui um scroll ativado', (tester) async { - await tester.pumpWidget(createSut()); + testWidgets('com três campos, a prestação fica "a descobrir"', ( + tester, + ) async { + await pump(tester); + await fillExample(tester); - expect(find.byType(SingleChildScrollView), findsOneWidget); + expect(inBand('a prestação'), findsOneWidget); + expect( + find.descendant( + of: row(FinancialCalculationTarget.finalValue), + matching: find.text(FieldRow.calculateText), + ), + findsOneWidget, + ); }); - }); - group("FinancingPage - Testes da PageHeader", () { - testWidgets('Deve conseguir criar uma PageHeader', (tester) async { - await tester.pumpWidget(createSut()); - expect(find.byType(PageHeader), findsOneWidget); + + testWidgets('2.500 / 12 / 2%: prestação, total, juros, taxa anual e ' + 'barra 88/12', (tester) async { + await pump(tester); + await fillExample(tester); + + await tester.tap(find.text('Calcular')); + await tester.pumpAndSettle(); + + expect(inBand('Sua prestação vai ser'), findsOneWidget); + expect(inBand('236,40'), findsOneWidget); + expect(inBand('12 parcelas iguais, pela Tabela Price'), findsOneWidget); + expect(formCubit.finalValue.text, '236,40'); + + expect(find.text('Do total que você vai pagar'), findsOneWidget); + expect(find.textContaining('2.836,80'), findsOneWidget); + expect(find.textContaining('336,80'), findsOneWidget); + expect(find.text('26,82% a.a.'), findsOneWidget); + expect(find.text('Emprestado 88%'), findsOneWidget); + expect(find.text('Juros 12%'), findsOneWidget); + }); + + testWidgets('tocar na linha "a descobrir" também calcula', (tester) async { + await pump(tester); + await fillExample(tester); + + await tester.tap(find.text(FieldRow.calculateText)); + await tester.pumpAndSettle(); + + expect(inBand('236,40'), findsOneWidget); + }); + + testWidgets('calcula a taxa a partir da prestação', (tester) async { + await pump(tester); + await type(tester, FinancialCalculationTarget.initialValue, '250000'); + await type(tester, FinancialCalculationTarget.periods, '12'); + await type(tester, FinancialCalculationTarget.finalValue, '23640'); + + await tester.tap(find.text('Calcular')); + await tester.pumpAndSettle(); + + expect(inBand('A taxa de juros é de'), findsOneWidget); + expect(inBand('2,00% ao mês'), findsOneWidget); + }); + + testWidgets('com menos de três campos, mostra o aviso com os campos ' + 'vazios e desabilita "Calcular"', (tester) async { + await pump(tester); + await type(tester, FinancialCalculationTarget.initialValue, '250000'); + await type(tester, FinancialCalculationTarget.periods, '12'); + + expect(find.text('Faltam dados para calcular'), findsOneWidget); + expect( + find.text( + 'Preencha a taxa de juros ou a prestação. ' + 'O campo que ficar em branco é calculado.', + ), + findsOneWidget, + ); + expect(inBand('falta 1 campo'), findsOneWidget); + expect( + tester.widget(find.byType(FilledButton)).onPressed, + isNull, + ); + }); + + testWidgets('limpar mostra "Campos limpos" e desfazer recalcula', ( + tester, + ) async { + await pump(tester); + await fillExample(tester); + await tester.tap(find.text('Calcular')); + await tester.pumpAndSettle(); + + await tester.tap(find.bySemanticsLabel(CalculatorBand.clearLabel)); + await tester.pumpAndSettle(); + + expect(find.text('Campos limpos'), findsOneWidget); + expect(inBand('e descubra o quarto'), findsOneWidget); + expect(formCubit.initialValue.text, isEmpty); + + await tester.tap(find.text('Desfazer')); + await tester.pumpAndSettle(); + + expect(formCubit.initialValue.text, '2.500,00'); + expect(inBand('236,40'), findsOneWidget); + expect(find.byType(ResultDetails), findsOneWidget); + }); + + testWidgets('"?" abre a ajuda', (tester) async { + await pump(tester); + + await tester.tap(find.bySemanticsLabel(CalculatorBand.helpLabel)); + await tester.pumpAndSettle(); + + expect(find.text('Como funciona'), findsOneWidget); }); + for (final themeMode in [ThemeMode.light, ThemeMode.dark]) { + testWidgets('em 360dp com fonte em 130% no tema ${themeMode.name}', ( + tester, + ) async { + await pump( + tester, + themeMode: themeMode, + size: const Size(360, 780), + textScale: 1.3, + ); + await fillExample(tester); + await tester.tap(find.text('Calcular')); + await tester.pumpAndSettle(); - testWidgets("Deve conter uma fontSize de 18 e fontWeight bold", (tester) async { - await tester.pumpWidget(createSut()); + expect(tester.takeException(), isNull); + expect(inBand('236,40'), findsOneWidget); + }); + } - final textFinder = find.text('Financiamento'); - final RenderParagraph renderObject = tester.renderObject(textFinder.first); + testWidgets('o teclado não cobre o campo em foco', (tester) async { + await pump(tester, size: const Size(360, 700)); - final TextStyle? resolvedStyle = renderObject.text.style; + await tester.showKeyboard( + find.descendant( + of: row(FinancialCalculationTarget.finalValue), + matching: find.byType(TextField), + ), + ); + tester.view.viewInsets = const FakeViewPadding(bottom: 320); + await tester.pumpAndSettle(); - expect(resolvedStyle?.fontSize, 18); - expect(resolvedStyle?.fontWeight, FontWeight.w700); + final field = tester.getRect(row(FinancialCalculationTarget.finalValue)); + final bar = tester.getRect(find.byType(BottomActionBar)); + expect(bar.bottom, 700 - 320); + expect(field.bottom, lessThanOrEqualTo(bar.top)); }); }); } diff --git a/test/modules/financial_calculators/presentation/utils/financing_statement_test.dart b/test/modules/financial_calculators/presentation/utils/financing_statement_test.dart new file mode 100644 index 0000000..e686c40 --- /dev/null +++ b/test/modules/financial_calculators/presentation/utils/financing_statement_test.dart @@ -0,0 +1,136 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:real_calc/core/widgets/calculator_band/band_statement.dart'; +import 'package:real_calc/modules/financial_calculators/domain/entities/financial_calculation.dart'; +import 'package:real_calc/modules/financial_calculators/domain/enum/financial_calculation_target.dart'; +import 'package:real_calc/modules/financial_calculators/presentation/utils/financing_statement.dart'; + +void main() { + const initialValue = FinancialCalculationTarget.initialValue; + const periods = FinancialCalculationTarget.periods; + const rate = FinancialCalculationTarget.interestRate; + const installment = FinancialCalculationTarget.finalValue; + + final solved = FinancialCalculation( + initialValue: 2500, + periods: 12, + rate: 2, + finalValue: 236.3990, + ); + final nbsp = String.fromCharCode(0xA0); + + BandStatement statement( + Set empty, { + FinancialCalculationTarget? calculated, + }) => financingStatement( + emptyFields: empty, + values: calculated == null ? FinancialCalculation() : solved, + calculatedField: calculated, + ); + + group('financingStatement', () { + test('inicial', () { + expect( + statement({initialValue, periods, rate, installment}), + const BandStatement( + kicker: 'Informe três valores', + value: 'e descubra o quarto', + sub: 'Qualquer campo pode ser o resultado.', + ), + ); + }); + + test('falta a prestação', () { + expect( + statement({installment}), + const BandStatement( + kicker: 'Você quer descobrir', + value: 'a prestação', + sub: 'Os outros três campos já estão preenchidos.', + ), + ); + }); + + test('"Você quer descobrir" vale para os quatro campos', () { + expect(statement({initialValue}).value, 'o valor financiado'); + expect(statement({periods}).value, 'o prazo'); + expect(statement({rate}).value, 'a taxa de juros'); + }); + + test('prestação calculada', () { + expect( + statement({}, calculated: installment), + const BandStatement( + kicker: 'Sua prestação vai ser', + value: '236,40', + isMoney: true, + sub: '12 parcelas iguais, pela Tabela Price', + ), + ); + }); + + test('valor calculado', () { + expect( + statement({}, calculated: initialValue), + BandStatement( + kicker: 'Você pode financiar', + value: '2.500,00', + isMoney: true, + sub: 'em 12 parcelas de R\$${nbsp}236,40', + ), + ); + }); + + test('prazo calculado', () { + expect( + statement({}, calculated: periods), + BandStatement( + kicker: 'Você quita em', + value: '12 meses', + sub: 'com parcelas de R\$${nbsp}236,40', + ), + ); + }); + + test('taxa calculada', () { + expect( + statement({}, calculated: rate), + const BandStatement( + kicker: 'A taxa de juros é de', + value: '2,00% ao mês', + sub: '26,82% ao ano', + ), + ); + }); + + test('faltam campos: "falta 1" e "faltam 2"', () { + expect( + statement({rate, installment}), + const BandStatement( + kicker: 'Para calcular, ainda', + value: 'falta 1 campo', + sub: 'Deixe em branco só o que você quer descobrir.', + ), + ); + expect(statement({periods, rate, installment}).value, 'faltam 2 campos'); + }); + + test('com os quatro preenchidos e nada calculado', () { + expect(statement({}).value, '1 campo em branco'); + }); + }); + + group('financingMissingFieldsMessage', () { + test('cita os campos vazios na ordem da tela', () { + expect( + financingMissingFieldsMessage({installment, rate}), + 'Preencha a taxa de juros ou a prestação. ' + 'O campo que ficar em branco é calculado.', + ); + expect( + financingMissingFieldsMessage({installment, periods, rate}), + 'Preencha o prazo, a taxa de juros ou a prestação. ' + 'O campo que ficar em branco é calculado.', + ); + }); + }); +} From 3706e220a6f1d4787d2ae2cb451e74b00b8fd723 Mon Sep 17 00:00:00 2001 From: Rodrigo Mamede <61851910+Rodrigosdk@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:12:20 -0300 Subject: [PATCH 11/22] =?UTF-8?q?feat:=20migrar=20calculadora=20de=20Dep?= =?UTF-8?q?=C3=B3sitos=20Regulares=20(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: extrair CalculatorFormView e as frases comuns das calculadoras (#124) * feat: adicionar DepositsStatement para montar a frase da faixa dos Depósitos Regulares (#124) * feat: migrar a RegularDepositsPage para a estrutura do Financiamento (#124) --- lib/modules/financial_calculators/module.dart | 5 +- .../presentation/pages/financing_page.dart | 177 ++------------ .../pages/regular_deposits_page.dart | 144 ++++------- .../utils/calculator_statement.dart | 80 ++++++ .../utils/deposits_statement.dart | 57 +++++ .../utils/financing_statement.dart | 79 +----- .../widgets/calculator_form_view.dart | 183 ++++++++++++++ .../pages/regular_deposits_page_test.dart | 228 +++++++++++++++--- .../utils/deposits_statement_test.dart | 98 ++++++++ 9 files changed, 692 insertions(+), 359 deletions(-) create mode 100644 lib/modules/financial_calculators/presentation/utils/calculator_statement.dart create mode 100644 lib/modules/financial_calculators/presentation/utils/deposits_statement.dart create mode 100644 lib/modules/financial_calculators/presentation/widgets/calculator_form_view.dart create mode 100644 test/modules/financial_calculators/presentation/utils/deposits_statement_test.dart diff --git a/lib/modules/financial_calculators/module.dart b/lib/modules/financial_calculators/module.dart index d75033a..e7ea5c0 100644 --- a/lib/modules/financial_calculators/module.dart +++ b/lib/modules/financial_calculators/module.dart @@ -51,10 +51,7 @@ class FinancialCalculatorsModule extends AppModule { BlocProvider(create: (_) => Modular.get()), BlocProvider(create: (_) => Modular.get()), ], - child: const CalculatorScope( - kind: CalculatorKind.deposits, - child: RegularDepositsPage(), - ), + child: const RegularDepositsPage(), ), ); r.child( diff --git a/lib/modules/financial_calculators/presentation/pages/financing_page.dart b/lib/modules/financial_calculators/presentation/pages/financing_page.dart index e209648..19349ef 100644 --- a/lib/modules/financial_calculators/presentation/pages/financing_page.dart +++ b/lib/modules/financial_calculators/presentation/pages/financing_page.dart @@ -3,49 +3,20 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:real_calc/core/themes/calculator_palette.dart'; import 'package:real_calc/core/themes/calculator_scope.dart'; -import 'package:real_calc/core/themes/spacing.dart'; import 'package:real_calc/core/utils/decimal_input_formatter.dart'; -import 'package:real_calc/core/widgets/bottom_action_bar.dart'; -import 'package:real_calc/core/widgets/calculator_band/calculator_band.dart'; -import 'package:real_calc/core/widgets/field_list/field_list.dart'; -import 'package:real_calc/core/widgets/field_list/field_row.dart'; -import 'package:real_calc/core/widgets/form_notice.dart'; -import 'package:real_calc/core/widgets/help_sheet/help_sheet.dart'; -import 'package:real_calc/core/widgets/result_details/result_details.dart'; import '../../domain/enum/financial_calculation_target.dart'; import '../cubit/financing/financing_cubit.dart'; import '../cubit/forms/financing_form_cubit.dart'; import '../utils/financing_statement.dart'; import '../utils/result_summaries.dart'; - -class _FieldConfig { - final FinancialCalculationTarget target; - final String label; - final String? prefix; - final String? unit; - final String? secondLine; - final String hint; - final TextInputType keyboardType; - final List formatters; - - const _FieldConfig({ - required this.target, - required this.label, - required this.hint, - required this.keyboardType, - required this.formatters, - this.prefix, - this.unit, - this.secondLine, - }); -} +import '../widgets/calculator_form_view.dart'; class FinancingPage extends StatelessWidget { static const title = 'Financiamento'; - static final _fields = [ - _FieldConfig( + static final fields = [ + CalculatorFieldConfig( target: FinancialCalculationTarget.initialValue, label: 'Valor financiado', prefix: 'R\$', @@ -53,7 +24,7 @@ class FinancingPage extends StatelessWidget { keyboardType: const TextInputType.numberWithOptions(decimal: true), formatters: [DecimalInputFormatter()], ), - _FieldConfig( + CalculatorFieldConfig( target: FinancialCalculationTarget.periods, label: 'Prazo', unit: 'meses', @@ -61,7 +32,7 @@ class FinancingPage extends StatelessWidget { keyboardType: TextInputType.number, formatters: [FilteringTextInputFormatter.digitsOnly], ), - _FieldConfig( + CalculatorFieldConfig( target: FinancialCalculationTarget.interestRate, label: 'Taxa de juros', unit: '% a.m.', @@ -69,7 +40,7 @@ class FinancingPage extends StatelessWidget { keyboardType: const TextInputType.numberWithOptions(decimal: true), formatters: [DecimalInputFormatter()], ), - _FieldConfig( + CalculatorFieldConfig( target: FinancialCalculationTarget.finalValue, label: 'Prestação', prefix: 'R\$', @@ -86,7 +57,7 @@ class FinancingPage extends StatelessWidget { Widget build(BuildContext context) { return CalculatorScope( kind: CalculatorKind.financing, - child: BlocListener( + child: BlocConsumer( listener: (context, state) { final formCubit = context.read(); if (state is FinancingCalculated) { @@ -95,128 +66,18 @@ class FinancingPage extends StatelessWidget { formCubit.invalidateCalculatedField(); } }, - child: const _FinancingView(), - ), - ); - } -} - -class _FinancingView extends StatelessWidget { - const _FinancingView(); - - @override - Widget build(BuildContext context) { - final formCubit = context.watch(); - final formState = formCubit.state; - final financingState = context.watch().state; - - final emptyFields = { - for (final field in FinancialCalculationTarget.values) - if (formCubit.controllerFor(field).text.trim().isEmpty) field, - }; - final calculatedField = formState.calculatedField; - final values = formCubit.financing; - final filled = - FinancialCalculationTarget.values.length - emptyFields.length; - final error = financingState is FinancingError ? financingState : null; - - void calculate() { - if (formCubit.state.canCalculate) { - context.read().calculate(formCubit.financing); - } - } - - void clear() { - final snapshot = { - for (final field in FinancialCalculationTarget.values) - field: formCubit.controllerFor(field).text, - }; - final previouslyCalculated = formCubit.state.calculatedField; - FocusScope.of(context).unfocus(); - formCubit.clear(); - showFieldsClearedSnackBar( - context, - onUndo: () { - for (final MapEntry(key: field, value: text) in snapshot.entries) { - if (field != previouslyCalculated) { - formCubit.controllerFor(field).text = text; - } - } - if (previouslyCalculated != null) calculate(); - }, - ); - } - - return Scaffold( - body: Column( - children: [ - CalculatorBand( - title: FinancingPage.title, - statement: financingStatement( - emptyFields: emptyFields, - values: values, - calculatedField: calculatedField, - ), - onHelp: () => showHelpSheet(context, CalculatorKind.financing), - onClear: clear, - ), - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.fromLTRB( - AppSpacing.screen, - AppSpacing.sm, - AppSpacing.screen, - AppSpacing.lg, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FieldList( - children: [ - for (final field in FinancingPage._fields) - FieldRow( - key: ValueKey(field.target), - label: field.label, - controller: formCubit.controllerFor(field.target), - state: formState.stateOf(field.target), - prefix: field.prefix, - unit: field.unit, - secondLine: field.secondLine, - hint: field.hint, - keyboardType: field.keyboardType, - inputFormatters: field.formatters, - onCalculate: calculate, - ), - ], - ), - if (error != null) - Padding( - padding: const EdgeInsets.only(top: AppSpacing.md), - child: FormNotice( - title: 'Não foi possível calcular', - message: error.message ?? 'Confira os valores.', - isError: true, - ), - ) - else if (filled > 0 && filled < 3) - Padding( - padding: const EdgeInsets.only(top: AppSpacing.md), - child: FormNotice( - title: 'Faltam dados para calcular', - message: financingMissingFieldsMessage(emptyFields), - ), - ), - if (calculatedField != null) - ResultDetails(summary: financingResultSummary(values)), - ], - ), - ), - ), - BottomActionBar( - primaryLabel: 'Calcular', - onPrimary: formState.canCalculate ? calculate : null, - ), - ], + builder: (context, state) => CalculatorFormView( + kind: CalculatorKind.financing, + title: title, + fields: fields, + statement: financingStatement, + missingMessage: financingMissingFieldsMessage, + summary: financingResultSummary, + onCalculate: context.read().calculate, + errorMessage: state is FinancingError + ? state.message ?? 'Confira os valores.' + : null, + ), ), ); } diff --git a/lib/modules/financial_calculators/presentation/pages/regular_deposits_page.dart b/lib/modules/financial_calculators/presentation/pages/regular_deposits_page.dart index 161f7a4..968eefc 100644 --- a/lib/modules/financial_calculators/presentation/pages/regular_deposits_page.dart +++ b/lib/modules/financial_calculators/presentation/pages/regular_deposits_page.dart @@ -1,122 +1,82 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:real_calc/core/themes/spacing.dart'; -import 'package:real_calc/core/widgets/page_header.dart'; -import 'package:real_calc/core/widgets/title_widget.dart'; -import 'package:real_calc/core/widgets/help_card.dart'; -import '../../../../core/utils/decimal_input_formatter.dart'; +import 'package:real_calc/core/themes/calculator_palette.dart'; +import 'package:real_calc/core/themes/calculator_scope.dart'; +import 'package:real_calc/core/utils/decimal_input_formatter.dart'; + import '../../domain/enum/financial_calculation_target.dart'; -import '../../../../core/widgets/types/input_field_state.dart'; import '../cubit/forms/financing_form_cubit.dart'; import '../cubit/regular_deposits/regular_deposits_cubit.dart'; -import '../models/field_spec.dart'; -import '../widgets/forms_view.dart'; +import '../utils/deposits_statement.dart'; +import '../utils/result_summaries.dart'; +import '../widgets/calculator_form_view.dart'; class RegularDepositsPage extends StatelessWidget { - static final List> _fieldSpecs = [ - FieldSpec( - field: FinancialCalculationTarget.initialValue, - label: 'Valor do depósito (R\$)', + static const title = 'Depósitos Regulares'; + + static final fields = [ + CalculatorFieldConfig( + target: FinancialCalculationTarget.initialValue, + label: 'Depósito', + prefix: 'R\$', + secondLine: 'todo mês', hint: '0,00', - icon: Icons.savings_outlined, + keyboardType: const TextInputType.numberWithOptions(decimal: true), formatters: [DecimalInputFormatter()], ), - FieldSpec( - field: FinancialCalculationTarget.periods, - label: 'Prazo (meses)', + CalculatorFieldConfig( + target: FinancialCalculationTarget.periods, + label: 'Prazo', + unit: 'meses', hint: '0', - icon: Icons.calendar_today_outlined, + keyboardType: TextInputType.number, formatters: [FilteringTextInputFormatter.digitsOnly], ), - FieldSpec( - field: FinancialCalculationTarget.interestRate, - label: 'Taxa de juros (% ao mês)', + CalculatorFieldConfig( + target: FinancialCalculationTarget.interestRate, + label: 'Taxa de juros', + unit: '% a.m.', hint: '0,00', - icon: Icons.percent, + keyboardType: const TextInputType.numberWithOptions(decimal: true), formatters: [DecimalInputFormatter()], ), - FieldSpec( - field: FinancialCalculationTarget.finalValue, + CalculatorFieldConfig( + target: FinancialCalculationTarget.finalValue, label: 'Valor final', + prefix: 'R\$', hint: '0,00', - icon: Icons.wallet_outlined, + keyboardType: const TextInputType.numberWithOptions(decimal: true), formatters: [DecimalInputFormatter()], ), ]; const RegularDepositsPage({super.key}); - BlocListener _buildForms() { - return BlocListener( - listener: (context, state) { - final formCubit = context.read(); - if (state is RegularDepositsCalculated) { - formCubit.applyResult(state.value, state.calculatedField); - } else if (state is RegularDepositsError) { - formCubit.invalidateCalculatedField(); - } - }, - child: BlocBuilder( - builder: (context, financingState) { - final formCubit = context.watch(); - final formState = formCubit.state; - - void calculate() { - if (formState.canCalculate) { - context.read().calculate( - formCubit.financing, - ); - } - } - - return FormsView( - fieldSpecs: _fieldSpecs, - controllerFor: formCubit.controllerFor, - stateOf: formState.stateOf, - isLoading: financingState is RegularDepositsLoading, - errorMessage: financingState is RegularDepositsError - ? financingState.message ?? 'Erro ao calcular.' - : null, - onCalculate: calculate, - onClear: formCubit.clear, - onFieldTap: (field) { - if (formState.stateOf(field) == InputFieldState.unknown) { - calculate(); - } - }, - ); - }, - ), - ); - } - @override Widget build(BuildContext context) { - return Scaffold( - appBar: PageHeader(title: 'Depósitos Regulares'), - body: SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md + 4, - vertical: AppSpacing.md + 4, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: AppSpacing.lg, - children: [ - const TitleWidget( - title: 'Depósitos Regulares', - subtitle: - 'Calcule o valor futuro, a taxa, o prazo ou o depósito mensal.', - ), - const HelpCard( - message: - 'Preencha 3 campos e toque no 4° para calcular automaticamente', - ), - _buildForms(), - ], - ), + return CalculatorScope( + kind: CalculatorKind.deposits, + child: BlocConsumer( + listener: (context, state) { + final formCubit = context.read(); + if (state is RegularDepositsCalculated) { + formCubit.applyResult(state.value, state.calculatedField); + } else if (state is RegularDepositsError) { + formCubit.invalidateCalculatedField(); + } + }, + builder: (context, state) => CalculatorFormView( + kind: CalculatorKind.deposits, + title: title, + fields: fields, + statement: depositsStatement, + missingMessage: depositsMissingFieldsMessage, + summary: regularDepositsResultSummary, + onCalculate: context.read().calculate, + errorMessage: state is RegularDepositsError + ? state.message ?? 'Confira os valores.' + : null, ), ), ); diff --git a/lib/modules/financial_calculators/presentation/utils/calculator_statement.dart b/lib/modules/financial_calculators/presentation/utils/calculator_statement.dart new file mode 100644 index 0000000..e5ac642 --- /dev/null +++ b/lib/modules/financial_calculators/presentation/utils/calculator_statement.dart @@ -0,0 +1,80 @@ +import 'package:intl/intl.dart'; +import 'package:real_calc/core/utils/br_number_format.dart'; +import 'package:real_calc/core/widgets/calculator_band/band_statement.dart'; + +import '../../domain/enum/financial_calculation_target.dart'; + +const calculatorFieldOrder = [ + FinancialCalculationTarget.initialValue, + FinancialCalculationTarget.periods, + FinancialCalculationTarget.interestRate, + FinancialCalculationTarget.finalValue, +]; + +typedef FieldName = String Function(FinancialCalculationTarget field); + +final _money = NumberFormat('#,##0.00', 'pt_BR'); + +String moneyValue(double value) => _money.format(toCents(value) / 100); + +String money(double value) => formatCents(toCents(value)); + +String months(int count) => count == 1 ? '1 mês' : '$count meses'; + +BandStatement pendingStatement( + Set emptyFields, + FieldName fieldName, +) { + if (emptyFields.length == calculatorFieldOrder.length) { + return const BandStatement( + kicker: 'Informe três valores', + value: 'e descubra o quarto', + sub: 'Qualquer campo pode ser o resultado.', + ); + } + + if (emptyFields.length == 1) { + return BandStatement( + kicker: 'Você quer descobrir', + value: fieldName(emptyFields.single), + sub: 'Os outros três campos já estão preenchidos.', + ); + } + + if (emptyFields.isEmpty) { + return const BandStatement( + kicker: 'Para calcular, deixe', + value: '1 campo em branco', + sub: 'Apague o valor que você quer descobrir.', + ); + } + + final missing = emptyFields.length - 1; + return BandStatement( + kicker: 'Para calcular, ainda', + value: missing == 1 ? 'falta 1 campo' : 'faltam $missing campos', + sub: 'Deixe em branco só o que você quer descobrir.', + ); +} + +String missingFieldsMessage( + Set emptyFields, + FieldName fieldName, +) { + final names = [ + for (final field in calculatorFieldOrder) + if (emptyFields.contains(field)) fieldName(field), + ]; + final list = names.length == 1 + ? names.single + : '${names.sublist(0, names.length - 1).join(', ')} ou ${names.last}'; + return 'Preencha $list. O campo que ficar em branco é calculado.'; +} + +BandStatement rateStatement(double monthlyRate, {required String kicker}) { + return BandStatement( + kicker: kicker, + value: '${formatPercent(monthlyRate)} ao mês', + sub: '${formatPercent(annualEquivalentRate(monthlyRate))} ao ano', + ); +} diff --git a/lib/modules/financial_calculators/presentation/utils/deposits_statement.dart b/lib/modules/financial_calculators/presentation/utils/deposits_statement.dart new file mode 100644 index 0000000..2c95ea8 --- /dev/null +++ b/lib/modules/financial_calculators/presentation/utils/deposits_statement.dart @@ -0,0 +1,57 @@ +import 'package:real_calc/core/utils/br_number_format.dart'; +import 'package:real_calc/core/widgets/calculator_band/band_statement.dart'; + +import '../../domain/entities/financial_calculation.dart'; +import '../../domain/enum/financial_calculation_target.dart'; +import 'calculator_statement.dart'; + +String depositsFieldName(FinancialCalculationTarget field) => switch (field) { + FinancialCalculationTarget.initialValue => 'o depósito', + FinancialCalculationTarget.periods => 'o prazo', + FinancialCalculationTarget.interestRate => 'a taxa de juros', + FinancialCalculationTarget.finalValue => 'o valor final', +}; + +BandStatement depositsStatement({ + required Set emptyFields, + required FinancialCalculation values, + FinancialCalculationTarget? calculatedField, +}) { + if (calculatedField == null) { + return pendingStatement(emptyFields, depositsFieldName); + } + + final deposit = money(values.initialValue); + final total = money(values.finalValue); + final yieldCents = + toCents(values.finalValue) - + toCents(values.initialValue) * values.periods; + final yieldText = '${formatCents(yieldCents)} de rendimento'; + + return switch (calculatedField) { + FinancialCalculationTarget.finalValue => BandStatement( + kicker: 'Guardando $deposit por mês, você terá', + value: moneyValue(values.finalValue), + isMoney: true, + sub: 'em ${months(values.periods)}, com $yieldText', + ), + FinancialCalculationTarget.initialValue => BandStatement( + kicker: 'Para juntar $total, guarde por mês', + value: moneyValue(values.initialValue), + isMoney: true, + sub: 'durante ${months(values.periods)}', + ), + FinancialCalculationTarget.periods => BandStatement( + kicker: 'Guardando $deposit por mês, você junta $total em', + value: months(values.periods), + sub: 'com $yieldText', + ), + FinancialCalculationTarget.interestRate => rateStatement( + values.rate, + kicker: 'Para isso, o dinheiro precisa render', + ), + }; +} + +String depositsMissingFieldsMessage(Set empty) => + missingFieldsMessage(empty, depositsFieldName); diff --git a/lib/modules/financial_calculators/presentation/utils/financing_statement.dart b/lib/modules/financial_calculators/presentation/utils/financing_statement.dart index 8d74195..7871fe7 100644 --- a/lib/modules/financial_calculators/presentation/utils/financing_statement.dart +++ b/lib/modules/financial_calculators/presentation/utils/financing_statement.dart @@ -1,16 +1,8 @@ -import 'package:intl/intl.dart'; -import 'package:real_calc/core/utils/br_number_format.dart'; import 'package:real_calc/core/widgets/calculator_band/band_statement.dart'; import '../../domain/entities/financial_calculation.dart'; import '../../domain/enum/financial_calculation_target.dart'; - -const _fieldOrder = [ - FinancialCalculationTarget.initialValue, - FinancialCalculationTarget.periods, - FinancialCalculationTarget.interestRate, - FinancialCalculationTarget.finalValue, -]; +import 'calculator_statement.dart'; String financingFieldName(FinancialCalculationTarget field) => switch (field) { FinancialCalculationTarget.initialValue => 'o valor financiado', @@ -24,64 +16,17 @@ BandStatement financingStatement({ required FinancialCalculation values, FinancialCalculationTarget? calculatedField, }) { - if (calculatedField != null) return _answer(calculatedField, values); - - if (emptyFields.length == _fieldOrder.length) { - return const BandStatement( - kicker: 'Informe três valores', - value: 'e descubra o quarto', - sub: 'Qualquer campo pode ser o resultado.', - ); - } - - if (emptyFields.length == 1) { - return BandStatement( - kicker: 'Você quer descobrir', - value: financingFieldName(emptyFields.single), - sub: 'Os outros três campos já estão preenchidos.', - ); - } - - if (emptyFields.isEmpty) { - return const BandStatement( - kicker: 'Para calcular, deixe', - value: '1 campo em branco', - sub: 'Apague o valor que você quer descobrir.', - ); + if (calculatedField == null) { + return pendingStatement(emptyFields, financingFieldName); } - final missing = emptyFields.length - 1; - return BandStatement( - kicker: 'Para calcular, ainda', - value: missing == 1 ? 'falta 1 campo' : 'faltam $missing campos', - sub: 'Deixe em branco só o que você quer descobrir.', - ); -} - -String financingMissingFieldsMessage(Set empty) { - final names = [ - for (final field in _fieldOrder) - if (empty.contains(field)) financingFieldName(field), - ]; - final list = names.length == 1 - ? names.single - : '${names.sublist(0, names.length - 1).join(', ')} ou ${names.last}'; - return 'Preencha $list. O campo que ficar em branco é calculado.'; -} - -final _money = NumberFormat('#,##0.00', 'pt_BR'); - -BandStatement _answer( - FinancialCalculationTarget field, - FinancialCalculation values, -) { - final installment = formatCents(toCents(values.finalValue)); + final installment = money(values.finalValue); final periods = values.periods; - return switch (field) { + return switch (calculatedField) { FinancialCalculationTarget.finalValue => BandStatement( kicker: 'Sua prestação vai ser', - value: _money.format(toCents(values.finalValue) / 100), + value: moneyValue(values.finalValue), isMoney: true, sub: periods == 1 ? '1 parcela, pela Tabela Price' @@ -89,7 +34,7 @@ BandStatement _answer( ), FinancialCalculationTarget.initialValue => BandStatement( kicker: 'Você pode financiar', - value: _money.format(toCents(values.initialValue) / 100), + value: moneyValue(values.initialValue), isMoney: true, sub: periods == 1 ? 'em 1 parcela de $installment' @@ -97,13 +42,15 @@ BandStatement _answer( ), FinancialCalculationTarget.periods => BandStatement( kicker: 'Você quita em', - value: periods == 1 ? '1 mês' : '$periods meses', + value: months(periods), sub: 'com parcelas de $installment', ), - FinancialCalculationTarget.interestRate => BandStatement( + FinancialCalculationTarget.interestRate => rateStatement( + values.rate, kicker: 'A taxa de juros é de', - value: '${formatPercent(values.rate)} ao mês', - sub: '${formatPercent(annualEquivalentRate(values.rate))} ao ano', ), }; } + +String financingMissingFieldsMessage(Set empty) => + missingFieldsMessage(empty, financingFieldName); diff --git a/lib/modules/financial_calculators/presentation/widgets/calculator_form_view.dart b/lib/modules/financial_calculators/presentation/widgets/calculator_form_view.dart new file mode 100644 index 0000000..109513a --- /dev/null +++ b/lib/modules/financial_calculators/presentation/widgets/calculator_form_view.dart @@ -0,0 +1,183 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:real_calc/core/themes/calculator_palette.dart'; +import 'package:real_calc/core/themes/spacing.dart'; +import 'package:real_calc/core/widgets/bottom_action_bar.dart'; +import 'package:real_calc/core/widgets/calculator_band/band_statement.dart'; +import 'package:real_calc/core/widgets/calculator_band/calculator_band.dart'; +import 'package:real_calc/core/widgets/field_list/field_list.dart'; +import 'package:real_calc/core/widgets/field_list/field_row.dart'; +import 'package:real_calc/core/widgets/form_notice.dart'; +import 'package:real_calc/core/widgets/help_sheet/help_sheet.dart'; +import 'package:real_calc/core/widgets/result_details/result_details.dart'; +import 'package:real_calc/core/widgets/result_details/result_summary.dart'; + +import '../../domain/entities/financial_calculation.dart'; +import '../../domain/enum/financial_calculation_target.dart'; +import '../cubit/forms/financing_form_cubit.dart'; + +class CalculatorFieldConfig { + final FinancialCalculationTarget target; + final String label; + final String hint; + final TextInputType keyboardType; + final List formatters; + final String? prefix; + final String? unit; + final String? secondLine; + + const CalculatorFieldConfig({ + required this.target, + required this.label, + required this.hint, + required this.keyboardType, + required this.formatters, + this.prefix, + this.unit, + this.secondLine, + }); +} + +typedef StatementBuilder = + BandStatement Function({ + required Set emptyFields, + required FinancialCalculation values, + FinancialCalculationTarget? calculatedField, + }); + +class CalculatorFormView extends StatelessWidget { + final CalculatorKind kind; + final String title; + final List fields; + final StatementBuilder statement; + final String Function(Set) missingMessage; + final ResultSummary Function(FinancialCalculation) summary; + final ValueChanged onCalculate; + final String? errorMessage; + + const CalculatorFormView({ + super.key, + required this.kind, + required this.title, + required this.fields, + required this.statement, + required this.missingMessage, + required this.summary, + required this.onCalculate, + this.errorMessage, + }); + + @override + Widget build(BuildContext context) { + final formCubit = context.watch(); + final formState = formCubit.state; + + final emptyFields = { + for (final field in FinancialCalculationTarget.values) + if (formCubit.controllerFor(field).text.trim().isEmpty) field, + }; + final calculatedField = formState.calculatedField; + final values = formCubit.financing; + final filled = + FinancialCalculationTarget.values.length - emptyFields.length; + + void calculate() { + if (formCubit.state.canCalculate) onCalculate(formCubit.financing); + } + + void clear() { + final snapshot = { + for (final field in FinancialCalculationTarget.values) + field: formCubit.controllerFor(field).text, + }; + final previouslyCalculated = formCubit.state.calculatedField; + FocusScope.of(context).unfocus(); + formCubit.clear(); + showFieldsClearedSnackBar( + context, + onUndo: () { + for (final MapEntry(key: field, value: text) in snapshot.entries) { + if (field != previouslyCalculated) { + formCubit.controllerFor(field).text = text; + } + } + if (previouslyCalculated != null) calculate(); + }, + ); + } + + return Scaffold( + body: Column( + children: [ + CalculatorBand( + title: title, + statement: statement( + emptyFields: emptyFields, + values: values, + calculatedField: calculatedField, + ), + onHelp: () => showHelpSheet(context, kind), + onClear: clear, + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB( + AppSpacing.screen, + AppSpacing.sm, + AppSpacing.screen, + AppSpacing.lg, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FieldList( + children: [ + for (final field in fields) + FieldRow( + key: ValueKey(field.target), + label: field.label, + controller: formCubit.controllerFor(field.target), + state: formState.stateOf(field.target), + prefix: field.prefix, + unit: field.unit, + secondLine: field.secondLine, + hint: field.hint, + keyboardType: field.keyboardType, + inputFormatters: field.formatters, + onCalculate: calculate, + ), + ], + ), + if (errorMessage != null) + Padding( + padding: const EdgeInsets.only(top: AppSpacing.md), + child: FormNotice( + title: 'Não foi possível calcular', + message: errorMessage!, + isError: true, + ), + ) + else if (filled > 0 && filled < 3) + Padding( + padding: const EdgeInsets.only(top: AppSpacing.md), + child: FormNotice( + title: 'Faltam dados para calcular', + message: missingMessage(emptyFields), + ), + ), + if (calculatedField != null) + ResultDetails(summary: summary(values)), + ], + ), + ), + ), + BottomActionBar( + primaryLabel: 'Calcular', + onPrimary: formState.canCalculate ? calculate : null, + ), + ], + ), + ); + } +} diff --git a/test/modules/financial_calculators/presentation/pages/regular_deposits_page_test.dart b/test/modules/financial_calculators/presentation/pages/regular_deposits_page_test.dart index 2e3740a..e06feff 100644 --- a/test/modules/financial_calculators/presentation/pages/regular_deposits_page_test.dart +++ b/test/modules/financial_calculators/presentation/pages/regular_deposits_page_test.dart @@ -2,65 +2,215 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:real_calc/core/themes/app_theme.dart'; -import 'package:real_calc/core/widgets/page_header.dart'; -import 'package:real_calc/core/widgets/title_widget.dart'; +import 'package:real_calc/core/themes/calculator_palette.dart'; +import 'package:real_calc/core/widgets/bottom_action_bar.dart'; +import 'package:real_calc/core/widgets/calculator_band/calculator_band.dart'; +import 'package:real_calc/core/widgets/field_list/field_row.dart'; +import 'package:real_calc/core/widgets/result_details/result_details.dart'; import 'package:real_calc/modules/financial_calculators/domain/enum/financial_calculation_target.dart'; import 'package:real_calc/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit.dart'; -import 'package:real_calc/modules/financial_calculators/presentation/widgets/forms_view.dart'; -import 'package:real_calc/core/widgets/help_card.dart'; import 'package:real_calc/modules/financial_calculators/presentation/cubit/regular_deposits/regular_deposits_cubit.dart'; import 'package:real_calc/modules/financial_calculators/presentation/pages/regular_deposits_page.dart'; - -import '../../../../test_harness.dart'; +import 'package:real_calc/modules/financial_calculators/use_case/regular_deposits/calculate_deposit.dart'; void main() { - late TestHarness harness; - + late RegularDepositsCubit depositsCubit; + late FinancingFormCubit formCubit; + setUp(() { - harness = TestHarness()..setUpDefaults(); + depositsCubit = RegularDepositsCubit(CalculateDeposit()); + formCubit = FinancingFormCubit(); }); - - tearDown(() => harness.dispose()); - - Widget createSut() { - return MaterialApp( - theme: AppTheme.lightTheme, - darkTheme: AppTheme.darkTheme, - themeMode: ThemeMode.dark, - home: MultiBlocProvider( - providers: [ - BlocProvider.value( - value: harness.regularDepositsCubit, - ), - BlocProvider.value( - value: harness.financingFormCubit, - ), - ], - child: const RegularDepositsPage(), + + tearDown(() async { + await depositsCubit.close(); + await formCubit.close(); + }); + + Future pump( + WidgetTester tester, { + ThemeMode themeMode = ThemeMode.light, + Size size = const Size(400, 900), + double textScale = 1, + }) async { + tester.view.physicalSize = size; + tester.view.devicePixelRatio = 1; + tester.platformDispatcher.textScaleFactorTestValue = textScale; + addTearDown(tester.view.reset); + addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue); + + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + themeMode: themeMode, + home: MultiBlocProvider( + providers: [ + BlocProvider.value(value: depositsCubit), + BlocProvider.value(value: formCubit), + ], + child: const RegularDepositsPage(), + ), ), ); } - group('RegularDepositsPage - Testes de Integração da Tela', () { - testWidgets('Deve renderizar a árvore de componentes completa com sucesso', (tester) async { - await tester.pumpWidget(createSut()); + Finder row(FinancialCalculationTarget field) => find.byKey(ValueKey(field)); + + Future type( + WidgetTester tester, + FinancialCalculationTarget field, + String text, + ) async { + await tester.enterText( + find.descendant(of: row(field), matching: find.byType(TextField)), + text, + ); + await tester.pump(); + } + + Future fillExample(WidgetTester tester) async { + await type(tester, FinancialCalculationTarget.initialValue, '20000'); + await type(tester, FinancialCalculationTarget.periods, '24'); + await type(tester, FinancialCalculationTarget.interestRate, '080'); + } + + Finder inBand(String text) => find.descendant( + of: find.byType(CalculatorBand), + matching: find.text(text), + ); + + group('RegularDepositsPage', () { + testWidgets('monta a tela na cor dos Depósitos', (tester) async { + await pump(tester); + + expect(find.text('Depósitos Regulares'), findsOneWidget); + expect(find.byType(FieldRow), findsNWidgets(4)); + for (final label in [ + 'Depósito', + 'Prazo', + 'Taxa de juros', + 'Valor final', + ]) { + expect(find.text(label), findsOneWidget); + } + expect(find.text('todo mês'), findsOneWidget); + expect(inBand('e descubra o quarto'), findsOneWidget); + + final band = tester.widget( + find + .descendant( + of: find.byType(CalculatorBand), + matching: find.byType(ColoredBox), + ) + .first, + ); + expect(band.color, CalculatorPalette.of(CalculatorKind.deposits).solid); + }); + + testWidgets('com três campos, pergunta pelo valor final', (tester) async { + await pump(tester); + await fillExample(tester); - expect(find.byType(PageHeader), findsOneWidget); - expect(find.byType(TitleWidget), findsOneWidget); - expect(find.byType(HelpCard), findsOneWidget); - expect(find.byType(FormsView), findsOneWidget); + expect(inBand('Você quer descobrir'), findsOneWidget); + expect(inBand('o valor final'), findsOneWidget); + }); + + testWidgets('200 / 24 / 0,80%: valor final, depositado, rendimento, ' + 'taxa anual e barra 90/10', (tester) async { + await pump(tester); + await fillExample(tester); + + await tester.tap(find.text('Calcular')); + await tester.pumpAndSettle(); + + expect(inBand('5.310,78'), findsOneWidget); + expect(formCubit.finalValue.text, '5.310,78'); + expect(find.text('Do valor final'), findsOneWidget); + expect(find.textContaining('4.800,00'), findsOneWidget); + expect(find.textContaining('510,78'), findsNWidgets(2)); + expect(find.text('10,03% a.a.'), findsOneWidget); + expect(find.text('Depositado 90%'), findsOneWidget); + expect(find.text('Rendimento 10%'), findsOneWidget); + }); + + testWidgets('calcula o depósito a partir do valor final', (tester) async { + await pump(tester); + await type(tester, FinancialCalculationTarget.periods, '24'); + await type(tester, FinancialCalculationTarget.interestRate, '080'); + await type(tester, FinancialCalculationTarget.finalValue, '531078'); + + await tester.tap(find.text(FieldRow.calculateText)); + await tester.pumpAndSettle(); + + expect(inBand('Para juntar R\$\u00A05.310,78, guarde por mês'), findsOne); + expect(inBand('200,00'), findsOneWidget); + }); - expect(find.text('Depósitos Regulares'), findsNWidgets(2)); + testWidgets('com menos de três campos, mostra o aviso', (tester) async { + await pump(tester); + await type(tester, FinancialCalculationTarget.initialValue, '20000'); + + expect(find.text('Faltam dados para calcular'), findsOneWidget); expect( - find.text('Calcule o valor futuro, a taxa, o prazo ou o depósito mensal.'), + find.text( + 'Preencha o prazo, a taxa de juros ou o valor final. ' + 'O campo que ficar em branco é calculado.', + ), findsOneWidget, ); }); - testWidgets('Deve garantir que a página inteira possui um scroll ativado', (tester) async { - await tester.pumpWidget(createSut()); + testWidgets('limpar e desfazer', (tester) async { + await pump(tester); + await fillExample(tester); + await tester.tap(find.text('Calcular')); + await tester.pumpAndSettle(); + + await tester.tap(find.bySemanticsLabel(CalculatorBand.clearLabel)); + await tester.pumpAndSettle(); + expect(find.byType(ResultDetails), findsNothing); + + await tester.tap(find.text('Desfazer')); + await tester.pumpAndSettle(); + expect(inBand('5.310,78'), findsOneWidget); + }); + + for (final themeMode in [ThemeMode.light, ThemeMode.dark]) { + testWidgets('em 360dp com fonte em 130% no tema ${themeMode.name}', ( + tester, + ) async { + await pump( + tester, + themeMode: themeMode, + size: const Size(360, 780), + textScale: 1.3, + ); + await fillExample(tester); + await tester.tap(find.text('Calcular')); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + expect(inBand('5.310,78'), findsOneWidget); + }); + } + + testWidgets('o teclado não cobre o campo em foco', (tester) async { + await pump(tester, size: const Size(360, 700)); + + await tester.showKeyboard( + find.descendant( + of: row(FinancialCalculationTarget.finalValue), + matching: find.byType(TextField), + ), + ); + tester.view.viewInsets = const FakeViewPadding(bottom: 320); + await tester.pumpAndSettle(); - expect(find.byType(SingleChildScrollView), findsOneWidget); + final field = tester.getRect(row(FinancialCalculationTarget.finalValue)); + final bar = tester.getRect(find.byType(BottomActionBar)); + expect(bar.bottom, 700 - 320); + expect(field.bottom, lessThanOrEqualTo(bar.top)); }); }); } diff --git a/test/modules/financial_calculators/presentation/utils/deposits_statement_test.dart b/test/modules/financial_calculators/presentation/utils/deposits_statement_test.dart new file mode 100644 index 0000000..0a6950b --- /dev/null +++ b/test/modules/financial_calculators/presentation/utils/deposits_statement_test.dart @@ -0,0 +1,98 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:real_calc/core/widgets/calculator_band/band_statement.dart'; +import 'package:real_calc/modules/financial_calculators/domain/entities/financial_calculation.dart'; +import 'package:real_calc/modules/financial_calculators/domain/enum/financial_calculation_target.dart'; +import 'package:real_calc/modules/financial_calculators/presentation/utils/deposits_statement.dart'; + +void main() { + const deposit = FinancialCalculationTarget.initialValue; + const periods = FinancialCalculationTarget.periods; + const rate = FinancialCalculationTarget.interestRate; + const finalValue = FinancialCalculationTarget.finalValue; + + final solved = FinancialCalculation( + initialValue: 200, + periods: 24, + rate: 0.8, + finalValue: 5310.7801, + ); + final nbsp = String.fromCharCode(0xA0); + String money(String value) => 'R\$$nbsp$value'; + + BandStatement statement( + Set empty, { + FinancialCalculationTarget? calculated, + }) => depositsStatement( + emptyFields: empty, + values: calculated == null ? FinancialCalculation() : solved, + calculatedField: calculated, + ); + + group('depositsStatement', () { + test('valor final calculado', () { + expect( + statement({}, calculated: finalValue), + BandStatement( + kicker: 'Guardando ${money('200,00')} por mês, você terá', + value: '5.310,78', + isMoney: true, + sub: 'em 24 meses, com ${money('510,78')} de rendimento', + ), + ); + }); + + test('depósito calculado', () { + expect( + statement({}, calculated: deposit), + BandStatement( + kicker: 'Para juntar ${money('5.310,78')}, guarde por mês', + value: '200,00', + isMoney: true, + sub: 'durante 24 meses', + ), + ); + }); + + test('prazo calculado', () { + expect( + statement({}, calculated: periods), + BandStatement( + kicker: + 'Guardando ${money('200,00')} por mês, você junta ' + '${money('5.310,78')} em', + value: '24 meses', + sub: 'com ${money('510,78')} de rendimento', + ), + ); + }); + + test('taxa calculada', () { + expect( + statement({}, calculated: rate), + const BandStatement( + kicker: 'Para isso, o dinheiro precisa render', + value: '0,80% ao mês', + sub: '10,03% ao ano', + ), + ); + }); + + test('inicial, pergunta e aviso seguem o Financiamento', () { + expect( + statement({deposit, periods, rate, finalValue}).value, + 'e descubra o quarto', + ); + expect(statement({finalValue}).value, 'o valor final'); + expect(statement({deposit}).value, 'o depósito'); + expect(statement({rate, finalValue}).value, 'falta 1 campo'); + }); + + test('aviso cita os campos vazios', () { + expect( + depositsMissingFieldsMessage({deposit, finalValue}), + 'Preencha o depósito ou o valor final. ' + 'O campo que ficar em branco é calculado.', + ); + }); + }); +} From 75b58d1d30c904d9f9aacbd26bf78d1820e801ee Mon Sep 17 00:00:00 2001 From: Rodrigo Mamede <61851910+Rodrigosdk@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:06:18 -0300 Subject: [PATCH 12/22] feat: migrar calculadora de Valor Futuro (#112) * refactor: usar valor aplicado e valor futuro nos detalhes e na ajuda do Valor Futuro (#112) * feat: adicionar FutureValueStatement para montar a frase da faixa do Valor Futuro (#112) * feat: migrar a FutureValuePage para a estrutura do Financiamento (#112) --- lib/core/widgets/help_sheet/help_content.dart | 11 +- lib/modules/financial_calculators/module.dart | 7 +- .../presentation/pages/future_value_page.dart | 141 ++++------- .../utils/future_value_statement.dart | 56 ++++ .../presentation/utils/result_summaries.dart | 14 +- .../pages/future_value_page_test.dart | 239 ++++++++++++++---- .../utils/future_value_statement_test.dart | 96 +++++++ .../utils/result_summaries_test.dart | 7 +- 8 files changed, 410 insertions(+), 161 deletions(-) create mode 100644 lib/modules/financial_calculators/presentation/utils/future_value_statement.dart create mode 100644 test/modules/financial_calculators/presentation/utils/future_value_statement_test.dart diff --git a/lib/core/widgets/help_sheet/help_content.dart b/lib/core/widgets/help_sheet/help_content.dart index 981496f..9f00bda 100644 --- a/lib/core/widgets/help_sheet/help_content.dart +++ b/lib/core/widgets/help_sheet/help_content.dart @@ -81,18 +81,21 @@ class HelpContent { static const futureValue = HelpContent( intro: 'Veja quanto um valor aplicado hoje rende com juros compostos, ou ' - 'descubra o capital, o prazo ou a taxa.', + 'descubra o valor aplicado, o prazo ou a taxa.', fields: [ - HelpField('Capital', 'O valor aplicado hoje, uma única vez.'), + HelpField('Valor aplicado', 'O valor aplicado hoje, uma única vez.'), HelpField('Prazo', 'Por quantos meses o dinheiro fica aplicado.'), _rate, - HelpField('Valor final', 'Quanto o capital vale no fim do prazo.'), + HelpField( + 'Valor futuro', + 'Quanto o valor aplicado vale no fim do prazo.', + ), ], example: 'R\$ 1.000,00 a 1% ao mês, durante 12 meses, viram', exampleResult: 'R\$ 1.126,83', methodology: 'Juros compostos: os juros de cada mês também passam a render. VP ' - 'é o capital, i a taxa mensal e n o prazo.', + 'é o valor aplicado, i a taxa mensal e n o prazo.', formula: 'VF = VP × (1 + i)ⁿ', ); diff --git a/lib/modules/financial_calculators/module.dart b/lib/modules/financial_calculators/module.dart index e7ea5c0..142928c 100644 --- a/lib/modules/financial_calculators/module.dart +++ b/lib/modules/financial_calculators/module.dart @@ -1,8 +1,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:real_calc/modules/app_module.dart'; -import 'package:real_calc/core/themes/calculator_scope.dart'; -import 'package:real_calc/core/themes/calculator_palette.dart'; import 'package:real_calc/modules/financial_calculators/presentation/pages/future_value_page.dart'; import 'package:real_calc/modules/financial_calculators/use_case/future_value/i_calculate_future_value.dart'; import 'package:real_calc/modules/financial_calculators/use_case/regular_deposits/i_calculate_deposit.dart'; @@ -61,10 +59,7 @@ class FinancialCalculatorsModule extends AppModule { BlocProvider(create: (_) => Modular.get()), BlocProvider(create: (_) => Modular.get()), ], - child: const CalculatorScope( - kind: CalculatorKind.futureValue, - child: FutureValuePage(), - ), + child: const FutureValuePage(), ), ); } diff --git a/lib/modules/financial_calculators/presentation/pages/future_value_page.dart b/lib/modules/financial_calculators/presentation/pages/future_value_page.dart index 8f7e679..6fb863e 100644 --- a/lib/modules/financial_calculators/presentation/pages/future_value_page.dart +++ b/lib/modules/financial_calculators/presentation/pages/future_value_page.dart @@ -1,120 +1,81 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:real_calc/core/themes/spacing.dart'; -import 'package:real_calc/core/widgets/page_header.dart'; -import 'package:real_calc/core/widgets/title_widget.dart'; -import 'package:real_calc/modules/financial_calculators/presentation/models/field_spec.dart'; -import 'package:real_calc/core/widgets/help_card.dart'; +import 'package:real_calc/core/themes/calculator_palette.dart'; +import 'package:real_calc/core/themes/calculator_scope.dart'; +import 'package:real_calc/core/utils/decimal_input_formatter.dart'; -import '../../../../core/utils/decimal_input_formatter.dart'; import '../../domain/enum/financial_calculation_target.dart'; -import '../../../../core/widgets/types/input_field_state.dart'; import '../cubit/forms/financing_form_cubit.dart'; import '../cubit/future_value/future_value_cubit.dart'; -import '../widgets/forms_view.dart'; +import '../utils/future_value_statement.dart'; +import '../utils/result_summaries.dart'; +import '../widgets/calculator_form_view.dart'; class FutureValuePage extends StatelessWidget { - static final List> _fieldSpecs = [ - FieldSpec( - field: FinancialCalculationTarget.initialValue, - label: 'Capital (R\$)', + static const title = 'Valor Futuro'; + + static final fields = [ + CalculatorFieldConfig( + target: FinancialCalculationTarget.initialValue, + label: 'Valor aplicado', + prefix: 'R\$', hint: '0,00', - icon: Icons.attach_money, + keyboardType: const TextInputType.numberWithOptions(decimal: true), formatters: [DecimalInputFormatter()], ), - FieldSpec( - field: FinancialCalculationTarget.periods, - label: 'Prazo (meses)', + CalculatorFieldConfig( + target: FinancialCalculationTarget.periods, + label: 'Prazo', + unit: 'meses', hint: '0', - icon: Icons.calendar_today, + keyboardType: TextInputType.number, formatters: [FilteringTextInputFormatter.digitsOnly], ), - FieldSpec( - field: FinancialCalculationTarget.interestRate, - label: 'Taxa de juros (% ao mês)', + CalculatorFieldConfig( + target: FinancialCalculationTarget.interestRate, + label: 'Taxa de juros', + unit: '% a.m.', hint: '0,00', - icon: Icons.percent, + keyboardType: const TextInputType.numberWithOptions(decimal: true), formatters: [DecimalInputFormatter()], ), - FieldSpec( - field: FinancialCalculationTarget.finalValue, - label: 'Valor final', + CalculatorFieldConfig( + target: FinancialCalculationTarget.finalValue, + label: 'Valor futuro', + prefix: 'R\$', hint: '0,00', - icon: Icons.payments, + keyboardType: const TextInputType.numberWithOptions(decimal: true), formatters: [DecimalInputFormatter()], ), ]; const FutureValuePage({super.key}); - BlocListener _buildForms() { - return BlocListener( - listener: (context, state) { - final formCubit = context.read(); - if (state is FutureValueCalculated) { - formCubit.applyResult(state.value, state.calculatedField); - } else if (state is FutureValueError) { - formCubit.invalidateCalculatedField(); - } - }, - child: BlocBuilder( - builder: (context, financingState) { - final formCubit = context.watch(); - final formState = formCubit.state; - - void calculate() { - if (formState.canCalculate) { - context.read().calculate(formCubit.financing); - } - } - - return FormsView( - fieldSpecs: _fieldSpecs, - controllerFor: formCubit.controllerFor, - stateOf: formState.stateOf, - isLoading: financingState is FutureValueLoading, - errorMessage: financingState is FutureValueError - ? financingState.message ?? 'Erro ao calcular.' - : null, - onCalculate: calculate, - onClear: formCubit.clear, - onFieldTap: (field) { - if (formState.stateOf(field) == InputFieldState.unknown) { - calculate(); - } - }, - ); - }, - ), - ); - } - @override Widget build(BuildContext context) { - return Scaffold( - appBar: PageHeader(title: 'Valor Futuro'), - body: SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md + 4, - vertical: AppSpacing.md + 4, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: AppSpacing.lg, - children: [ - const TitleWidget( - title: 'Valor Futuro', - subtitle: 'Calcule o valor futuro do seu capital.', - ), - const HelpCard( - message: - 'Preencha 3 campos e toque no 4° para calcular automaticamente', - ), - _buildForms(), - ], - ), + return CalculatorScope( + kind: CalculatorKind.futureValue, + child: BlocConsumer( + listener: (context, state) { + final formCubit = context.read(); + if (state is FutureValueCalculated) { + formCubit.applyResult(state.value, state.calculatedField); + } else if (state is FutureValueError) { + formCubit.invalidateCalculatedField(); + } + }, + builder: (context, state) => CalculatorFormView( + kind: CalculatorKind.futureValue, + title: title, + fields: fields, + statement: futureValueStatement, + missingMessage: futureValueMissingFieldsMessage, + summary: futureValueResultSummary, + onCalculate: context.read().calculate, + errorMessage: state is FutureValueError + ? state.message ?? 'Confira os valores.' + : null, ), ), ); diff --git a/lib/modules/financial_calculators/presentation/utils/future_value_statement.dart b/lib/modules/financial_calculators/presentation/utils/future_value_statement.dart new file mode 100644 index 0000000..3843190 --- /dev/null +++ b/lib/modules/financial_calculators/presentation/utils/future_value_statement.dart @@ -0,0 +1,56 @@ +import 'package:real_calc/core/utils/br_number_format.dart'; +import 'package:real_calc/core/widgets/calculator_band/band_statement.dart'; + +import '../../domain/entities/financial_calculation.dart'; +import '../../domain/enum/financial_calculation_target.dart'; +import 'calculator_statement.dart'; + +String futureValueFieldName(FinancialCalculationTarget field) => + switch (field) { + FinancialCalculationTarget.initialValue => 'o valor aplicado', + FinancialCalculationTarget.periods => 'o prazo', + FinancialCalculationTarget.interestRate => 'a taxa de juros', + FinancialCalculationTarget.finalValue => 'o valor futuro', + }; + +BandStatement futureValueStatement({ + required Set emptyFields, + required FinancialCalculation values, + FinancialCalculationTarget? calculatedField, +}) { + if (calculatedField == null) { + return pendingStatement(emptyFields, futureValueFieldName); + } + + final applied = money(values.initialValue); + final future = money(values.finalValue); + final yieldCents = toCents(values.finalValue) - toCents(values.initialValue); + final yieldText = '${formatCents(yieldCents)} de rendimento'; + + return switch (calculatedField) { + FinancialCalculationTarget.finalValue => BandStatement( + kicker: 'Aplicando $applied hoje, você terá', + value: moneyValue(values.finalValue), + isMoney: true, + sub: 'em ${months(values.periods)}, com $yieldText', + ), + FinancialCalculationTarget.initialValue => BandStatement( + kicker: 'Para ter $future, aplique hoje', + value: moneyValue(values.initialValue), + isMoney: true, + sub: 'por ${months(values.periods)}', + ), + FinancialCalculationTarget.periods => BandStatement( + kicker: '$applied viram $future em', + value: months(values.periods), + sub: 'com $yieldText', + ), + FinancialCalculationTarget.interestRate => rateStatement( + values.rate, + kicker: 'Para isso, o dinheiro precisa render', + ), + }; +} + +String futureValueMissingFieldsMessage(Set empty) => + missingFieldsMessage(empty, futureValueFieldName); diff --git a/lib/modules/financial_calculators/presentation/utils/result_summaries.dart b/lib/modules/financial_calculators/presentation/utils/result_summaries.dart index 4733c77..a1a52d1 100644 --- a/lib/modules/financial_calculators/presentation/utils/result_summaries.dart +++ b/lib/modules/financial_calculators/presentation/utils/result_summaries.dart @@ -51,22 +51,22 @@ ResultSummary regularDepositsResultSummary(FinancialCalculation calculation) { ); } -/// Valor Futuro: [FinancialCalculation.initialValue] é o capital e +/// Valor Futuro: [FinancialCalculation.initialValue] é o valor aplicado e /// [FinancialCalculation.finalValue], o valor final. ResultSummary futureValueResultSummary(FinancialCalculation calculation) { - final capital = toCents(calculation.initialValue); - final yield_ = toCents(calculation.finalValue) - capital; + final applied = toCents(calculation.initialValue); + final yield_ = toCents(calculation.finalValue) - applied; return ResultSummary( - title: 'Do valor final', + title: 'Do valor futuro', composition: ResultComposition.fromCents( - ownLabel: 'Capital', - ownCents: capital, + ownLabel: 'Valor aplicado', + ownCents: applied, otherLabel: 'Rendimento', otherCents: yield_, ), lines: [ - ResultDetailLine('Capital investido', formatCents(capital)), + ResultDetailLine('Valor aplicado', formatCents(applied)), ResultDetailLine('Rendimento', formatCents(yield_)), _annualRateLine(calculation.rate), ], diff --git a/test/modules/financial_calculators/presentation/pages/future_value_page_test.dart b/test/modules/financial_calculators/presentation/pages/future_value_page_test.dart index ae7500c..a71f0d1 100644 --- a/test/modules/financial_calculators/presentation/pages/future_value_page_test.dart +++ b/test/modules/financial_calculators/presentation/pages/future_value_page_test.dart @@ -1,82 +1,217 @@ -import 'package:flutter/rendering.dart'; -import 'package:flutter_test/flutter_test.dart'; - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:real_calc/core/themes/app_theme.dart'; -import 'package:real_calc/core/widgets/page_header.dart'; -import 'package:real_calc/core/widgets/title_widget.dart'; +import 'package:real_calc/core/themes/calculator_palette.dart'; +import 'package:real_calc/core/widgets/bottom_action_bar.dart'; +import 'package:real_calc/core/widgets/calculator_band/calculator_band.dart'; +import 'package:real_calc/core/widgets/field_list/field_row.dart'; +import 'package:real_calc/core/widgets/result_details/result_details.dart'; import 'package:real_calc/modules/financial_calculators/domain/enum/financial_calculation_target.dart'; import 'package:real_calc/modules/financial_calculators/presentation/cubit/forms/financing_form_cubit.dart'; import 'package:real_calc/modules/financial_calculators/presentation/cubit/future_value/future_value_cubit.dart'; import 'package:real_calc/modules/financial_calculators/presentation/pages/future_value_page.dart'; -import 'package:real_calc/modules/financial_calculators/presentation/widgets/forms_view.dart'; -import 'package:real_calc/core/widgets/help_card.dart'; - -import '../../../../test_harness.dart'; +import 'package:real_calc/modules/financial_calculators/use_case/future_value/calculate_future_value.dart'; void main() { - late TestHarness harness; - + late FutureValueCubit futureValueCubit; + late FinancingFormCubit formCubit; + setUp(() { - harness = TestHarness()..setUpDefaults(); + futureValueCubit = FutureValueCubit(CalculateFutureValue()); + formCubit = FinancingFormCubit(); + }); + + tearDown(() async { + await futureValueCubit.close(); + await formCubit.close(); }); - - tearDown(() => harness.dispose()); - - Widget createSut() { - return MaterialApp( - theme: AppTheme.lightTheme, - darkTheme: AppTheme.darkTheme, - themeMode: ThemeMode.dark, - home: MultiBlocProvider( - providers: [ - BlocProvider.value( - value: harness.futureValueCubit, - ), - BlocProvider.value( - value: harness.financingFormCubit, - ), - ], - child: const FutureValuePage(), + + Future pump( + WidgetTester tester, { + ThemeMode themeMode = ThemeMode.light, + Size size = const Size(400, 900), + double textScale = 1, + }) async { + tester.view.physicalSize = size; + tester.view.devicePixelRatio = 1; + tester.platformDispatcher.textScaleFactorTestValue = textScale; + addTearDown(tester.view.reset); + addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue); + + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + themeMode: themeMode, + home: MultiBlocProvider( + providers: [ + BlocProvider.value(value: futureValueCubit), + BlocProvider.value(value: formCubit), + ], + child: const FutureValuePage(), + ), ), ); } - group('FutureValuePage - Testes de Integração da Tela', () { - testWidgets('Deve renderizar a árvore de componentes completa com sucesso', (tester) async { - await tester.pumpWidget(createSut()); + Finder row(FinancialCalculationTarget field) => find.byKey(ValueKey(field)); + + Future type( + WidgetTester tester, + FinancialCalculationTarget field, + String text, + ) async { + await tester.enterText( + find.descendant(of: row(field), matching: find.byType(TextField)), + text, + ); + await tester.pump(); + } + + Future fillExample(WidgetTester tester) async { + await type(tester, FinancialCalculationTarget.initialValue, '255555'); + await type(tester, FinancialCalculationTarget.periods, '25'); + await type(tester, FinancialCalculationTarget.interestRate, '033'); + } + + Finder inBand(String text) => find.descendant( + of: find.byType(CalculatorBand), + matching: find.text(text), + ); + + group('FutureValuePage', () { + testWidgets('monta a tela na cor do Valor Futuro', (tester) async { + await pump(tester); - expect(find.byType(PageHeader), findsOneWidget); - expect(find.byType(TitleWidget), findsOneWidget); - expect(find.byType(HelpCard), findsOneWidget); - expect(find.byType(FormsView), findsOneWidget); + expect(find.text('Valor Futuro'), findsOneWidget); + expect(find.byType(FieldRow), findsNWidgets(4)); + for (final label in [ + 'Valor aplicado', + 'Prazo', + 'Taxa de juros', + 'Valor futuro', + ]) { + expect(find.text(label), findsOneWidget); + } + expect(inBand('e descubra o quarto'), findsOneWidget); + + final band = tester.widget( + find + .descendant( + of: find.byType(CalculatorBand), + matching: find.byType(ColoredBox), + ) + .first, + ); + expect( + band.color, + CalculatorPalette.of(CalculatorKind.futureValue).solid, + ); }); - testWidgets('Deve garantir que a página inteira possui um scroll ativado', (tester) async { - await tester.pumpWidget(createSut()); + testWidgets('com três campos, pergunta pelo valor futuro', (tester) async { + await pump(tester); + await fillExample(tester); - expect(find.byType(SingleChildScrollView), findsOneWidget); + expect(inBand('Você quer descobrir'), findsOneWidget); + expect(inBand('o valor futuro'), findsOneWidget); }); - }); - group("FutureValuePage - Testes da PageHeader", () { - testWidgets('Deve conseguir criar uma PageHeader', (tester) async { - await tester.pumpWidget(createSut()); - expect(find.byType(PageHeader), findsOneWidget); + testWidgets('2.555,55 / 25 / 0,33%: valor futuro, rendimento, taxa anual ' + 'e barra 92/8', (tester) async { + await pump(tester); + await fillExample(tester); + + await tester.tap(find.text('Calcular')); + await tester.pumpAndSettle(); + + expect(inBand('2.774,95'), findsOneWidget); + expect(formCubit.finalValue.text, '2.774,95'); + expect(find.text('Do valor futuro'), findsOneWidget); + expect(find.textContaining('219,40'), findsNWidgets(2)); + expect(find.text('4,03% a.a.'), findsOneWidget); + expect(find.text('Valor aplicado 92%'), findsOneWidget); + expect(find.text('Rendimento 8%'), findsOneWidget); + }); + + testWidgets('calcula o prazo a partir do valor futuro', (tester) async { + await pump(tester); + await type(tester, FinancialCalculationTarget.initialValue, '255555'); + await type(tester, FinancialCalculationTarget.interestRate, '033'); + await type(tester, FinancialCalculationTarget.finalValue, '277495'); + + await tester.tap(find.text(FieldRow.calculateText)); + await tester.pumpAndSettle(); + + expect(inBand('25 meses'), findsOneWidget); + }); + + testWidgets('com menos de três campos, mostra o aviso', (tester) async { + await pump(tester); + await type(tester, FinancialCalculationTarget.initialValue, '255555'); + await type(tester, FinancialCalculationTarget.periods, '25'); + + expect(find.text('Faltam dados para calcular'), findsOneWidget); + expect( + find.text( + 'Preencha a taxa de juros ou o valor futuro. ' + 'O campo que ficar em branco é calculado.', + ), + findsOneWidget, + ); + }); + + testWidgets('limpar e desfazer', (tester) async { + await pump(tester); + await fillExample(tester); + await tester.tap(find.text('Calcular')); + await tester.pumpAndSettle(); + + await tester.tap(find.bySemanticsLabel(CalculatorBand.clearLabel)); + await tester.pumpAndSettle(); + expect(find.byType(ResultDetails), findsNothing); + + await tester.tap(find.text('Desfazer')); + await tester.pumpAndSettle(); + expect(inBand('2.774,95'), findsOneWidget); }); + for (final themeMode in [ThemeMode.light, ThemeMode.dark]) { + testWidgets('em 360dp com fonte em 130% no tema ${themeMode.name}', ( + tester, + ) async { + await pump( + tester, + themeMode: themeMode, + size: const Size(360, 780), + textScale: 1.3, + ); + await fillExample(tester); + await tester.tap(find.text('Calcular')); + await tester.pumpAndSettle(); - testWidgets("Deve conter uma fontSize de 18 e fontWeight bold", (tester) async { - await tester.pumpWidget(createSut()); + expect(tester.takeException(), isNull); + expect(inBand('2.774,95'), findsOneWidget); + }); + } - final textFinder = find.text('Valor Futuro'); - final RenderParagraph renderObject = tester.renderObject(textFinder.first); + testWidgets('o teclado não cobre o campo em foco', (tester) async { + await pump(tester, size: const Size(360, 700)); - final TextStyle? resolvedStyle = renderObject.text.style; + await tester.showKeyboard( + find.descendant( + of: row(FinancialCalculationTarget.finalValue), + matching: find.byType(TextField), + ), + ); + tester.view.viewInsets = const FakeViewPadding(bottom: 320); + await tester.pumpAndSettle(); - expect(resolvedStyle?.fontSize, 18); - expect(resolvedStyle?.fontWeight, FontWeight.w700); + final field = tester.getRect(row(FinancialCalculationTarget.finalValue)); + final bar = tester.getRect(find.byType(BottomActionBar)); + expect(bar.bottom, 700 - 320); + expect(field.bottom, lessThanOrEqualTo(bar.top)); }); }); } diff --git a/test/modules/financial_calculators/presentation/utils/future_value_statement_test.dart b/test/modules/financial_calculators/presentation/utils/future_value_statement_test.dart new file mode 100644 index 0000000..68b1381 --- /dev/null +++ b/test/modules/financial_calculators/presentation/utils/future_value_statement_test.dart @@ -0,0 +1,96 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:real_calc/core/widgets/calculator_band/band_statement.dart'; +import 'package:real_calc/modules/financial_calculators/domain/entities/financial_calculation.dart'; +import 'package:real_calc/modules/financial_calculators/domain/enum/financial_calculation_target.dart'; +import 'package:real_calc/modules/financial_calculators/presentation/utils/future_value_statement.dart'; + +void main() { + const applied = FinancialCalculationTarget.initialValue; + const periods = FinancialCalculationTarget.periods; + const rate = FinancialCalculationTarget.interestRate; + const future = FinancialCalculationTarget.finalValue; + + final solved = FinancialCalculation( + initialValue: 2555.55, + periods: 25, + rate: 0.33, + finalValue: 2774.947, + ); + final nbsp = String.fromCharCode(0xA0); + String money(String value) => 'R\$$nbsp$value'; + + BandStatement statement( + Set empty, { + FinancialCalculationTarget? calculated, + }) => futureValueStatement( + emptyFields: empty, + values: calculated == null ? FinancialCalculation() : solved, + calculatedField: calculated, + ); + + group('futureValueStatement', () { + test('valor futuro calculado', () { + expect( + statement({}, calculated: future), + BandStatement( + kicker: 'Aplicando ${money('2.555,55')} hoje, você terá', + value: '2.774,95', + isMoney: true, + sub: 'em 25 meses, com ${money('219,40')} de rendimento', + ), + ); + }); + + test('valor aplicado calculado', () { + expect( + statement({}, calculated: applied), + BandStatement( + kicker: 'Para ter ${money('2.774,95')}, aplique hoje', + value: '2.555,55', + isMoney: true, + sub: 'por 25 meses', + ), + ); + }); + + test('prazo calculado', () { + expect( + statement({}, calculated: periods), + BandStatement( + kicker: '${money('2.555,55')} viram ${money('2.774,95')} em', + value: '25 meses', + sub: 'com ${money('219,40')} de rendimento', + ), + ); + }); + + test('taxa calculada', () { + expect( + statement({}, calculated: rate), + const BandStatement( + kicker: 'Para isso, o dinheiro precisa render', + value: '0,33% ao mês', + sub: '4,03% ao ano', + ), + ); + }); + + test('inicial, pergunta e aviso seguem o Financiamento', () { + expect( + statement({applied, periods, rate, future}).value, + 'e descubra o quarto', + ); + expect(statement({future}).value, 'o valor futuro'); + expect(statement({applied}).value, 'o valor aplicado'); + expect(statement({periods, rate, future}).value, 'faltam 2 campos'); + }); + + test('aviso cita os campos vazios', () { + expect( + futureValueMissingFieldsMessage({rate, future}), + 'Preencha a taxa de juros ou o valor futuro. ' + 'O campo que ficar em branco é calculado.', + ); + }); + }); +} diff --git a/test/modules/financial_calculators/presentation/utils/result_summaries_test.dart b/test/modules/financial_calculators/presentation/utils/result_summaries_test.dart index 9304104..3868f0d 100644 --- a/test/modules/financial_calculators/presentation/utils/result_summaries_test.dart +++ b/test/modules/financial_calculators/presentation/utils/result_summaries_test.dart @@ -67,11 +67,14 @@ void main() { ); expect(summary.lines, const [ - ResultDetailLine('Capital investido', 'R\$\u00A01.000,00'), + ResultDetailLine('Valor aplicado', 'R\$\u00A01.000,00'), ResultDetailLine('Rendimento', 'R\$\u00A0126,83'), ResultDetailLine('Taxa anual equivalente', '12,68% a.a.'), ]); - expect(summary.composition!.own, const ResultSegment('Capital', 89)); + expect( + summary.composition!.own, + const ResultSegment('Valor aplicado', 89), + ); expect(summary.composition!.other, const ResultSegment('Rendimento', 11)); }); From f4d0abe96a3d26b187466d58b494100271127b35 Mon Sep 17 00:00:00 2001 From: Rodrigo Mamede <61851910+Rodrigosdk@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:41:07 -0300 Subject: [PATCH 13/22] feat: compartilhar resultados de todas as calculadoras (#122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: generalizar ValueCorrectionSharer em ResultSharer no core (#122) * feat: gerar o texto de compartilhamento das calculadoras financeiras (#122) * feat: ligar o Compartilhar das três calculadoras, ancorado no botão (#122) --- lib/core/services/result_sharer.dart | 6 + .../services/share_plus_result_sharer.dart | 16 +++ lib/core/utils/share_text.dart | 24 ++++ lib/core/widgets/bottom_action_bar.dart | 29 +++-- .../presentation/pages/financing_page.dart | 9 +- .../presentation/pages/future_value_page.dart | 12 +- .../pages/regular_deposits_page.dart | 12 +- .../presentation/utils/share_texts.dart | 113 ++++++++++++++++++ .../widgets/calculator_form_view.dart | 12 ++ .../services/value_correction_sharer.dart | 3 - .../share_plus_value_correction_sharer.dart | 11 -- lib/modules/value_correction/module.dart | 8 +- .../pages/value_correction_result_page.dart | 12 +- .../utils/value_correction_share_text.dart | 3 +- test/core/widgets/bottom_action_bar_test.dart | 7 +- .../pages/financing_page_test.dart | 32 ++++- .../presentation/utils/share_texts_test.dart | 103 ++++++++++++++++ 17 files changed, 369 insertions(+), 43 deletions(-) create mode 100644 lib/core/services/result_sharer.dart create mode 100644 lib/core/services/share_plus_result_sharer.dart create mode 100644 lib/core/utils/share_text.dart create mode 100644 lib/modules/financial_calculators/presentation/utils/share_texts.dart delete mode 100644 lib/modules/value_correction/domain/services/value_correction_sharer.dart delete mode 100644 lib/modules/value_correction/infrastructure/services/share_plus_value_correction_sharer.dart create mode 100644 test/modules/financial_calculators/presentation/utils/share_texts_test.dart diff --git a/lib/core/services/result_sharer.dart b/lib/core/services/result_sharer.dart new file mode 100644 index 0000000..128fb6d --- /dev/null +++ b/lib/core/services/result_sharer.dart @@ -0,0 +1,6 @@ +import 'dart:ui'; + +abstract interface class ResultSharer { + /// [origin] ancora o popover no iPad. + Future share(String text, {Rect? origin}); +} diff --git a/lib/core/services/share_plus_result_sharer.dart b/lib/core/services/share_plus_result_sharer.dart new file mode 100644 index 0000000..ec573d4 --- /dev/null +++ b/lib/core/services/share_plus_result_sharer.dart @@ -0,0 +1,16 @@ +import 'dart:ui'; + +import 'package:share_plus/share_plus.dart'; + +import 'result_sharer.dart'; + +class SharePlusResultSharer implements ResultSharer { + const SharePlusResultSharer(); + + @override + Future share(String text, {Rect? origin}) async { + await SharePlus.instance.share( + ShareParams(text: text, sharePositionOrigin: origin), + ); + } +} diff --git a/lib/core/utils/share_text.dart b/lib/core/utils/share_text.dart new file mode 100644 index 0000000..5f09a32 --- /dev/null +++ b/lib/core/utils/share_text.dart @@ -0,0 +1,24 @@ +import 'package:real_calc/core/widgets/calculator_band/band_statement.dart'; + +const shareDisclaimer = 'Simulação feita no RealCalc, sem valor oficial.'; + +String shareText({ + required String calculator, + required BandStatement statement, + required List<(String, String)> lines, +}) { + final headline = [ + ?statement.kicker, + statement.isMoney ? 'R\$ ${statement.value}' : statement.value, + ].join(' '); + + return [ + 'RealCalc · $calculator', + headline, + ?statement.sub, + '', + for (final (label, value) in lines) '$label: $value', + '', + shareDisclaimer, + ].join('\n').replaceAll(String.fromCharCode(0xA0), ' '); +} diff --git a/lib/core/widgets/bottom_action_bar.dart b/lib/core/widgets/bottom_action_bar.dart index c842648..60d44e5 100644 --- a/lib/core/widgets/bottom_action_bar.dart +++ b/lib/core/widgets/bottom_action_bar.dart @@ -13,7 +13,7 @@ class BottomActionBar extends StatelessWidget { final String primaryLabel; final VoidCallback? onPrimary; - final VoidCallback? onShare; + final ValueChanged? onShare; const BottomActionBar({ super.key, @@ -48,16 +48,20 @@ class BottomActionBar extends StatelessWidget { label: shareLabel, button: true, enabled: onShare != null, - child: OutlinedButton( - onPressed: onShare, - style: OutlinedButton.styleFrom( - padding: EdgeInsets.zero, - shape: const CircleBorder(), - side: BorderSide(color: colors.hairline, width: 1.5), - foregroundColor: colors.textPrimary, - disabledForegroundColor: colors.textPrimary, + child: Builder( + builder: (buttonContext) => OutlinedButton( + onPressed: onShare == null + ? null + : () => onShare!(_globalRect(buttonContext)), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.zero, + shape: const CircleBorder(), + side: BorderSide(color: colors.hairline, width: 1.5), + foregroundColor: colors.textPrimary, + disabledForegroundColor: colors.textPrimary, + ), + child: const Icon(Icons.ios_share, size: 22), ), - child: const Icon(Icons.ios_share, size: 22), ), ), ), @@ -104,3 +108,8 @@ showFieldsClearedSnackBar( ), ); } + +Rect _globalRect(BuildContext context) { + final box = context.findRenderObject()! as RenderBox; + return box.localToGlobal(Offset.zero) & box.size; +} diff --git a/lib/modules/financial_calculators/presentation/pages/financing_page.dart b/lib/modules/financial_calculators/presentation/pages/financing_page.dart index 19349ef..360b60a 100644 --- a/lib/modules/financial_calculators/presentation/pages/financing_page.dart +++ b/lib/modules/financial_calculators/presentation/pages/financing_page.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:real_calc/core/services/result_sharer.dart'; +import 'package:real_calc/core/services/share_plus_result_sharer.dart'; import 'package:real_calc/core/themes/calculator_palette.dart'; import 'package:real_calc/core/themes/calculator_scope.dart'; import 'package:real_calc/core/utils/decimal_input_formatter.dart'; @@ -10,6 +12,7 @@ import '../cubit/financing/financing_cubit.dart'; import '../cubit/forms/financing_form_cubit.dart'; import '../utils/financing_statement.dart'; import '../utils/result_summaries.dart'; +import '../utils/share_texts.dart'; import '../widgets/calculator_form_view.dart'; class FinancingPage extends StatelessWidget { @@ -51,7 +54,9 @@ class FinancingPage extends StatelessWidget { ), ]; - const FinancingPage({super.key}); + final ResultSharer sharer; + + const FinancingPage({super.key, this.sharer = const SharePlusResultSharer()}); @override Widget build(BuildContext context) { @@ -74,6 +79,8 @@ class FinancingPage extends StatelessWidget { missingMessage: financingMissingFieldsMessage, summary: financingResultSummary, onCalculate: context.read().calculate, + shareText: financingShareText, + sharer: sharer, errorMessage: state is FinancingError ? state.message ?? 'Confira os valores.' : null, diff --git a/lib/modules/financial_calculators/presentation/pages/future_value_page.dart b/lib/modules/financial_calculators/presentation/pages/future_value_page.dart index 6fb863e..d1803e0 100644 --- a/lib/modules/financial_calculators/presentation/pages/future_value_page.dart +++ b/lib/modules/financial_calculators/presentation/pages/future_value_page.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:real_calc/core/services/result_sharer.dart'; +import 'package:real_calc/core/services/share_plus_result_sharer.dart'; import 'package:real_calc/core/themes/calculator_palette.dart'; import 'package:real_calc/core/themes/calculator_scope.dart'; import 'package:real_calc/core/utils/decimal_input_formatter.dart'; @@ -10,6 +12,7 @@ import '../cubit/forms/financing_form_cubit.dart'; import '../cubit/future_value/future_value_cubit.dart'; import '../utils/future_value_statement.dart'; import '../utils/result_summaries.dart'; +import '../utils/share_texts.dart'; import '../widgets/calculator_form_view.dart'; class FutureValuePage extends StatelessWidget { @@ -50,7 +53,12 @@ class FutureValuePage extends StatelessWidget { ), ]; - const FutureValuePage({super.key}); + final ResultSharer sharer; + + const FutureValuePage({ + super.key, + this.sharer = const SharePlusResultSharer(), + }); @override Widget build(BuildContext context) { @@ -73,6 +81,8 @@ class FutureValuePage extends StatelessWidget { missingMessage: futureValueMissingFieldsMessage, summary: futureValueResultSummary, onCalculate: context.read().calculate, + shareText: futureValueShareText, + sharer: sharer, errorMessage: state is FutureValueError ? state.message ?? 'Confira os valores.' : null, diff --git a/lib/modules/financial_calculators/presentation/pages/regular_deposits_page.dart b/lib/modules/financial_calculators/presentation/pages/regular_deposits_page.dart index 968eefc..b6c097a 100644 --- a/lib/modules/financial_calculators/presentation/pages/regular_deposits_page.dart +++ b/lib/modules/financial_calculators/presentation/pages/regular_deposits_page.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:real_calc/core/services/result_sharer.dart'; +import 'package:real_calc/core/services/share_plus_result_sharer.dart'; import 'package:real_calc/core/themes/calculator_palette.dart'; import 'package:real_calc/core/themes/calculator_scope.dart'; import 'package:real_calc/core/utils/decimal_input_formatter.dart'; @@ -10,6 +12,7 @@ import '../cubit/forms/financing_form_cubit.dart'; import '../cubit/regular_deposits/regular_deposits_cubit.dart'; import '../utils/deposits_statement.dart'; import '../utils/result_summaries.dart'; +import '../utils/share_texts.dart'; import '../widgets/calculator_form_view.dart'; class RegularDepositsPage extends StatelessWidget { @@ -51,7 +54,12 @@ class RegularDepositsPage extends StatelessWidget { ), ]; - const RegularDepositsPage({super.key}); + final ResultSharer sharer; + + const RegularDepositsPage({ + super.key, + this.sharer = const SharePlusResultSharer(), + }); @override Widget build(BuildContext context) { @@ -74,6 +82,8 @@ class RegularDepositsPage extends StatelessWidget { missingMessage: depositsMissingFieldsMessage, summary: regularDepositsResultSummary, onCalculate: context.read().calculate, + shareText: depositsShareText, + sharer: sharer, errorMessage: state is RegularDepositsError ? state.message ?? 'Confira os valores.' : null, diff --git a/lib/modules/financial_calculators/presentation/utils/share_texts.dart b/lib/modules/financial_calculators/presentation/utils/share_texts.dart new file mode 100644 index 0000000..e2d25a2 --- /dev/null +++ b/lib/modules/financial_calculators/presentation/utils/share_texts.dart @@ -0,0 +1,113 @@ +import 'package:real_calc/core/utils/br_number_format.dart'; +import 'package:real_calc/core/utils/share_text.dart'; + +import '../../domain/entities/financial_calculation.dart'; +import '../../domain/enum/financial_calculation_target.dart'; +import 'calculator_statement.dart'; +import 'deposits_statement.dart'; +import 'financing_statement.dart'; +import 'future_value_statement.dart'; + +typedef ShareTextBuilder = + String Function( + FinancialCalculation values, + FinancialCalculationTarget calculatedField, + ); + +List<(String, String)> _fieldLines( + FinancialCalculation values, + FinancialCalculationTarget calculatedField, + Map labels, +) => [ + for (final field in calculatorFieldOrder) + if (field != calculatedField) + ( + labels[field]!, + switch (field) { + FinancialCalculationTarget.initialValue => money(values.initialValue), + FinancialCalculationTarget.periods => months(values.periods), + FinancialCalculationTarget.interestRate => + '${formatPercent(values.rate)} ao mês', + FinancialCalculationTarget.finalValue => money(values.finalValue), + }, + ), +]; + +String financingShareText( + FinancialCalculation values, + FinancialCalculationTarget calculatedField, +) { + final total = toCents(values.finalValue) * values.periods; + final interest = total - toCents(values.initialValue); + + return shareText( + calculator: 'Financiamento', + statement: financingStatement( + emptyFields: const {}, + values: values, + calculatedField: calculatedField, + ), + lines: [ + ..._fieldLines(values, calculatedField, const { + FinancialCalculationTarget.initialValue: 'Valor financiado', + FinancialCalculationTarget.periods: 'Prazo', + FinancialCalculationTarget.interestRate: 'Taxa de juros', + FinancialCalculationTarget.finalValue: 'Prestação', + }), + ('Total pago', formatCents(total)), + ('Juros', formatCents(interest)), + ], + ); +} + +String depositsShareText( + FinancialCalculation values, + FinancialCalculationTarget calculatedField, +) { + final deposited = toCents(values.initialValue) * values.periods; + final yield_ = toCents(values.finalValue) - deposited; + + return shareText( + calculator: 'Depósitos Regulares', + statement: depositsStatement( + emptyFields: const {}, + values: values, + calculatedField: calculatedField, + ), + lines: [ + ..._fieldLines(values, calculatedField, const { + FinancialCalculationTarget.initialValue: 'Depósito mensal', + FinancialCalculationTarget.periods: 'Prazo', + FinancialCalculationTarget.interestRate: 'Taxa de juros', + FinancialCalculationTarget.finalValue: 'Valor final', + }), + ('Total depositado', formatCents(deposited)), + ('Rendimento', formatCents(yield_)), + ], + ); +} + +String futureValueShareText( + FinancialCalculation values, + FinancialCalculationTarget calculatedField, +) { + final yield_ = toCents(values.finalValue) - toCents(values.initialValue); + + return shareText( + calculator: 'Valor Futuro', + statement: futureValueStatement( + emptyFields: const {}, + values: values, + calculatedField: calculatedField, + ), + lines: [ + ..._fieldLines(values, calculatedField, const { + FinancialCalculationTarget.initialValue: 'Valor aplicado', + FinancialCalculationTarget.periods: 'Prazo', + FinancialCalculationTarget.interestRate: 'Taxa de juros', + FinancialCalculationTarget.finalValue: 'Valor futuro', + }), + ('Rendimento', formatCents(yield_)), + ], + ); +} diff --git a/lib/modules/financial_calculators/presentation/widgets/calculator_form_view.dart b/lib/modules/financial_calculators/presentation/widgets/calculator_form_view.dart index 109513a..7fd0c57 100644 --- a/lib/modules/financial_calculators/presentation/widgets/calculator_form_view.dart +++ b/lib/modules/financial_calculators/presentation/widgets/calculator_form_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:real_calc/core/services/result_sharer.dart'; import 'package:real_calc/core/themes/calculator_palette.dart'; import 'package:real_calc/core/themes/spacing.dart'; import 'package:real_calc/core/widgets/bottom_action_bar.dart'; @@ -16,6 +17,7 @@ import 'package:real_calc/core/widgets/result_details/result_summary.dart'; import '../../domain/entities/financial_calculation.dart'; import '../../domain/enum/financial_calculation_target.dart'; import '../cubit/forms/financing_form_cubit.dart'; +import '../utils/share_texts.dart'; class CalculatorFieldConfig { final FinancialCalculationTarget target; @@ -54,6 +56,8 @@ class CalculatorFormView extends StatelessWidget { final String Function(Set) missingMessage; final ResultSummary Function(FinancialCalculation) summary; final ValueChanged onCalculate; + final ShareTextBuilder shareText; + final ResultSharer sharer; final String? errorMessage; const CalculatorFormView({ @@ -65,6 +69,8 @@ class CalculatorFormView extends StatelessWidget { required this.missingMessage, required this.summary, required this.onCalculate, + required this.shareText, + required this.sharer, this.errorMessage, }); @@ -175,6 +181,12 @@ class CalculatorFormView extends StatelessWidget { BottomActionBar( primaryLabel: 'Calcular', onPrimary: formState.canCalculate ? calculate : null, + onShare: calculatedField == null + ? null + : (origin) => sharer.share( + shareText(values, calculatedField), + origin: origin, + ), ), ], ), diff --git a/lib/modules/value_correction/domain/services/value_correction_sharer.dart b/lib/modules/value_correction/domain/services/value_correction_sharer.dart deleted file mode 100644 index 36648f4..0000000 --- a/lib/modules/value_correction/domain/services/value_correction_sharer.dart +++ /dev/null @@ -1,3 +0,0 @@ -abstract interface class ValueCorrectionSharer { - Future share(String text); -} diff --git a/lib/modules/value_correction/infrastructure/services/share_plus_value_correction_sharer.dart b/lib/modules/value_correction/infrastructure/services/share_plus_value_correction_sharer.dart deleted file mode 100644 index df4e4ef..0000000 --- a/lib/modules/value_correction/infrastructure/services/share_plus_value_correction_sharer.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:share_plus/share_plus.dart'; - -import '../../domain/services/value_correction_sharer.dart'; - - -class SharePlusValueCorrectionSharer implements ValueCorrectionSharer { - @override - Future share(String text) async { - await SharePlus.instance.share(ShareParams(text: text)); - } -} diff --git a/lib/modules/value_correction/module.dart b/lib/modules/value_correction/module.dart index 242a851..110dfb8 100644 --- a/lib/modules/value_correction/module.dart +++ b/lib/modules/value_correction/module.dart @@ -1,6 +1,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:real_calc/core/module.dart'; +import 'package:real_calc/core/services/result_sharer.dart'; +import 'package:real_calc/core/services/share_plus_result_sharer.dart'; import 'package:real_calc/core/themes/calculator_palette.dart'; import 'package:real_calc/core/themes/calculator_scope.dart'; @@ -8,11 +10,9 @@ import '../../core/routes/app_routes.dart'; import 'domain/entites/value_correction.dart'; import 'domain/enum/correction_index.dart'; import 'domain/repositories/i_correction_series_repository.dart'; -import 'domain/services/value_correction_sharer.dart'; import 'domain/validation/i_value_correction_validation.dart'; import 'domain/validation/value_correction_validation.dart'; import 'infrastructure/repositories/sgs_repository.dart'; -import 'infrastructure/services/share_plus_value_correction_sharer.dart'; import 'presentation/cubit/value_correction/value_correction_cubit.dart'; import 'presentation/cubit/value_correction/request_parsing/i_value_correction_request_parser.dart'; import 'presentation/cubit/value_correction/request_parsing/value_correction_request_parser.dart'; @@ -71,9 +71,7 @@ class ValueCorrectionModule extends Module { i.addLazySingleton(CorrectionDataPreparer.new); i.addLazySingleton(SgsRepository.new); i.addLazySingleton(CalculateValueCorrection.new); - i.addLazySingleton( - SharePlusValueCorrectionSharer.new, - ); + i.addLazySingleton(SharePlusResultSharer.new); i.addLazySingleton(ValueCorrectionFormCubit.new); i.addLazySingleton(ValueCorrectionCubit.new); } diff --git a/lib/modules/value_correction/presentation/pages/value_correction_result_page.dart b/lib/modules/value_correction/presentation/pages/value_correction_result_page.dart index caa8112..8c21641 100644 --- a/lib/modules/value_correction/presentation/pages/value_correction_result_page.dart +++ b/lib/modules/value_correction/presentation/pages/value_correction_result_page.dart @@ -9,15 +9,12 @@ import 'package:real_calc/modules/value_correction/domain/entites/value_correcti import 'package:real_calc/modules/value_correction/domain/enum/correction_index.dart'; import 'package:real_calc/modules/value_correction/presentation/utils/value_correction_share_text.dart'; -import '../../domain/services/value_correction_sharer.dart'; +import 'package:real_calc/core/services/result_sharer.dart'; class ValueCorrectionResultPage extends StatelessWidget { final ValueCorrection result; - const ValueCorrectionResultPage({ - super.key, - required this.result, - }); + const ValueCorrectionResultPage({super.key, required this.result}); @override Widget build(BuildContext context) { @@ -99,9 +96,8 @@ class ValueCorrectionResultPage extends StatelessWidget { ), ), OptionsBottomForms( - onCalculate: () => Modular.get().share( - buildShareText(result), - ), + onCalculate: () => + Modular.get().share(buildShareText(result)), onClear: () => Modular.to.pop(), calculateLabel: 'Compartilhar', clearLabel: 'Editar Dados', diff --git a/lib/modules/value_correction/presentation/utils/value_correction_share_text.dart b/lib/modules/value_correction/presentation/utils/value_correction_share_text.dart index aa10e72..212ffee 100644 --- a/lib/modules/value_correction/presentation/utils/value_correction_share_text.dart +++ b/lib/modules/value_correction/presentation/utils/value_correction_share_text.dart @@ -1,4 +1,5 @@ import 'package:intl/intl.dart'; +import 'package:real_calc/core/utils/share_text.dart'; import '../../domain/entites/value_correction.dart'; import '../../domain/enum/correction_index.dart'; @@ -47,6 +48,6 @@ String buildShareText(ValueCorrection correction) { ); } - lines.addAll(['', 'Simulação feita no RealCalc, sem valor oficial.']); + lines.addAll(['', shareDisclaimer]); return lines.join('\n'); } diff --git a/test/core/widgets/bottom_action_bar_test.dart b/test/core/widgets/bottom_action_bar_test.dart index c3c9815..222993c 100644 --- a/test/core/widgets/bottom_action_bar_test.dart +++ b/test/core/widgets/bottom_action_bar_test.dart @@ -70,12 +70,16 @@ void main() { testWidgets('com resultado, compartilhar fica ativo', (tester) async { var shared = 0; + Rect? sharedFrom; await pump( tester, BottomActionBar( primaryLabel: 'Calcular', onPrimary: () {}, - onShare: () => shared++, + onShare: (origin) { + shared++; + sharedFrom = origin; + }, ), ); @@ -88,6 +92,7 @@ void main() { expect(opacity.opacity, 1); await tester.tap(find.bySemanticsLabel(BottomActionBar.shareLabel)); expect(shared, 1); + expect(sharedFrom, tester.getRect(find.byType(OutlinedButton))); }); testWidgets('botões de 56px, principal em pílula na cor da calculadora ' diff --git a/test/modules/financial_calculators/presentation/pages/financing_page_test.dart b/test/modules/financial_calculators/presentation/pages/financing_page_test.dart index 5646d4b..485813b 100644 --- a/test/modules/financial_calculators/presentation/pages/financing_page_test.dart +++ b/test/modules/financial_calculators/presentation/pages/financing_page_test.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:real_calc/core/services/result_sharer.dart'; import 'package:real_calc/core/themes/app_theme.dart'; import 'package:real_calc/core/widgets/bottom_action_bar.dart'; import 'package:real_calc/core/widgets/calculator_band/calculator_band.dart'; @@ -13,11 +14,21 @@ import 'package:real_calc/modules/financial_calculators/presentation/cubit/forms import 'package:real_calc/modules/financial_calculators/presentation/pages/financing_page.dart'; import 'package:real_calc/modules/financial_calculators/use_case/financing/calculate_financing.dart'; +class _FakeSharer implements ResultSharer { + final shared = <(String, Rect?)>[]; + + @override + Future share(String text, {Rect? origin}) async => + shared.add((text, origin)); +} + void main() { + late _FakeSharer sharer; late FinancingCubit financingCubit; late FinancingFormCubit formCubit; setUp(() { + sharer = _FakeSharer(); financingCubit = FinancingCubit(CalculateFinancing()); formCubit = FinancingFormCubit(); }); @@ -49,7 +60,7 @@ void main() { BlocProvider.value(value: financingCubit), BlocProvider.value(value: formCubit), ], - child: const FinancingPage(), + child: FinancingPage(sharer: sharer), ), ), ); @@ -209,6 +220,25 @@ void main() { expect(find.byType(ResultDetails), findsOneWidget); }); + testWidgets('compartilhar fica desabilitado sem resultado e envia o ' + 'texto ancorado no botão depois de calcular', (tester) async { + await pump(tester); + await fillExample(tester); + + final share = find.byType(OutlinedButton); + expect(tester.widget(share).onPressed, isNull); + + await tester.tap(find.text('Calcular')); + await tester.pumpAndSettle(); + await tester.tap(share); + + expect(sharer.shared, hasLength(1)); + final (text, origin) = sharer.shared.single; + expect(text, startsWith('RealCalc · Financiamento\n')); + expect(text, contains('Total pago: R\$ 2.836,80')); + expect(origin, tester.getRect(share)); + }); + testWidgets('"?" abre a ajuda', (tester) async { await pump(tester); diff --git a/test/modules/financial_calculators/presentation/utils/share_texts_test.dart b/test/modules/financial_calculators/presentation/utils/share_texts_test.dart new file mode 100644 index 0000000..33caba4 --- /dev/null +++ b/test/modules/financial_calculators/presentation/utils/share_texts_test.dart @@ -0,0 +1,103 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:real_calc/modules/financial_calculators/domain/entities/financial_calculation.dart'; +import 'package:real_calc/modules/financial_calculators/domain/enum/financial_calculation_target.dart'; +import 'package:real_calc/modules/financial_calculators/presentation/utils/share_texts.dart'; + +void main() { + group('financingShareText', () { + final values = FinancialCalculation( + initialValue: 2500, + periods: 12, + rate: 2, + finalValue: 236.3990, + ); + + test('prestação calculada, igual ao exemplo da issue', () { + expect( + financingShareText(values, FinancialCalculationTarget.finalValue), + 'RealCalc · Financiamento\n' + 'Sua prestação vai ser R\$ 236,40\n' + '12 parcelas iguais, pela Tabela Price\n' + '\n' + 'Valor financiado: R\$ 2.500,00\n' + 'Prazo: 12 meses\n' + 'Taxa de juros: 2,00% ao mês\n' + 'Total pago: R\$ 2.836,80\n' + 'Juros: R\$ 336,80\n' + '\n' + 'Simulação feita no RealCalc, sem valor oficial.', + ); + }); + + test('taxa calculada lista a prestação no lugar da taxa', () { + final text = financingShareText( + values, + FinancialCalculationTarget.interestRate, + ); + + expect(text, contains('A taxa de juros é de 2,00% ao mês\n')); + expect(text, contains('26,82% ao ano\n')); + expect(text, contains('Prestação: R\$ 236,40\n')); + expect(text, isNot(contains('Taxa de juros:'))); + }); + + test('não tem espaço não separável', () { + final text = financingShareText( + values, + FinancialCalculationTarget.initialValue, + ); + expect(text.contains(String.fromCharCode(0xA0)), isFalse); + expect(text, contains('Você pode financiar R\$ 2.500,00\n')); + expect(text, contains('em 12 parcelas de R\$ 236,40\n')); + }); + }); + + test('depositsShareText', () { + expect( + depositsShareText( + FinancialCalculation( + initialValue: 200, + periods: 24, + rate: 0.8, + finalValue: 5310.7801, + ), + FinancialCalculationTarget.finalValue, + ), + 'RealCalc · Depósitos Regulares\n' + 'Guardando R\$ 200,00 por mês, você terá R\$ 5.310,78\n' + 'em 24 meses, com R\$ 510,78 de rendimento\n' + '\n' + 'Depósito mensal: R\$ 200,00\n' + 'Prazo: 24 meses\n' + 'Taxa de juros: 0,80% ao mês\n' + 'Total depositado: R\$ 4.800,00\n' + 'Rendimento: R\$ 510,78\n' + '\n' + 'Simulação feita no RealCalc, sem valor oficial.', + ); + }); + + test('futureValueShareText', () { + expect( + futureValueShareText( + FinancialCalculation( + initialValue: 2555.55, + periods: 25, + rate: 0.33, + finalValue: 2774.947, + ), + FinancialCalculationTarget.finalValue, + ), + 'RealCalc · Valor Futuro\n' + 'Aplicando R\$ 2.555,55 hoje, você terá R\$ 2.774,95\n' + 'em 25 meses, com R\$ 219,40 de rendimento\n' + '\n' + 'Valor aplicado: R\$ 2.555,55\n' + 'Prazo: 25 meses\n' + 'Taxa de juros: 0,33% ao mês\n' + 'Rendimento: R\$ 219,40\n' + '\n' + 'Simulação feita no RealCalc, sem valor oficial.', + ); + }); +} From a9461dabd45f62f4d95a0507393d811468816c3e Mon Sep 17 00:00:00 2001 From: Rodrigo Mamede <61851910+Rodrigosdk@users.noreply.github.com> Date: Wed, 28 Oct 2026 11:58:46 -0300 Subject: [PATCH 14/22] =?UTF-8?q?chore:=20logo=20com=20ajuste=20fino:=20as?= =?UTF-8?q?sets,=20=C3=ADcone=20do=20app=20e=20AppLogo=20(#128)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: regerar o ícone do app com flutter_launcher_icons, adaptativo e monocromático (#128) * chore: tela de abertura com fundo #14121F e o símbolo novo no Android e no iOS (#128) * feat: atualizar o símbolo com ajuste fino, adicionar a versão pequena e o AppLogo (#128) --- .../drawable-hdpi/ic_launcher_foreground.png | Bin 0 -> 2808 bytes .../drawable-hdpi/ic_launcher_monochrome.png | Bin 0 -> 2724 bytes .../drawable-mdpi/ic_launcher_foreground.png | Bin 0 -> 1765 bytes .../drawable-mdpi/ic_launcher_monochrome.png | Bin 0 -> 1734 bytes .../res/drawable-v21/launch_background.xml | 8 +- .../drawable-xhdpi/ic_launcher_foreground.png | Bin 0 -> 3533 bytes .../drawable-xhdpi/ic_launcher_monochrome.png | Bin 0 -> 3307 bytes .../ic_launcher_foreground.png | Bin 0 -> 5315 bytes .../ic_launcher_monochrome.png | Bin 0 -> 4735 bytes .../ic_launcher_foreground.png | Bin 0 -> 7279 bytes .../ic_launcher_monochrome.png | Bin 0 -> 6382 bytes .../main/res/drawable/launch_background.xml | 8 +- .../src/main/res/drawable/launch_image.png | Bin 32256 -> 0 bytes .../res/mipmap-anydpi-v26/ic_launcher.xml | 14 ++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3401 -> 1349 bytes .../src/main/res/mipmap-hdpi/launch_image.png | Bin 0 -> 3332 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2364 -> 775 bytes .../src/main/res/mipmap-mdpi/launch_image.png | Bin 0 -> 2239 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4768 -> 1683 bytes .../main/res/mipmap-xhdpi/launch_image.png | Bin 0 -> 4277 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 6808 -> 2631 bytes .../main/res/mipmap-xxhdpi/launch_image.png | Bin 0 -> 6865 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 10066 -> 3340 bytes .../main/res/mipmap-xxxhdpi/launch_image.png | Bin 0 -> 9299 bytes android/app/src/main/res/play_store_512.png | Bin 32256 -> 9282 bytes .../src/main/res/values-night-v31/styles.xml | 11 ++ .../app/src/main/res/values-v31/styles.xml | 11 ++ android/app/src/main/res/values/colors.xml | 5 + assets/branding/app_icon.svg | 18 +-- assets/branding/app_icon_pequeno.svg | 10 ++ assets/launcher/adaptive_foreground_1024.png | Bin 0 -> 15997 bytes assets/launcher/adaptive_monochrome_1024.png | Bin 0 -> 13597 bytes assets/launcher/icon_1024.png | Bin 0 -> 17567 bytes .../AppIcon.appiconset/Contents.json | 123 +----------------- .../Icon-App-1024x1024@1x.png | Bin 10932 -> 20202 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 295 -> 316 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 406 -> 629 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 450 -> 1038 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 282 -> 424 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 462 -> 973 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 704 -> 1523 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 406 -> 629 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 586 -> 1326 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 862 -> 2094 bytes .../AppIcon.appiconset/Icon-App-50x50@1x.png | Bin 0 -> 834 bytes .../AppIcon.appiconset/Icon-App-50x50@2x.png | Bin 0 -> 1737 bytes .../AppIcon.appiconset/Icon-App-57x57@1x.png | Bin 0 -> 957 bytes .../AppIcon.appiconset/Icon-App-57x57@2x.png | Bin 0 -> 1998 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 862 -> 2094 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 1674 -> 3143 bytes .../AppIcon.appiconset/Icon-App-72x72@1x.png | Bin 0 -> 1349 bytes .../AppIcon.appiconset/Icon-App-72x72@2x.png | Bin 0 -> 2631 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 762 -> 1372 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 1226 -> 2934 bytes .../Icon-App-83.5x83.5@2x.png | Bin 1418 -> 3063 bytes .../LaunchImage.imageset/LaunchImage.png | Bin 68 -> 2239 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 68 -> 4277 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 68 -> 6865 bytes ios/Runner/Base.lproj/LaunchScreen.storyboard | 2 +- lib/core/themes/app_image.dart | 5 +- lib/core/widgets/app_logo.dart | 56 ++++++++ pubspec.lock | 56 ++++++++ pubspec.yaml | 14 +- test/core/widgets/app_logo_test.dart | 81 ++++++++++++ 64 files changed, 276 insertions(+), 146 deletions(-) create mode 100644 android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png create mode 100644 android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png create mode 100644 android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png create mode 100644 android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png create mode 100644 android/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png delete mode 100644 android/app/src/main/res/drawable/launch_image.png create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 android/app/src/main/res/mipmap-hdpi/launch_image.png create mode 100644 android/app/src/main/res/mipmap-mdpi/launch_image.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/launch_image.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/launch_image.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/launch_image.png create mode 100644 android/app/src/main/res/values-night-v31/styles.xml create mode 100644 android/app/src/main/res/values-v31/styles.xml create mode 100644 android/app/src/main/res/values/colors.xml create mode 100644 assets/branding/app_icon_pequeno.svg create mode 100644 assets/launcher/adaptive_foreground_1024.png create mode 100644 assets/launcher/adaptive_monochrome_1024.png create mode 100644 assets/launcher/icon_1024.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png create mode 100644 lib/core/widgets/app_logo.dart create mode 100644 test/core/widgets/app_logo_test.dart diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..4fcc831b2aaa9cf36f7d71afd842d3d65f0100e6 GIT binary patch literal 2808 zcmdUx`9BkmAIE*lmFqM5M6vo*t{m}k$CzS5u21FO9GNTEnmJ~gD}2OEl88`p6d}w# zd_q>(+_|TbP3|K$!+iDqFFrrKU+*8@kM}RH$LsNWJRkpUZ7M7vDZs_WC2Vd6xpTY= ze+@tHv36{UOL1`tw3|b2*rH#qPM>o8gK?g;8NPcW>_*VXW&uUTZxhaVNsm#@Ni?|Y zY-$c~q1*)->SJ?*JM)?2WoOf3?DXeur$IK+W!-)_Nu6X~n-RNr{EY%hK*8QM_0^}9 zLfl?1&$rxmnGqKXe6V>DKHZG{6~SI1G2ZJjMXznRVlF{mow#)36u+q1^`z@3jJU-B zm!HG{V!7k_WiCQ4{`TKF9Z6>-jmNx=0UtK=b%g}1o~eA0&RR-c;Bi10HltNmKgM1? zB35(mwN;KhC%x2sKirkgS@rQ|_zg?Q6cZzt3h8B684dFM>1tnJXZqyw%u8!?E6lt> zccbxY6it=Pq?zXW?b~62jN$ds{EAYF&8>4-$Ix{U6sS=Wy+1}05^?!>cHWuYq#%Jy z8J|WO1?xcj0@HxtE^Try!Hl)YoQ-JI)6zMd0p8mgbG3RorJVP(qhUMa&dnghLh9`- zcv#T#pdb6Q%JMn*I4!j@fNgs7I5nkqg z1rD1ld)LA{9n`xO;^(LEZyg*p+~Bl=nuw5`>m+8%A#KzO_6 z;5iG_fkZn*Z~Jf8M5=4;n^{d!v5whUFW$5cO*;&diP$GYE)kBQ{d4nI@q%t^idnq& zy3~XfJE+LJjKJUA{Q6c)#ixP|V_BQ*(m?3Uc({29*J?Wl&n z*lSSMwKjWYnzC7yAeY_NtCp5|4rg*VtL&zD)}nA|`_t}^_l)~`M#bN02wER!F7icI zj}DugY~8OyY+e{3SCOYoLVPEYn_Zt>aOFbpAU=)J^qxo0Ch;`6EWL_s$vO+UGm)p- zHQWkbY8%+)`QQGG@3?2I>D0iSj7vBzjj?ZoB5Ts9?48Q--&XsJseP@~q1jPYnSojz zc;^~jP?_ayzu-F9q({O}K&<%fgPV#cZ!i|%KHb;)QAUIZ}n*v!F2*NTh| zkE-5w-|n*7&rLC&ti*QMdHHFK!*)O2g-K?{T#pyhI{L{+IjM=TnHqdEp1AZHFhK!M#2C_qPcazB^+~RI(}shLzJ(D*e7$OBeE$W3odmzpLaU)weMe zjgs6ZcHe-I6_Rp9t69N!{=;fCtNOEcqbo_F6O?y>6FjA>{%)LffXFoTrwfxPUKM+u zs}tuU67*(j=^4o;0p_p~xp1A*i!>JX^isdbVK%avs?tT#eVDYuNDF55x8FMS2n?Q_ z