Angular＋Ionicからのクレジットカード支払いサイトの利用方法

はじめに
---
Angular＋Ionicから某クレジットカード支払いサイトを利用するときに苦労したので、備忘録として。
データをPOSTして支払いサイトの画面に遷移し、クレジットカードの支払いは支払いサイトに任せるケースが対象。

通常のサイトであれば、単純に遷移して戻ってくるだけでよいが、

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285246/6099037b-6bb2-332a-b9d8-437803b710f5.png)|
|:-:|

Angular＋Ionicの場合、Ionicがローカルで動作しているため、同じように画面遷移してしまうと元の画面に戻ってくることができない。

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285246/e939a4df-7ad9-b471-c800-859911f7abff.png)|
|:-:|

InAppBrowserを使った方法
---
最初に試したのが、InAppBrowserで別ウインドウを開く方法。
結論から言うと、できるにはできたがいまいちな動きになった。
というのも、別ウインドウで支払いサイトを開いて支払いを終えた後、自動的にウインドウを閉じたいのだが、セキュリティ的な制限で自ウインドウを閉じることができなかったためだ。

回避策はありそうだが、ブラウザのバージョンや環境に依存しそう。
確実なのは、「右上の×ボタンを押して、支払いを終了してください。」などと表示してユーザに閉じてもらう方法だが・・・いまいち。

いちおう、このときの手順を記載する。

```typescript
import { InAppBrowser } from '@ionic-native/in-app-browser/ngx';

openPaymentSiteWindow() {
  // 支払いサイトへPOSTするデータを生成する。
  const order = XXXXX();

  // データをPOSTさせるためのスクリプト生成
  const action = "https://payment-site.com/payment"
  const script = `
  function formSubmit() {
    var form = document.createElement('form');
    form.action = '${action}';
    form.method = 'post';
    form.appendChild(sendData("data1", "${order.data1}"));
    form.appendChild(sendData("data2", "${order.data2}"));
    form.appendChild(sendData("data3", "${order.data3}"));

    document.body.appendChild(form);

    form.submit();
  }

  function sendData(name, value) {
    var ele = document.createElement("input");
    ele.name = name;
    ele.type = "hidden";
    ele.value = value;
    return ele;
  }

  formSubmit();
  `
    // いったんブランクのページを開く。
    var ref = this.iab.create('about:blank', '_blank', 'location=yes');

    // ウインドウが開いたら、POST用のスクリプトを実行する。
    ref.on("loadstop").subscribe((event)=>{
      ref.executeScript({ code: script});
    });

    // 支払いサイトから戻ってきた後のイベントを定義する。
    ref.on("exit").subscribe((event)=>{
      alert("payment end.");
    });

    // ウインドウを開く。
    ref.show();
}
```

iframeを使った方法
---
次に試したのが、iframe内に支払いサイトを表示させるというもの。
こちらはうまくいった。
最初はiframeを隠しておき、支払いサイト表示時にiframeを表示、戻ったらまた隠す。

```html:画面側
<iframe
  style="width: 100%; height: 100%;"
  [src]="payment_site_url"
  [style.display]="disp_payment_site"
></iframe>
```

```typescript:内部処理
import { DomSanitizer, SafeResourceUrl} from '@angular/platform-browser';

  constructor(
    private sanitizer: DomSanitizer,
  ) {}

  // 支払いサイトURL
  const action = "https://payment-site.com/payment"

  disp_payment_site = "none";
  payment_site_url: SafeResourceUrl;
  creditPayment(billing: PaymentModel) {
    try {
      console.log("クレジットカード支払い画面に遷移");

      // 支払いサイトの表示
      this.disp_payment_site = "block";
      const script = this.CreateScript();
      const url = URL.createObjectURL(new Blob([script], {type: 'text/html'}));
      this.payment_site_url =  this.sanitizer.bypassSecurityTrustResourceUrl(url);   

      // 支払いサイトからの復帰イベントの受信
      window.addEventListener('message', event => {
        console.log("支払いサイトからの復帰");
        console.log(event.data);
        this.disp_payment_site = "none";
        this.payment_site_url = null;

        alert(event.data)
      }); 
    } catch(error) {
      alert(error.message);
      this.disp_payment_site = "none";
      this.payment_site_url = null;
    }
  }

  // 支払いサイトに遷移するためのスクリプトを生成する
  // ※ パラメータをpostするため、スクリプト内でformデータを生成してsubmitする
  public CreateScript() {

    // 支払いサイトへPOSTするデータを生成する。
    const order = XXXXX();

    const script = `
  <html>
    <body>支払いサイトに遷移します...</body>
  </html>
  <script>
    function formSubmit() {
      var form = document.createElement('form');
      form.action = '${action}';
      form.method = 'post';
      form.acceptCharset = 'shift_jis';
      form.appendChild(sendData("data1", "${order.data1}"));
      form.appendChild(sendData("data2", "${order.data2}"));
      form.appendChild(sendData("data3", "${order.data3}"));

      document.body.appendChild(form);

      form.submit();
    }

    function sendData(name, value) {
      var ele = document.createElement("input");
      ele.name = name;
      ele.type = "hidden";
      ele.value = value;
      return ele;
    }

    formSubmit();
  </script>
    `
      
    return script;
  }
```

```python:結果受付URLのサーバ処理
return HttpResponse('支払いサイトから戻ります...<script>window.parent.postMessage("success", "*");</script>')
```

結果受付URLからは、
`window.parent.postMessage("success", "*")`
のスクリプトを実行させるだけでよい。
これによって、親の画面（呼び出し元の画面）に復帰イベントが通知されるので、復帰イベント内でまたiframeを隠してやる。

その他にも、サーバ側のDB更新などをさせたければ、ここで処理すればよい。
また、ここの"success"の部分に自由にデータをセットできるので、支払いサイトからの結果などを画面側に返すことも可能。

注意点
---
※ iframe内に別サイトを表示する場合、bypassSecurityTrustResourceUrlなどを使用して安全なUrlであることを宣言してやる必要がある。
※ その他、実ソースからの抜粋ですので、漏れなどありましたらすみません。。。

2021/05/07 追記
---
上記の実装だと、初期表示時に`payment_site_url`がnullのため、iframe内に`http://localhost:8100/`を表示しようとしてしまうようで、無駄にアプリ全体のコンストラクタが走ってしまう問題が発生した。
回避するために、ダミーページをセットするように修正した。

```typescript:修正箇所
  dummy_page = "<html><body></body></html>";
  dummy_url = URL.createObjectURL(new Blob([this.dummy_page], {type: 'text/html'}));
  dummy_url2 =  this.sanitizer.bypassSecurityTrustResourceUrl(this.dummy_url);   

  disp_payment_site = "none";
  payment_site_url: SafeResourceUrl = this.dummy_url2;
```