diff --git a/.github/workflows/flutter_test.yml b/.github/workflows/flutter_test.yml index 02665af..fa07d11 100644 --- a/.github/workflows/flutter_test.yml +++ b/.github/workflows/flutter_test.yml @@ -10,9 +10,30 @@ jobs: integration-test: name: integration-test runs-on: ubuntu-latest + env: + API_LEVEL: 30 + TARGET: default + ARCH: x86_64 steps: + - name: Free disk space + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + android: false + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: true + - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v6 + + - name: Run Mockoon CLI + uses: mockoon/cli-action@v3 + with: + data-file: 'https://raw.githubusercontent.com/conekta/openapi/main/mocks/conekta_api.json' + port: 3000 - name: Set up Flutter uses: subosito/flutter-action@v2 @@ -20,40 +41,87 @@ jobs: flutter-version: '3.x' channel: 'stable' cache: true + + - name: Cache Gradle and pub + uses: actions/cache@v5 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + ~/.pub-cache + android/.gradle + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'pubspec.lock') }} + restore-keys: | + gradle-${{ runner.os }}- + - name: Get Flutter dependencies run: flutter pub get - - name: Accept Android licenses - run: yes | flutter doctor --android-licenses - - name: Enable KVM run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - name: Set up Android SDK + + - name: AVD cache + uses: actions/cache@v5 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-v2-${{ env.API_LEVEL }}-${{ env.TARGET }}-${{ env.ARCH }} + + - name: Create AVD snapshot + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ env.API_LEVEL }} + target: ${{ env.TARGET }} + arch: ${{ env.ARCH }} + cores: 4 + ram-size: 4096M + heap-size: 1024M + disable-animations: false + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + script: echo "Generated AVD snapshot for caching." + + - name: Run integration tests on Android emulator uses: reactivecircus/android-emulator-runner@v2 with: - api-level: 31 + api-level: ${{ env.API_LEVEL }} + target: ${{ env.TARGET }} + arch: ${{ env.ARCH }} + cores: 4 + ram-size: 4096M heap-size: 1024M disable-animations: true - cores: 8 - ram-size: 12288M - target: google_apis - arch: x86_64 + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none script: flutter test integration_test/ test: - name: Run Flutter Tests - runs-on: ubuntu-latest + name: Run Flutter Tests (Flutter ${{ matrix.flutter-version }}) + runs-on: aws-runner-set + strategy: + fail-fast: false + matrix: + flutter-version: ['3.19.x', '3.24.x', '3.29.x', '3.32.x', '3.x'] steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v6 + + - name: Install xz-utils + run: | + if ! command -v xz >/dev/null 2>&1; then + sudo apt-get update && sudo apt-get install -y xz-utils + fi - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.x' + flutter-version: ${{ matrix.flutter-version }} channel: 'stable' cache: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 138dd5e..be22910 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,13 @@ # Changelog + +## [1.0.1] - 2026-04-17 +### Added +- `CardFormConfig` class to group optional UI flags passed to `CardForm` via the `config` parameter. +- `CardFormConfig.hideLogo` (default `false`) to show/hide the "Paga segura con Conekta" section. +- `CardFormConfig.submitButtonText` (default `null`) to override the submit button label; falls back to the localized value when `null`. +- Widget tests for `CardForm` config flags and `CardFormConfig` defaults. +- Example usages in `example/lib/main.dart`. + ## [1.0.0] - 2025-09-30 - Final release diff --git a/README.md b/README.md index f20b9cb..d420ddf 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A customizable Flutter widget for securely collecting credit card information. This library provides a user-friendly form with built-in validation and localization support, making it easy to integrate into your Flutter applications for payment processing. -**Current Version:** 1.0.0 +**Current Version:** 1.0.1 ## Features @@ -16,7 +16,7 @@ A customizable Flutter widget for securely collecting credit card information. T ## Minimum Requirements -* **Dart SDK:** Version 2.17.0 or higher +* **Dart SDK:** Version 3.3.0 or higher (Flutter 3.19.0+) ## Installation diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 399f698..bd3592b 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -4,4 +4,5 @@ to allow setting breakpoints, to provide hot reload, etc. --> + diff --git a/example/lib/main.dart b/example/lib/main.dart index fd5f9f9..be584f1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -55,8 +55,34 @@ class CreditCardFormScreen extends StatelessWidget { onSubmitted: _onSubmitted, paymentService: paymentService, locale: const Locale('es'), + config: const CardFormConfig( + hideLogo: false, + submitButtonText: 'Pagar ahora', + ), ), ), ); } } + +/// Example: hide the "Paga segura con Conekta" badge and keep the localized +/// submit label ("Continuar"/"Continue") by leaving [submitButtonText] as null. +/// +/// ```dart +/// CardForm( +/// paymentService: paymentService, +/// onSubmitted: _onSubmitted, +/// config: const CardFormConfig(hideLogo: true), +/// ) +/// ``` +/// +/// Example: English locale with a custom submit label. +/// +/// ```dart +/// CardForm( +/// paymentService: paymentService, +/// locale: const Locale('en'), +/// onSubmitted: _onSubmitted, +/// config: const CardFormConfig(submitButtonText: 'Pay now'), +/// ) +/// ``` diff --git a/integration_test/card_form_test.dart b/integration_test/card_form_test.dart index 1b71747..422bb60 100644 --- a/integration_test/card_form_test.dart +++ b/integration_test/card_form_test.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:conekta_component/card_input_flutter.dart'; import 'package:conekta_component/l10n/app_localizations.dart'; import 'package:conekta_component/src/fields/card_cvv_field.dart'; @@ -9,17 +11,20 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'mocks/mock_payment_service.dart'; - void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('CardForm submits and returns success', (tester) async { + // Android emulator reaches the host machine via 10.0.2.2. + final mockoonHost = + Platform.isAndroid ? 'http://10.0.2.2:3000' : 'http://localhost:3000'; + + testWidgets('CardForm submits against Mockoon and returns success', + (tester) async { bool submitted = false; await tester.pumpWidget( MaterialApp( - localizationsDelegates: [ + localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, @@ -27,7 +32,8 @@ void main() { ], home: Scaffold( body: CardForm( - paymentService: MockPaymentService(apiKey: "key_xxx"), + paymentService: + PaymentService(apiKey: 'key_xxx', host: mockoonHost), onSubmitted: (result) { submitted = result is Success>; }, @@ -38,11 +44,11 @@ void main() { await tester.enterText(find.byType(CardNameField), 'Juan Pérez'); await tester.enterText(find.byType(CardNumberField), '4242424242424242'); - await tester.enterText(find.byType(CardExpiryFields), '12/25'); + await tester.enterText(find.byType(CardExpiryFields), '12/30'); await tester.enterText(find.byType(CardCVVField), '123'); await tester.tap(find.byType(ElevatedButton)); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(const Duration(seconds: 30)); expect(submitted, isTrue); }); diff --git a/integration_test/mocks/mock_payment_service.dart b/integration_test/mocks/mock_payment_service.dart deleted file mode 100644 index cadd425..0000000 --- a/integration_test/mocks/mock_payment_service.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:conekta_component/src/models/card_model.dart'; -import 'package:conekta_component/src/services/payment_service.dart'; - -class MockPaymentService extends PaymentService { - MockPaymentService({required super.apiKey}); - - @override - Future> sendPayment( - CardModel card, String locale) async { - return { - 'id': 'tok_12345', - 'status': 'success', - }; - } -} diff --git a/l10n.yaml b/l10n.yaml index 15338f2..af4d3c1 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -1,3 +1,4 @@ arb-dir: lib/l10n template-arb-file: app_en.arb output-localization-file: app_localizations.dart +output-dir: lib/l10n diff --git a/lib/card_input_flutter.dart b/lib/card_input_flutter.dart index 7f5bdc0..4a007ec 100644 --- a/lib/card_input_flutter.dart +++ b/lib/card_input_flutter.dart @@ -1,6 +1,5 @@ -library credit_card_input_flutter; - export 'src/card_form.dart'; export 'src/services/payment_service.dart'; export 'src/services/result.dart'; export 'src/models/card_model.dart'; +export 'src/models/card_form_config.dart'; diff --git a/lib/src/card_form.dart b/lib/src/card_form.dart index 667fe98..cc0331c 100644 --- a/lib/src/card_form.dart +++ b/lib/src/card_form.dart @@ -7,6 +7,7 @@ import 'fields/card_cvv_field.dart'; import 'fields/card_expiry_fields.dart'; import 'fields/card_name_field.dart'; import 'fields/card_number_field.dart'; +import 'models/card_form_config.dart'; import 'models/card_model.dart'; import 'services/payment_service.dart'; import 'utils/dark_theme.dart'; @@ -19,12 +20,14 @@ class CardForm extends StatefulWidget { final PaymentService paymentService; final Locale locale; final ThemeData? themeData; + final CardFormConfig config; const CardForm( {super.key, this.onSubmitted, required this.paymentService, this.locale = const Locale('es'), - this.themeData}); + this.themeData, + this.config = const CardFormConfig()}); @override State createState() => _CardFormState(); @@ -37,6 +40,7 @@ class _CardFormState extends State { final cardNumberController = TextEditingController(); final nameController = TextEditingController(); final cvvController = TextEditingController(); + final nameFocusNode = FocusNode(); bool _isLoading = false; @override void dispose() { @@ -44,6 +48,7 @@ class _CardFormState extends State { nameController.dispose(); expiryDateController.dispose(); cvvController.dispose(); + nameFocusNode.dispose(); super.dispose(); } @@ -68,6 +73,7 @@ class _CardFormState extends State { final result = await widget.paymentService .sendPayment(card, widget.locale.languageCode); cleanFields(); + nameFocusNode.requestFocus(); widget.onSubmitted?.call(Success(result)); } on Exception catch (e) { widget.onSubmitted?.call(Failure(e)); @@ -79,7 +85,7 @@ class _CardFormState extends State { } } - cleanFields() { + void cleanFields() { cardNumberController.clear(); nameController.clear(); expiryDateController.clear(); @@ -115,7 +121,8 @@ class _CardFormState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SecurePaymentSection(), + if (!widget.config.hideLogo) + const SecurePaymentSection(), Text( AppLocalizations.of(localizedContext)! .cardNameLabel, @@ -124,6 +131,7 @@ class _CardFormState extends State { const SizedBox(height: 8), CardNameField( controller: nameController, + focusNode: nameFocusNode, decoration: InputDecoration( hintText: AppLocalizations.of(localizedContext)! .cardNameHint), @@ -258,8 +266,9 @@ class _CardFormState extends State { .colorScheme .onPrimary)) : Text( - AppLocalizations.of(localizedContext)! - .submitButton, + widget.config.submitButtonText ?? + AppLocalizations.of(localizedContext)! + .submitButton, style: TextStyle( fontSize: 16, color: Theme.of(themedContext) diff --git a/lib/src/fields/card_cvv_field.dart b/lib/src/fields/card_cvv_field.dart index 599e099..2a8522d 100644 --- a/lib/src/fields/card_cvv_field.dart +++ b/lib/src/fields/card_cvv_field.dart @@ -25,7 +25,7 @@ class CardCVVField extends StatelessWidget { return TextFormField( controller: controller, decoration: decoration ?? - InputDecoration( + const InputDecoration( labelText: 'CVV', isDense: true, ).applyDefaults(Theme.of(context).inputDecorationTheme), diff --git a/lib/src/fields/card_name_field.dart b/lib/src/fields/card_name_field.dart index 00df075..376981b 100644 --- a/lib/src/fields/card_name_field.dart +++ b/lib/src/fields/card_name_field.dart @@ -4,8 +4,10 @@ import 'package:flutter/material.dart'; class CardNameField extends StatelessWidget { final TextEditingController controller; final InputDecoration? decoration; + final FocusNode? focusNode; - const CardNameField({required this.controller, this.decoration, super.key}); + const CardNameField( + {required this.controller, this.decoration, this.focusNode, super.key}); String? _validateCardName(BuildContext context, String? value) { final name = value?.replaceAll(' ', ''); if (name == null || name.isEmpty) { @@ -20,6 +22,7 @@ class CardNameField extends StatelessWidget { return TextFormField( controller: controller, decoration: decoration, + focusNode: focusNode, validator: (value) => _validateCardName(context, value), ); } diff --git a/lib/src/models/card_form_config.dart b/lib/src/models/card_form_config.dart new file mode 100644 index 0000000..678d0f1 --- /dev/null +++ b/lib/src/models/card_form_config.dart @@ -0,0 +1,15 @@ +/// Configuration options for customizing [CardForm]'s appearance and copy. +class CardFormConfig { + /// When `true`, hides the "Secure payment with Conekta" logo/badge shown + /// below the form. Defaults to `false` (badge visible). + final bool hideLogo; + + /// Overrides the text displayed on the submit button. When `null`, the + /// button falls back to the localized default ("Pagar" / "Pay"). + final String? submitButtonText; + + const CardFormConfig({ + this.hideLogo = false, + this.submitButtonText, + }); +} diff --git a/lib/src/utils/dark_theme.dart b/lib/src/utils/dark_theme.dart index 3f2fc20..8ee0c48 100644 --- a/lib/src/utils/dark_theme.dart +++ b/lib/src/utils/dark_theme.dart @@ -16,7 +16,7 @@ final ThemeData darkTheme = ThemeData( fontSize: 14, fontWeight: FontWeight.w400, ), - titleSmall: TextStyle( + titleSmall: const TextStyle( color: DarkAppColors.label, fontSize: 14, fontWeight: FontWeight.w400, @@ -27,15 +27,15 @@ final ThemeData darkTheme = ThemeData( contentPadding: const EdgeInsets.only(left: 14, right: 14, top: 0, bottom: 0), enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: DarkAppColors.disabled), + borderSide: const BorderSide(color: DarkAppColors.disabled), borderRadius: BorderRadius.circular(6), ), focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: DarkAppColors.disabled), + borderSide: const BorderSide(color: DarkAppColors.disabled), borderRadius: BorderRadius.circular(6), ), border: OutlineInputBorder( - borderSide: BorderSide(color: DarkAppColors.disabled), + borderSide: const BorderSide(color: DarkAppColors.disabled), borderRadius: BorderRadius.circular(6), ), ), diff --git a/lib/src/utils/theme.dart b/lib/src/utils/theme.dart index 5241590..39180ce 100644 --- a/lib/src/utils/theme.dart +++ b/lib/src/utils/theme.dart @@ -17,7 +17,7 @@ final ThemeData cardInputTheme = ThemeData( fontSize: 14, fontWeight: FontWeight.w400, ), - titleSmall: TextStyle( + titleSmall: const TextStyle( color: AppColors.label, fontSize: 14, fontWeight: FontWeight.w400, @@ -28,15 +28,15 @@ final ThemeData cardInputTheme = ThemeData( contentPadding: const EdgeInsets.only(left: 14, right: 14, top: 0, bottom: 0), enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: AppColors.disabled), + borderSide: const BorderSide(color: AppColors.disabled), borderRadius: BorderRadius.circular(6), ), focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: AppColors.disabled), + borderSide: const BorderSide(color: AppColors.disabled), borderRadius: BorderRadius.circular(6), ), border: OutlineInputBorder( - borderSide: BorderSide(color: AppColors.disabled), + borderSide: const BorderSide(color: AppColors.disabled), borderRadius: BorderRadius.circular(6), ), ), diff --git a/lib/src/version.dart b/lib/src/version.dart index e65f208..7acc9d2 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -1 +1 @@ -const sdkVersion = '1.0.0'; +const sdkVersion = '1.0.1'; diff --git a/pubspec.yaml b/pubspec.yaml index 0cb903a..0dd0613 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,25 +1,25 @@ name: conekta_component description: A Flutter library for securely collecting and validating card payment information using Conekta. -version: 1.0.0 +version: 1.0.1 homepage: https://www.github.com/conekta/component-flutter repository: https://www.github.com/conekta/component-flutter environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=3.3.0 <4.0.0" dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter - intl: ^0.20.2 - http: ^0.13.6 + intl: ">=0.18.0 <0.21.0" + http: ^1.0.0 flutter_svg: ^2.0.0 - google_fonts: ^4.0.0 + google_fonts: ">=4.0.0 <7.0.0" encrypt: ^5.0.0 pointycastle: ^3.7.3 dev_dependencies: - flutter_lints: ^5.0.0 + flutter_lints: ">=2.0.0 <6.0.0" integration_test: sdk: flutter flutter_test: diff --git a/test/helpers/fake_http_client.dart b/test/helpers/fake_http_client.dart new file mode 100644 index 0000000..21119f6 --- /dev/null +++ b/test/helpers/fake_http_client.dart @@ -0,0 +1,122 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +/// Intercepts `dart:io` HttpClient calls and returns a minimal valid SVG. +/// +/// CardForm renders several `SvgPicture.network(...)` widgets (card brand +/// and CVV icons). In `flutter_test` those network calls fail with status +/// 400 by default and throw `HttpException`, which fails tests even though +/// the failure is unrelated to the code under test. +/// +/// Install via `HttpOverrides.runZoned` with `createHttpClient`: +/// ```dart +/// await HttpOverrides.runZoned>( +/// () async { /* test body */ }, +/// createHttpClient: (_) => FakeHttpClient(), +/// ); +/// ``` +final Uint8List _fakeSvgBytes = Uint8List.fromList( + utf8.encode(''), +); + +class FakeHttpClient implements HttpClient { + @override + bool autoUncompress = true; + @override + Duration? connectionTimeout; + @override + Duration idleTimeout = const Duration(seconds: 15); + @override + int? maxConnectionsPerHost; + @override + String? userAgent; + + @override + Future getUrl(Uri url) async => _FakeHttpClientRequest(); + + @override + Future openUrl(String method, Uri url) async => + _FakeHttpClientRequest(); + + @override + void close({bool force = false}) {} + + @override + dynamic noSuchMethod(Invocation invocation) => null; +} + +class _FakeHttpClientRequest implements HttpClientRequest { + @override + final HttpHeaders headers = _FakeHttpHeaders(); + + @override + Future close() async => _FakeHttpClientResponse(); + + @override + Future get done async => _FakeHttpClientResponse(); + + @override + Future addStream(Stream> stream) => stream.drain(); + + @override + Future flush() async {} + + @override + dynamic noSuchMethod(Invocation invocation) => null; +} + +class _FakeHttpHeaders implements HttpHeaders { + @override + dynamic noSuchMethod(Invocation invocation) => null; +} + +class _FakeHttpClientResponse extends Stream> + implements HttpClientResponse { + @override + int statusCode = 200; + + @override + String reasonPhrase = 'OK'; + + @override + int get contentLength => _fakeSvgBytes.length; + + @override + bool get isRedirect => false; + + @override + bool get persistentConnection => false; + + @override + List get redirects => const []; + + @override + List get cookies => const []; + + @override + HttpClientResponseCompressionState get compressionState => + HttpClientResponseCompressionState.notCompressed; + + @override + HttpHeaders get headers => _FakeHttpHeaders(); + + @override + StreamSubscription> listen( + void Function(List event)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) { + return Stream>.fromIterable([_fakeSvgBytes]).listen( + onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } + + @override + dynamic noSuchMethod(Invocation invocation) => null; +} diff --git a/test/widget/card_form_test.dart b/test/widget/card_form_test.dart new file mode 100644 index 0000000..4172fbe --- /dev/null +++ b/test/widget/card_form_test.dart @@ -0,0 +1,114 @@ +import 'dart:io'; + +import 'package:conekta_component/card_input_flutter.dart'; +import 'package:conekta_component/l10n/app_localizations.dart'; +import 'package:conekta_component/src/widgets/secure_payment_section.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../helpers/fake_http_client.dart'; + +Widget _wrap(Widget child) { + return MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('es'), Locale('en')], + home: Scaffold(body: child), + ); +} + +Future _pumpAndDrainSvgErrors(WidgetTester tester, Widget widget) async { + await HttpOverrides.runZoned>( + () async { + await tester.pumpWidget(widget); + await tester.pump(); + }, + createHttpClient: (SecurityContext? c) => FakeHttpClient(), + ); + while (tester.takeException() != null) {} +} + +void main() { + final paymentService = PaymentService(apiKey: 'key_test'); + + group('CardForm config', () { + testWidgets('hides SecurePaymentSection when hideLogo is true', + (WidgetTester tester) async { + await _pumpAndDrainSvgErrors(tester, _wrap( + CardForm( + paymentService: paymentService, + config: const CardFormConfig(hideLogo: true), + ), + )); + + expect(find.byType(SecurePaymentSection), findsNothing); + }); + + testWidgets('submit button uses localized text by default (es)', + (WidgetTester tester) async { + await _pumpAndDrainSvgErrors(tester, _wrap( + CardForm(paymentService: paymentService), + )); + + expect(find.widgetWithText(ElevatedButton, 'Continuar'), findsOneWidget); + }); + + testWidgets('submit button uses localized text when locale is en', + (WidgetTester tester) async { + await _pumpAndDrainSvgErrors(tester, _wrap( + CardForm( + paymentService: paymentService, + locale: const Locale('en'), + ), + )); + + expect(find.widgetWithText(ElevatedButton, 'Continue'), findsOneWidget); + }); + + testWidgets('submit button text is overridden by submitButtonText', + (WidgetTester tester) async { + await _pumpAndDrainSvgErrors(tester, _wrap( + CardForm( + paymentService: paymentService, + config: const CardFormConfig(submitButtonText: 'Pagar ahora'), + ), + )); + + expect(find.widgetWithText(ElevatedButton, 'Pagar ahora'), findsOneWidget); + expect(find.widgetWithText(ElevatedButton, 'Continuar'), findsNothing); + }); + + testWidgets('config flags combine: hidden badge + custom text', + (WidgetTester tester) async { + await _pumpAndDrainSvgErrors(tester, _wrap( + CardForm( + paymentService: paymentService, + config: const CardFormConfig( + hideLogo: true, + submitButtonText: 'Go', + ), + ), + )); + + expect(find.byType(SecurePaymentSection), findsNothing); + expect(find.widgetWithText(ElevatedButton, 'Go'), findsOneWidget); + }); + }); + + group('CardFormConfig defaults', () { + test('hideLogo defaults to false', () { + const config = CardFormConfig(); + expect(config.hideLogo, isFalse); + }); + + test('submitButtonText defaults to null', () { + const config = CardFormConfig(); + expect(config.submitButtonText, isNull); + }); + }); +} diff --git a/test/widget/secure_payment_section_test.dart b/test/widget/secure_payment_section_test.dart index fc30569..986dab5 100644 --- a/test/widget/secure_payment_section_test.dart +++ b/test/widget/secure_payment_section_test.dart @@ -6,7 +6,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - final String title = 'PAGA SEGURA CON'; + const String title = 'PAGA SEGURA CON'; final Widget localLogo = SvgPicture.asset( 'test/assets/conekta-logo-blue-full.svg', height: 20.0,