UPSIDER Tech Blog

UPSIDERアプリにおける PAN 入力フィールドの工夫

こんにちは。

今回は UPSIDER でモバイルアプリの開発を担当している app team から、クレジットカード番号(PAN: Primary Account Number)を入力するためのテキストフィールドに関して、UX を高めるために UPSIDER アプリで行っている実装上の工夫について紹介します。

UPSIDERアプリにおける入力チェックの考え方

UPSIDER では、クレジットカード情報を含む重要な入力に対しては、バックエンドで精緻なバリデーションやセキュリティ対策を行っています。

その前提の上で、本記事ではアプリ側で「どうすればユーザーがストレスなく正しく入力できるか」という UX の観点から、入力体験をどう設計しているかにフォーカスします。フロントエンドでもユーザーに早い段階で入力ミスを伝えることで、快適な体験に繋げたいと考えています。

入力体験を良くするために

クレジットカード番号の入力は、多くの人が一度は経験したことがあると思いますが、16桁の数字をミスなく入力するのは意外と面倒です。
特に以下のような点が UX の観点で課題になりがちです:

  • 数字が長く、視認性が低い
  • 入力ミスが起きやすい
  • ハイフンやスペース付きでコピペされるケースがある

UPSIDER のアプリでは、これらの課題に対応するため、Flutter の TextFormField に対して独自の InputFormatterValidator を組み合わせた仕組みを実装しています。

InputFormatter によるリアルタイム整形

Flutter には TextInputFormatterという仕組みがあり、入力されたテキストをリアルタイムで加工することができます。
UPSIDER では、ユーザーがクレジットカード番号を入力した際に4桁ごとにスペースを入れる整形を行うようにしています。

class _TextInputFormatter extends TextInputFormatter {
  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    final text = newValue.text;
    if (text.length <= 4) return newValue;

    final numbers = text.runes.where((c) => c >= 0x30 && c <= 0x39);
    final iterator = numbers.iterator;
    final formatted = StringBuffer();
    var i = 0;

    while (iterator.moveNext()) {
      formatted.write(iterator.current - 0x30);
      if (i % 4 == 3 && i != numbers.length - 1) {
        formatted.write(CardNumber.spacer); // 通常はスペース
      }
      i++;
    }

    return newValue.copyWith(
      text: formatted.toString(),
      selection: newValue.selection.copyWith(
        baseOffset: formatted.length,
        extentOffset: formatted.length,
      ),
    );
  }
}

これにより、例えば 4111111111111111 と入力された場合や 4111-1111-1111-1111 のような文字列をペーストされた場合でも、4111 1111 1111 1111 の形式に自動で整形されます。

また、FilteringTextInputFormatterLengthLimitingTextInputFormatter を併用し、数字以外の入力を制限したり、最大桁数を制御しています。

Validator による入力チェック

もうひとつ重要なのが、入力値の妥当性チェックです。
UPSIDER では、クレジットカード番号の形式が正しいかどうかを即時に判定し、必要に応じてユーザーにエラーを返すようにしています。

class _Validator {
  String? validate(String? value) {
    if (value == null || isBlank(value)) {
      return 'required';
    }
    if (value.length != _spacedCardNumberLength) {
      return 'invalid card number length';
    }
    if (!_isValidCardNumber(value)) {
      return 'invalid card number';
    }
    return null;
  }

  bool _isValidCardNumber(String cardNumber) {
    final number = cardNumber.runes.where((c) => c >= 0x30 && c <= 0x39);
    if (number.length != _cardNumberLength) return false;

    // 番号の正当性確認

    return result;
  }
}

ここでは、Luhn アルゴリズムをはじめ、クレジットカード番号の長さや数字の構造といった基本的な検証ロジックなどを組み合わせながら、番号の正当性を確認しています。

一方で、クレジットカード番号の検証を強めすぎないようにも留意しています。例えば PAN の最初の数桁(BIN: Bank Identification Number)は発行会社ごとに概ね決まっており、これを使ってブランドや発行元を判定することも可能です。ただし、このような検証をフロントエンド側で行う場合、カードブランドや BIN の追加・変更に合わせてフロントエンドのリリースが必要になります。結果として、新しいカードへの対応や機能提供が遅れてしまうことにもつながりかねません。

そのため、UPSIDER ではフロントエンドで過剰に検証しすぎず、ユーザー体験と開発運用のバランスをとるようにしています。

おわりに

UPSIDER のモバイル開発では、こうした細かなUI・UX改善にも日々取り組んでいます。
同様の課題に取り組んでいる方の参考になれば幸いです。