最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

AutoFillHints not saving credit card information on Flutter Web - Stack Overflow

programmeradmin3浏览0评论

I am building a Flutter Web application and would like for the user to be prompted to save the inputted credit card information to his browser. I have set up the AutoFillGroup with their respectives autoFillHints.

I have created a accessory widget to handle my TextFormField widgets. Much of it may not be relevant, but I will share it for context. It is as follows:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

/// Manages focus traversal for a custom text field.
/// This class holds a FocusNode and optionally links to the next and previous focus nodes.
class TextFieldFocusManager {
  FocusNode focusNode = FocusNode();
  TextFieldFocusManager? _next;
  TextFieldFocusManager? _previous;

  TextFieldFocusManager({TextFieldFocusManager? next, TextFieldFocusManager? previous}) {
    _next = next;
    _previous = previous;
  }

  /// Moves focus to the next field, if set.
  focusNext() {
    if (_next != null) {
      FocusScope.of(focusNode.context!).requestFocus(_next!.focusNode);
    }
  }

  /// Moves focus to the previous field, if set.
  focusPrevious() {
    if (_previous != null) {
      FocusScope.of(focusNode.context!).requestFocus(_previous!.focusNode);
    }
  }

  /// Sets focus on this field.
  focusSelf() {
    FocusScope.of(focusNode.context!).requestFocus(focusNode);
  }

  /// Sets the next focus manager. Throws if already set.
  setNext(TextFieldFocusManager next) {
    if (_next == null) {
      _next = next;
    } else {
      throw Exception('Next focus node already set');
    }
  }

  /// Sets the previous focus manager. Throws if already set.
  setPrevious(TextFieldFocusManager previous) {
    if (_previous == null) {
      _previous = previous;
    } else {
      throw Exception('Previous focus node already set');
    }
  }
}

/// A custom text field widget with validation, theming, and autofill support.
class WhiteTextField extends StatefulWidget {
  final TextEditingController controller;
  final String? hintText;
  final String? labelText;

  /// Validator function: returns an error message if the input is invalid, or null if valid.
  final String? Function(String?)? validator;
  final Widget? suffixIcon;
  final List<TextInputFormatter>? inputFormatters;

  /// The minimum number of characters required for the field to be considered complete.
  final int? minValidationLength;

  /// Whether the field is interactive.
  final bool enabled;
  final TextFieldFocusManager focusManager;
  final TextInputAction textInputAction;
  final TextInputType? keyboardType;

  /// Autofill hints for the field.
  final Iterable<String>? autofillHints;

  const WhiteTextField({
    super.key,
    required this.controller,
    required this.focusManager,
    required this.textInputAction,
    this.keyboardType = TextInputType.text,
    this.hintText,
    this.labelText,
    this.validator,
    this.suffixIcon,
    this.inputFormatters,
    this.minValidationLength,
    this.enabled = true,
    this.autofillHints,
  });

  @override
  State<WhiteTextField> createState() => _WhiteTextFieldState();
}

class _WhiteTextFieldState extends State<WhiteTextField> {
  late final FocusNode _focusNode;
  Color _borderColor = Colors.grey;
  String? _errorText;

  @override
  void initState() {
    super.initState();
    // Use the focus node provided by the focus manager.
    _focusNode = widget.focusManager.focusNode;

    // Listen to changes in focus and text to update validation.
    _focusNode.addListener(_updateValidationState);
    widget.controller.addListener(_updateValidationState);

    _updateValidationState();
  }

  /// Updates the border color and error text based on the current input and focus.
  void _updateValidationState() {
    // If the field is disabled, use a grey border and show no error.
    if (!widget.enabled) {
      setState(() {
        _borderColor = Colors.grey;
        _errorText = null;
      });
      return;
    }

    final text = widget.controller.text;
    // Check if the field meets the minimum length requirement.
    final isComplete = widget.minValidationLength == null
        ? true
        : text.length >= widget.minValidationLength!;

    if (!isComplete) {
      // If the field is not complete, skip validation.
      _errorText = null;
      // Use blue border when focused, grey otherwise.
      _borderColor = _focusNode.hasFocus ? Colors.blue : Colors.grey;
    } else {
      // Field is complete; run the validator if provided.
      if (widget.validator != null) {
        final error = widget.validator!(text);
        if (error == null) {
          _borderColor = Colors.green;
          _errorText = null;
        } else {
          _borderColor = Colors.red;
          _errorText = error;
        }
      } else {
        // No validator provided; assume the input is valid.
        _borderColor = Colors.green;
        _errorText = null;
      }
    }

    if (mounted) setState(() {});
  }

  @override
  void dispose() {
    _focusNode.removeListener(_updateValidationState);
    _focusNode.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // Determine text and background colors based on field state.
    final textColor = widget.enabled ? Colors.black : Colors.grey.shade700;
    final fillColor = widget.enabled ? Colors.white : Colors.grey.shade100;

    // If a suffix icon is provided and the field is valid, tint it green.
    Widget? suffix = widget.suffixIcon;
    if (widget.enabled && suffix != null && _errorText == null && _borderColor == Colors.green) {
      suffix = IconTheme(
        data: const IconThemeData(color: Colors.green),
        child: widget.suffixIcon!,
      );
    }

    return TextFormField(
      controller: widget.controller,
      onFieldSubmitted: (value) {
        // If the action is "done", dismiss the keyboard; otherwise, move focus to the next field.
        if (widget.textInputAction == TextInputAction.done) {
          FocusScope.of(context).unfocus();
        } else {
          widget.focusManager.focusNext();
        }
      },
      onEditingComplete: () => widget.focusManager.focusNext(),
      focusNode: _focusNode,
      enabled: widget.enabled,
      validator: widget.validator,
      inputFormatters: widget.inputFormatters,
      style: TextStyle(color: textColor),
      // Pass autofill hints to the underlying input.
      autofillHints: widget.autofillHints,
      decoration: InputDecoration(
        hintText: widget.hintText,
        labelText: widget.labelText,
        errorText: _errorText,
        errorMaxLines: 2,
        suffixIcon: suffix,
        filled: true,
        fillColor: fillColor,
        hintStyle: TextStyle(color: Colors.grey[500]),
        labelStyle: const TextStyle(color: Colors.black),
        contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
        enabledBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(4),
          borderSide: BorderSide(color: _borderColor),
        ),
        disabledBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(4),
          borderSide: const BorderSide(color: Colors.grey),
        ),
        focusedBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(4),
          borderSide: BorderSide(color: _borderColor),
        ),
      ),
      textInputAction: widget.textInputAction,
      keyboardType: widget.keyboardType,
    );
  }
}

And, when and how I create the Form is rather complicated, but here is the snippet where I would like for the AutoFillGroup to work:

Widget _buildCheckoutForm(BuildContext context) {
    final keyboardType = defaultTargetPlatform == TargetPlatform.iOS
        ? TextInputType.text
        : TextInputType.number;
    final textTheme = Theme.of(context).textTheme;
    return Column(
      children: [
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: Form(
            key: _formKey,
            child: AutofillGroup(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  // Card Information header
                  Text('Card Information', style: textTheme.bodyMedium),
                  const SizedBox(height: 8),
                  WhiteTextField(
                    focusManager: _cardNumberFocusManager,
                    textInputAction: TextInputAction.next,
                    controller: _cardNumberController,
                    enabled: !_isProcessing,
                    hintText: '1234 1234 1234 1234',
                    suffixIcon: getCardNetworkIcon(_currentCardNetwork),
                    inputFormatters: [
                      FilteringTextInputFormatter.digitsOnly,
                      CreditCardNumberInputFormatter(),
                    ],
                    minValidationLength: 19,
                    validator: Validators.validateCardNumber,
                    keyboardType: keyboardType,
                    autofillHints: const [AutofillHints.creditCardNumber],
                  ),
                  const SizedBox(height: 8),
                  // Row for Expiration Date and CVV
                  Row(
                    children: [
                      Expanded(
                        child: WhiteTextField(
                          focusManager: _expiryDateFocusManager,
                          textInputAction: TextInputAction.next,
                          controller: _expiryDateController,
                          enabled: !_isProcessing,
                          hintText: 'MM/YY',
                          inputFormatters: [
                            FilteringTextInputFormatter.digitsOnly,
                            CreditCardExpirationDateInputFormatter(),
                          ],
                          minValidationLength: 5,
                          validator: Validators.validateExpiryDate,
                          keyboardType: keyboardType,
                          autofillHints: const [AutofillHints.creditCardExpirationDate],
                        ),
                      ),
                      const SizedBox(width: 8),
                      Expanded(
                        child: WhiteTextField(
                          focusManager: _cvvFocusManager,
                          textInputAction: TextInputAction.next,
                          controller: _cvvController,
                          enabled: !_isProcessing,
                          hintText: 'CVV',
                          suffixIcon: const Icon(Icons.lock),
                          inputFormatters: [
                            FilteringTextInputFormatter.digitsOnly,
                            LengthLimitingTextInputFormatter(3),
                          ],
                          minValidationLength: 3,
                          validator: Validators.validateCVV,
                          keyboardType: keyboardType,
                          autofillHints: const [AutofillHints.creditCardSecurityCode],
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 16),
                  // Cardholder Name
                  Text('Name on Card', style: textTheme.bodyMedium),
                  const SizedBox(height: 8),
                  WhiteTextField(
                    focusManager: _cardHolderFocusManager,
                    textInputAction: TextInputAction.next,
                    controller: _cardHolderController,
                    enabled: !_isProcessing,
                    hintText: 'Name printed on card',
                    inputFormatters: [
                      LengthLimitingTextInputFormatter(50),
                    ],
                    minValidationLength: 5,
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Please enter the name printed on the card';
                      }
                      return null;
                    },
                    keyboardType: TextInputType.text,
                    autofillHints: const [AutofillHints.creditCardName],
                  ),
                  const SizedBox(height: 16),
                  // ID
                  Text('ID Number', style: textTheme.bodyMedium),
                  const SizedBox(height: 8),
                  WhiteTextField(
                    focusManager: _documentNumberFocusManager,
                    textInputAction: TextInputAction.done,
                    controller: _documentNumberController,
                    enabled: !_isProcessing,
                    hintText: 'Just numbers',
                    inputFormatters: [
                      FilteringTextInputFormatter.digitsOnly,
                    ],
                    minValidationLength: 14,
                    keyboardType: keyboardType,
                  ),
                ],
              ),
            ),
          ),
        ),
        const SizedBox(height: 16),
        // Checkbox to save card data.
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: Row(
            children: [
              Checkbox(
                value: _saveCardData,
                onChanged: _isProcessing
                    ? null
                    : (bool? value) {
                  setState(() {
                    _saveCardData = value ?? false;
                  });
                },
              ),
              const Text('Save card data'),
            ],
          ),
        ),
        const SizedBox(height: 4),
        // Pay button.
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: SizedBox(
            width: double.infinity,
            height: 48,
            child: ElevatedButton(
              style: ElevatedButton.styleFrom(
                backgroundColor: _isSuccess ? Colors.green : Colors.black,
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(4),
                ),
              ),
              onPressed: _processCreditCardPayment,
              child: const Text(
                'Pay',
                style: TextStyle(color: Colors.white, fontSize: 16),
              ),
            ),
          ),
        ),
        const SizedBox(height: 24),
        const PaymentFooter(),
      ],
    );
  }

I have tested it on Chrome desktop and mobile (Android) with no success. I tried running it locally and deployed in a https secure website. In the generated HTML, the autocomplete tags are correct for every field.

I also saw that the keyboardType could have an influence on the autoFillHints working. However, on Android where keyboardType = TextInputType.number, it also did not work.

发布评论

评论列表(0)

  1. 暂无评论