Stripe Elementsをカスタマイズしてクレジットカード入力のユーザービリティを向上させる

本記事は JP_Stripes Advent Calendar 2019 の15日目の記事です。昨日は中山千春さんによる Stripe入社1年、好きな自社プロダクトについて熱く語ってみる! でした。Stripe、そしてStripe Connectへの熱さが伝わる素晴らしい記事でした。

Stripe Elementsをカスタマイズし、クレジットカード入力のユーザービリティを向上させる取り組みと実装方法についてご紹介したいと思います。

Stripe Elementsとは?

Stripe Elementsは、Stripeの主にクレジットカード入力に利用できるUIコンポーネントライブラリです。Stripeを使ってスムーズな顧客体験が構築できるように考えられています。

Stripe Elements は、Stripe のフロントエンドチーム、デザインチーム、アナリティクスチームの協力のもとに開発されています。顧客が情報をすばやく正確に入力できるようサポートを行い、入力ミスを減らし、支払い処理の成功率を高めています。

Stripe Elementsは カスタマイズ性が非常に高い 点が魅力の一つです。

カスタマイズ例

「Stripe Elementsを使って、どうやって作るのか?」と言った悩みを持つ必要もなく、Stripe Elementsの公式のカスタマイズ例が公開されています。

気になるフォーム例を見つけたら GitHubでのソースの確認 を開くことでソースコードが確認できます。カスタマイズ可能な状態を提供するだけではなく、自分の作りたいUIに近しいサンプルから始められるのは開発者にとって非常にありがたいですね!

カスタマイズの必要性

カスタマイズしない場合でも、以下のようなフォームが作成できます。

こちらを使って、弊社が運営しているキャッシュレスカフェ「Developers.IO CAFE」で採用したところ、一般のお客様が利用される際に以下のような課題に直面しました。

  • フォームのどこに何を入力すれば良いか分からない
  • 「CVS」「CVC」と言う言葉の意味が分からない

オンラインショッピング、ECサイトなどといったWebサイトでのクレジットカード入力に慣れている方は気にならないかも知れませんが、初めて入力される方にとってはもう少し補助があったほうが良いかも知れません。このような課題を解決するため、入力フォームのカスタマイズを実施しました。

カスタマイズしてみた

Webアプリでは Vue.jsIonic を組み合わせており、それらのフレームワークの上にStripe Elementsのカスタマイズを行いました。

Vue.js は本来1ファイルで構成されますが、ブログでの見やすさのためTemplate、JavaScript、CSSをそれぞれ分けて掲載しています。

Template (DOM)

<template>
  <div>
    <form>
      <ion-grid>
        <ion-row>
          <ion-col>
            <div class="card-label-container">
              <label for="card-number" data-tid="elements.form.card_number_label">
                カード番号
              </label>
            </div>
            <div id="card-number" class="input empty"></div>
            <div class="baseline"></div>
          </ion-col>
        </ion-row>

        <ion-row>
          <ion-col>
            <div class="card-label-container">
              <label for="card-expiry" data-tid="elements.form.card_expiry_label">
                有効期限
              </label>
            </div>
            <div id="card-expiry" class="input empty"></div>
            <div class="baseline"></div>
          </ion-col>

          <ion-col>
            <div class="card-label-container">
              <label for="card-cvc" data-tid="elements.form.card_cvc_label">
                セキュリティコード
              </label>
            </div>
            <div id="card-cvc" class="input empty"></div>
            <div class="baseline"></div>
          </ion-col>
        </ion-row>
      </ion-grid>
    </form>
  </div>
</template>

ion-gridion-row ion-col はそれぞれグリッドの中に行、列を表現している要素と言うように置き換えて見てください。

今回はチェックアウトではなく単にクレジットカード登録のためのフォームを作りたかったので、クレジットカード登録に必要な入力コンポーネントを用意します。

  • カード番号入力
  • 有効期限入力
  • セキュリティコード入力

それぞれ、スマホで大きく表示できるように一行ずつに表示します。また、それぞれの入力欄にはラベルを付けることで、何を入力すれば良いか一目で分かるようにしました。

特に セキュリティコード がポイントで、デフォルトですと CVC や CVS と言う名前になります。こちらは実際のカフェの運用で、お客様から「CVSって何?」と言う質問があった場合に「セキュリティコードのことです」と回答すると100%理解される、と言う経験則からセキュリティコードというラベルを採用しました。実際、国内では一般的に使われている名称かと思います。

JavaScript

続いてJavaScriptコードです。

import config from '../../config';
const stripe = Stripe(config.stripe.token);

export default {
  name: 'CreditCardForm',
  props: {
    onInputComplete: { type: Function, required: true }
  },
  data() {
    return {
      complete: false,
      stripeOptions: {
        hidePostalCode: true
      },
      cardForm: {},
      state: {
        cardNumber: false,
        cardExpiry: false,
        cardCv: false
      }
    };
  },
  mounted() {
    this.show();
  },
  methods: {
    show() {
      const elements = stripe.elements({
        fonts: [{ cssSrc: 'https://fonts.googleapis.com/css?family=Source+Code+Pro' }]
      });
      const elementStyles = {
        base: {
          color: '#32325D',
          fontWeight: 500,
          fontFamily: 'Source Code Pro, Consolas, Menlo, monospace',
          fontSize: '1.2em',
          fontSmoothing: 'antialiased',
          '::placeholder': {
            color: '#CFD7DF'
          },
          ':-webkit-autofill': {
            color: '#e39f48'
          }
        },
        invalid: {
          color: '#E25950',
          '::placeholder': {
            color: '#FFCCA5'
          }
        }
      };
      const elementClasses = {
        focus: 'focused',
        empty: 'empty',
        invalid: 'invalid'
      };
      this.cardNumber = elements.create('cardNumber', {
        style: elementStyles,
        classes: elementClasses
      });
      this.cardNumber.mount('#card-number');
      this.cardNumber.addEventListener('change', event => {
        this.state.cardNumber = event.complete;
        if (event.complete) {
          this.cardExpiry.focus();
        }
        this.onInputComplete(this.completed());
      });

      this.cardExpiry = elements.create('cardExpiry', {
        style: elementStyles,
        classes: elementClasses
      });
      this.cardExpiry.mount('#card-expiry');
      this.cardExpiry.addEventListener('change', event => {
        this.state.cardExpiry = event.complete;
        if (event.complete) {
          this.cardCvc.focus();
        }
        this.onInputComplete(this.completed());
      });

      this.cardCvc = elements.create('cardCvc', {
        style: elementStyles,
        classes: elementClasses
      });
      this.cardCvc.mount('#card-cvc');
      this.cardCvc.addEventListener('change', event => {
        this.state.cardCvc = event.complete;
        this.onInputComplete(this.completed());
      });
    },
    completed() {
      return this.state.cardNumber && this.state.cardExpiry && this.state.cardCvc;
    },
    async getToken() {
      return await stripe.createToken(this.cardNumber);
    }
  }
};

要点を解説します。

まずStripe Elementsを使った入力フォームのお作法としては、対象の入力エレメントのオブジェクトを作成し、実際のDOMにマウントするといった手順を取ります。

// 対象の入力エレメントのオブジェクトを作成
this.cardNumber = elements.create('cardNumber', {
  style: elementStyles,
  classes: elementClasses
});
// 実際のDOMにマウント
this.cardNumber.mount('#card-number');

上記のコードは cardNumber つまりカード番号入力エレメントのオブジェクトを作成し、#card-number のDOMにマウントしています。Stripe ElementsのSDKの機能により、実際のtext-inputエレメントに置換されます。

elementStyles は、エレメントに適用するCSSを設定します。指定可能な項目は下記を参照してください。

elementClasses は、カードの入力状態によって切り替わるエレメントのクラスの名前を指定します。指定することで、後からCSSを調整したり、エレメントに制御を加えたりといったより細やかなカスタマイズが可能です。

さらに上記のコードでは、各入力項目のハンドリングを行なっています。

this.cardNumber.addEventListener('change', event => {
  this.state.cardNumber = event.complete;
  if (event.complete) {
    this.cardExpiry.focus();
  }
  this.onInputComplete(this.completed());
});

入力内容の変更に応じて、入力が完了していた場合に次の入力項目にフォーカスを当てる制御を行なっています。

propsonInputComplete は、この入力フォームのコンポーネント自体の入力が完了した際に呼ばれるイベントハンドラを外部から設定できるように用意しています。

props: {
  onInputComplete: { type: Function, required: true }
},

completed() は各入力項目の入力が全て完了しているかどうかを返します。onInputComplete の引数に渡すことで、入力が完了したかどうかを外部で判別できるようにしています。

completed() {
  return this.state.cardNumber && this.state.cardExpiry && this.state.cardCvc;
}

getToken() は、Stripeにクレジットカード情報を送り、トークンを生成するためのメソッドです。カード情報はStripe Elementsを経由して作成したエレメントで入力されているため createToken() を呼び出す際には引数には必要ありません。

async getToken() {
  return await stripe.createToken(this.cardNumber);
}

CSS

CSSについてはレイアウト調整、マージン調整程度ですので解説は割愛します。このあたりは好みにより調整してみてください。

.card-label-container {
  height: 1.5em;
  width: 100%;
  display: flex;
  margin: 10px 0;
  font-size: 0.8em;
  font-weight: 800;
  align-items: center;
}
.card-label-container ion-icon {
  font-size: 1.4em;
  margin: 0 6px;
}
.card-label-container label {
  pointer-events: none;
}
ion-row {
  margin: 10px 0;
}
.input {
  padding: 5px 0 6px 0;
  border-bottom: 1px solid #ddd;
}

動作確認

これらの実装で、下図のような表示にできます。このようなUIに変更した後、カフェでの「入力方法が分からない」と言った質問が格段に減りました!

まとめ

スマホで1画面でクレジットカードを入力してもらう場合、デフォルトの一行のフォームでは画面に余白が多くなってしまいます。せっかくのスペースを有効活用し、入力のユーザビリティを高められると良いと思います。

JP_Stripes Advent Calendar 2019、明日は taman777 さんによる「決済したい!っていう人には Stripe を使ってほしい!」です。お楽しみに!