21
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

NextjsでGMOペイメントゲートウェイのカードを登録する処理(プロトコルタイプ)

Last updated at Posted at 2021-07-29

対象読者

  1. NextやReactでcdnを使いたい方
  2. GMOペイメントゲートウェイをNextやReactで使いたい方

1の人はまとめまで飛んで下さい

バックグラウンド

業務で新サービスを作っています。
バックエンドはLaravel、フロントエンドはNextjsを使用しています。

その新サービスでは決済はクレジットカードを利用しています。
決済に関しては個人的にはstripeが良かったのですが、費用とかを鑑みた末...
選ばれたのはGMOでした
また仕様によりプロトコルタイプを選択しました。

stripe等のサービスと違いSDKもなくライブラリも充実しているわけでは無いのでなかなかに実装に工夫が必要でした。
値段とサービスどちらを取るかという論点は人によって価値観が違うので一概にどちらが優れているという比較はできませんが、これだけは言わせてください。
ドキュメントに関して一言だけ文句を言わせてください。このドキュメントの多さと分かりにくさ、「あえて言おう、カスであると」o1080076514625398358.jpeg

嘘です。GMOさんGMOの皆さんごめんなさい
このセリフ一度でいいから言ってみたかっただけなんです
GMOの良さはstripeやpaypalより手数料が安いところです。
あと電話サポートの方の対応は丁寧かつ、技術の知識もあるためか正確な返答が得られた点がとても良かったです。
ドキュメントで躓いたら片っ端から電話しましょう!
ただ公平な目で判断した際に少し手間であるのは紛れもない事実です。これからのユーザーやGMOさんのためにもなると思ったのでこの記事を書くことにしました。強いられてるんだQiitaを書くことを

前提条件

カードから毎月反響した分だけ引き下ろすためにはカードの持ち主をGMOに登録しておくことと、カード自体を登録する必要があります。

GMOのAPIからの返り値をarrayにする汎用関数

不満点その2。返り値が不親切であること

そのため使いやすく整形して返す関数を定義しました。
まずgmoのAPIから帰ってくる生のbodyがこちらです。

"CardSeq=0&DefaultFlag=1&CardName=&CardNo=*************111&Expire=2405&HolderName=&DeleteFlag=0"

ええ、、、&つなぎの文字列、、、
「あえて言おう、(自重)

このままだと使いにくいんで配列に変換する汎用関数を作ります。
curlやguzzleで取ってきた値を__$rawReturn__と仮定します

public static function gmoFormatter(string $url, array $params): array
{
    $rawReturn = self::postCurl($url, $params); // 実際にpostする汎用関数(長くなるので中身は省略) curlでもguzzleでも好きなものを使ってください
    $status = $rawRet['status'];
    $dataList = explode('&', $rawReturn['data']); // & 分解
    foreach($dataList as $d) {
        $dkv = explode('=', $data); // = 分解: 左辺がkey, 右辺がvalueになっている
        $ret[$dkv[0]] = $dkv[1];
        $dkv[0] === 'ErrCode' && $status === 200 && $status = 400; // 通信が成功しつつエラーコードがあればstatusを400にしちゃいます
    }

    $ret = ['status' => $status, 'data' => $ret]; // エラーコードがあったときにはstatusを書き換えているので判定がしやすくなっています。
    return $ret;
}

以下が整形した結果の配列です。どうでしょう?
圧倒的じゃないか、我が軍は

{
    "stauts":200,
    "data":[
        {
            "CardSeq":"0",
            "DefaultFlag":"1",
            "CardName":"",
            "CardNo":"*************111",
            "Expire":"2405",
            "HolderName":"",
            "DeleteFlag":"0"
        }
    ]
}

あらかじめ会員登録をしておいてメンバーIDを作っておく

毎月決済するための加盟店のカードを登録するためには加盟店をGMOのショップの会員に登録した上で、その会員のカードがどれかわからなければなりません。
そこで、今回はサービスの会員登録時に発行される一意のユーザーIDを利用して、memberIDを作成します。
そしてそのmemberIDと加盟店の名前を元にGMOのショップの会員としても同時に登録する処理を行なっています。

一言で言うと、ユーザーのinsertと並列してGMO側にも必要な情報を投げて会員登録している形になります。


use App\Domains\GmoCurlDomain;

class GmoDomain
{
    private function __constructor()
    {
        // gmo env系
        $this->apiUrl = config('custom.gmoApiUrl');
        $this->siteId = config('custom.gmoSiteId');
        $this->sitePw = config('custom.gmoSitePw');
        $this->shopId = config('custom.gmoShopId');
        $this->shopPw = config('custom.gmoShopPw');
    }

    /**
     * 会員登録する
     * @param String $memberName
     * @param String $memberId
     * @return Array $exe
     */
    public function saveMember(string $memberName, string $memberId): array
    {
        $url = $this->apiUrl . '/'
            . 'payment/' . 'SaveMember.idPass';

        $params = [
            'SiteID'     => $this->siteId,
            'SitePass'   => $this->sitePw,
            'MemberID'   => $memberId,
            'MemberName' => mb_convert_encoding($memberName, 'SJIS', 'UTF-8'), // 文字化け用に mb_convert_encoding
        ];

        $exe = GmoCurlDomain::gmoFormatter($url, $params);
        return $exe;
    }
}

プロトコルタイプってなに? トークン決済ってなに?

カード登録や決済の際のお話です。
加盟店のサーバーサイド等を経由して入力されたカード番号を直接GMOのカード登録や決済が行われるとカード番号の流出等のリスクが上がってしまうので、GMOではこのトークン決済というものを利用しています。

プロトコルタイプでカード登録をする方法2選

入力されたカード番号から決済やカード登録に必要なトークンを取得するために以下の2つの方法があります。
それぞれエンドポイントは違います。

  1. そのままAPIに投げる (sslのみ有効)
  2. cdnを利用してAPIに投げる (sslじゃなくてもOK)
    cdnを利用するとPOSTする前に暗号化してPOSTしてくれます。これでsslの代わりを担って通信を安全にしてくれています。

今回私は、ローカルでもテストとして使用したかったので2を選択しました。

Nextjs(React)でクレカを登録する

前提条件
GMOから受け取ったtokenを使ってクレカを登録する際には、以下の様な処理を行うAPIにポストしています。
もらってきたtokenと顧客固有のid(saveMemberのときにポストしたものと同じもの)やサイトID等を投げます。
カードの番号はもちろん、セキュリティコード、有効期限等が含まれていないのがちゃんとわかります。

public function saveNewCard(
    string $memberId,
    string $token
): array {
    $url = $this->apiUrl . '/'
        . 'payment/' . 'SaveCard.idPass';
    $params = [
        'SiteID' => $this->siteId,
        'SitePass' => $this->sitePw,
        'MemberID' => $memberId,
        'Token' => $token,
        'DefaultFlag' => 1,
    ];
    $exe = GmoCurlDomain::gmoFormatter($url, $params);
    return $exe;
}

Nextjs(React)でcdn内のclassをインスタンス化

Nextjsでcdnのクラスをインスタンス化して使う?できらぁ!

cdnで定義されたクラスをインスタンス化する際に、いかにそのクラスをNext(React)上に記述しないかが重要なポイントです。
以下のプロセスはそれを念頭に置いた上で行いました。

Multipayment がそのアンタッチャブルなクラスに該当します。

失敗例1 (devだと動くけどcompileはできない例)

dangerouslySetInnerHTMLを使用しています。
getCreateCardTokenはdangerouslySetInnerHTMLで定義したつもりですが、それはブラウザ上で定義されているだけで、next(react)内では定義してないのもちろんコンパイルはできません。でnpm run devでは動きますが、、、

const Credit: NextPage = () => {
    const [mess, setMess] = useState<string[]>([]);

    const startCreateCard = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        setMess(['送信中']);
        const ret = await getCreateCardToken(e);
        if (ret && ret['resultCode'] === '000') { // カード情報のトークン化に成功すればresultCode 000が返ってきます
            const res: Response = await fetchCreateCredit(newToken); // 返ってきたtokenをバックエンドにPOST // バックエンドでは受け取ったトークンやユーザー情報を利用してGMOのカード作成エンドポイントにポストする
            switch (res.status) {
                case 200:
                    setMess(null);
                    router.push(`<next page>`)
                    break;
                default:
                    setMess([`エラーが発生しました。`]);
            }
        } else {
            setMess([`エラーが発生しました。`]);
        }
    };

    return (
        <div>
            <Head>
                <script defer src={`${GMO_TOKEN_URL}`}></script>{/*  cdnのurlです  */}
                <script
                    defer
                    dangerouslySetInnerHTML={{
                        __html: `
                        // 定義されたままになるので再定義可能なvarを使う!!
                        // 即読み込みできないのでタイムアウト
                        setTimeout(() => {
                            Multipayment.init('<shopID>');
                        }, 1000);
                        
                        // componentの移動の際にも定義されたままになるので再定義可能なvarを使う!!
                        var getCreateCardToken = (e) => {
                            return new Promise(function (resolve, reject) {
                                Multipayment.getToken(
                                    {
                                        cardno: e.target.card.value,
                                        expire: e.target.expire.value,
                                        securitycode: e.target.securitycode.value,
                                    }, resolve
                                );
                            });
                        }
                    `,
                    }}
                />
            </Head>
            <form onSubmit={startCreateCard}>
              <label>カード番号</label>
              <input
                type="text"
                name="card"
                required
                maxLength={16}
                minLength={16}
              />
              
              以下省略


            </form>
        </div>
    )
}

export default Credit;

失敗例2 (compileできるけど動かない例)

では、適当にgetCreateCardTokenを宣言だけしてみたらどうだろう?
上のコードに以下を追加してみます。

useEffect(() => {
  var getCreateCardToken: Function;
}, []);

ページがマウントされた後にgetCreateCardTokenを宣言、dangerouslySetInnerHTMLでgetCreateCardTokenを上書きする作戦です。

"getCreateCardToken" is not a function

これならとりあえずはコンパイルできる状態にはなりました。
しかし、今度はブラウザ上で動かない。
dangerouslySetInnerHTMLで上書きするとgetCreateCardTokenはundefinedとなってしまっていました。

この後も色々と試行錯誤したが、長くなるので割愛します。

正解

コンパイルできて、ちゃんと動く正解の形です。
文字列を関数に変換できればいいのではないか?と思い、このような実装をしました。
予想通り完璧に動きました。

const Credit: NextPage = () => {
    const [mess, setMess] = useState<string[]>([]);

    const strFunction = `
        Multipayment.init('${GMO_KEY}');
        return new Promise(function (resolve, reject) {
            Multipayment.getToken(
                {
                    cardno: e.target.card.value,
                    expire: e.target.expire.value,
                    securitycode: e.target.securitycode.value,
                }, resolve
            );
        });
    `;
    let getCreateCardToken = new Function('e', strFunction);
    useEffect(() => {
        getCreateCardToken = new Function('e', setFunction);
    }, []);

    const startCreateCard = async (e: React.FormEventHandler<HTMLFormElement>) => {
        e.preventDefault();
        setMess(['送信中']);
        const ret = await getCreateCardToken(e);
        if (ret && ret['resultCode'] === '000') { // カード情報のトークン化に成功すればresultCode 000が返ってきます
            const res: Response = await fetchCreateCredit(newToken); // 返ってきたtokenをバックエンドにPOST // バックエンドでは受け取ったトークンやユーザー情報を利用してGMOのカード作成エンドポイントにポストする
            switch (res.status) {
                case 200:
                    setMess(null);
                    router.push(`<next page>`)
                    break;
                default:
                    setMess([`エラーが発生しました。`]);
            }
        } else {
            setMess([`エラーが発生しました。`]);
        }
    };

    return (
        <div>
            <Head>
                <script defer src={`${GMO_TOKEN_URL}`}></script>
            </Head>
            <form onSubmit={startCreateCard}>
              <label>カード番号</label>
              <input
                type="text"
                name="card"
                required
                maxLength={16}
                minLength={16}
              />
              
              以下省略


            </form>
        </div>
    )
}

export default Credit;

ミソはこの部分です。

const strFunction = `
    Multipayment.init('${GMO_KEY}');
    return new Promise(function (resolve, reject) {
        Multipayment.getToken(
            {
                cardno: e.target.card.value,
                expire: e.target.expire.value,
                securitycode: e.target.securitycode.value,
            }, resolve
        );
    });
`;
let getCreateCardToken = new Function('e', strFunction); // cdnがロードされていないので ここは意味合い的には宣言だけ
useEffect(() => {
    getCreateCardToken = new Function('e', strFunction); // 上書きする ここで定義される。 cdnがロードされた後に実行される
}, []);

useStateでなく、letを使ったのは一回だけしか変更がないためletでいいかな〜って思ったからです。

let getCreateCardToken = new Function('e', strFunction)
では以下のような処理が行われています。

let getCreateCardToken = (e) => {
    // strFunctionの中身
    Multipayment.init('${GMO_KEY}');
    return new Promise(function (resolve, reject) {
        Multipayment.getToken(
            {
                cardno: e.target.card.value,
                expire: e.target.expire.value,
                securitycode: e.target.securitycode.value,
            }, resolve
        );
    });
}

まとめ

nextやreactでcdn内のクラスをインスタンス化して使いたい場合、以下のようにやると良いです。

import Head from 'next/head';

const Sample: NextPage = () => {
    const strFunction = `
        <SomeClass>.init();
        何らかの処理
    `;
    let cdnFunction = new Function('arg', strFunction);
    useEffect(() => {
        cdnFunction = new Function('arg', strFunction);
    }, []);

    return (
        <div>
          <Head>
            <script src={`${CDN_SOURCE}`}></script>
          </Head>
        </div>
    )
}

export default Sample;

GMOペイメントゲートウェイについて、手数料が安く電話サポートが充実している点はとても素晴らしいと思います。
少し高くても手間をかけたくなかったらstripe、手間をかけてでも手数料を節約したいならGMOペイメントゲートウェイを選ぶといいんではないでしょうか。

最後に一言
すまんが、みんなのLGTMをくれ
DZpx5vcU8AAb7D8.jpeg

21
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?