Mark Ku's Blog
首頁 關於我
Apple Pay on Web + Cybersource 串接筆記
Payment
Apple Pay on Web + Cybersource 串接筆記
Mark Ku
Mark Ku
August 10, 2022
2 min

時空背景

因工作需要美國及德國電商網站,需要界接 Apple Pay。

Apple Pay 原理

參考 啾啾鞋影片

美國有多少人口使用 Apple Pay

參考 oberlo 網站

Apple Pay / Google Pay 和第三方支付的差異

Apple Pay / Google Pay 和第三方支付最大的不同是,第三方金流公司會協助處理和銀行帳務問題,但 Apple Pay / Google Pay 並不會。

在 Apple 官方的成功案例中 得知自己對接 Apple Pay 及銀行的公司規模都相當的大,其他大多數都是透過 Payment Provider,我猜可能大部分的銀行並沒有這麼標準及各國法規都不太一樣,各家銀行如果資料交換失敗,要處理的帳務問題就會很多,處理這段的問題是一般公司無法負擔的。

網頁如何發起支付

早期各家瀏覽器都是各自載入 JS Lib 去實作,後面 W3C 網站對瀏覽器的對支付訂義標準規格,現今Safari 及 Chrome 都己實作,PaymentRequest Api。 (相容性)

  • 實測 window.PaymentRequest,一定要 Https ,否則會在瀏覽器中,找不到這物件。
  • Apple Pay 只能在 Safari 上使用 ( desktop and mobile )

程式串接前需提前準備的項目

  • 付款頁需要 Https 環境 ( dev、prod )
  • 蘋果電腦及 iPhone
  • 商店需自行申辦 Apple Developer 開發者帳號 ( 99 USD / Year )
  • 在開發者後台商戶域名通過驗證
  • 在開發者後台上傳 金流商 CSR 及開發者 CSR 至蘋果後台
  • Apple Pay Button 及 相關 logo 需符合 Apple UI 規範

Apple Pay 付款流程

當使用者按下付款按鈕 > 前端 Call 後端 Api 去和蘋果驗證金流商戶,並建立交易的 session,取得用戶端 token > 此時 iphone 會請求使用者刷臉或指紋驗證 > Call 自己的後端 Api,向金流商請求建立訂單。

首先,在程式開發前,至 Apple 開發者後台設定並取得憑證

建立金流商戶 ( Merchant )

蘋果開發者後台> Certificates, Identifiers & Profiles

Identifiers > App IDs > Merchant IDs > Identifiers +

Merchant IDs > Continue

Name的部份隨便 key,輸入商戶識別 id Identifier (官方建議 {domainName} + {appName }),請先記錄起來,之後程式串接時要傳遞。

接著,從這畫面,我們得知正式撰寫金流程式前必須先準備,以下三項

一、上傳金流商(Cybersource 的 CSR)

Cybersource’s BackOffice > Payment Configuration > Apple Pay > Configure > 填入 Apple Merchant ID > Generate New Certificate Signing Request > 下載憑證 > 上傳至蘋果後台 Apple Pay Payment Processing on the Web

二、透過自己的蘋果電腦產生 CSR,去蘋果後台產生憑證

Keychain Access > Request a Certificate from Certifcate Authority…

2.輸入 CA 相關資訊

3. 選擇憑證格式 ( 預設 RSA 即可 )

4.至開發人員後台 > Apple Pay Merchant Identity Certificate > 將剛產生的 CSR 上傳上去
5.上傳完後會有個 Download,按下後會將憑證下載至電腦,之後商戶驗證時和蘋果發出請求需要憑證,但 NET x509 元件無法使用 Cer,因此要透過 Apple 電腦轉成 p12 檔。

6. 產生 P12 檔 for 後端商戶驗證 Api

將下載下來的 cer 拖進 Keychain Access 的 login, 剛拖進來會發現憑證是 certificate is not trusted。

此時到 APPLE PKI 網站
安裝圖中紅框選取的憑證後,剛安裝憑證就會變成 This certificate vaild.

右鍵 > Export ( 密碼可隨便輸 )

三、驗證域名是不是自己的

輸入驗證域名 > 下載驗證檔案 > 放在該台網站伺服器 > 按下 Verify按鈕

撰寫程式

參考蘋果官方的 Apple Pay Live Demo,可以從範例程式得知,Apple Pay 的前端主要的事件流程有:

  • onvalidatemerchant ( 使用者按下按鈕,至自己的後端驗證商戶 )
  • onpaymentauthorized ( 商戶驗證成功,觸發交易 )
  • onpaymentmethodselected ( 付款方式選擇 )
  • onshippingcontactselected ( 選擇收人時觸發 )
  • onshippingmethodselected ( 選擇運輸方式)

前端範例程式

<script src="https://applepay.cdn-apple.com/jsApi/v1/apple-pay-sdk.js"></script>

<style>
    apple-pay-button {
        --apple-pay-button-width: 150px;
        --apple-pay-button-height: 30px;
        --apple-pay-button-border-radius: 3px;
        --apple-pay-button-padding: 0px 0px;
        --apple-pay-button-box-sizing: border-box;
    }
</style>

<h1>Apple Pay Welcome</h1>
<h2>Apple Pay button is only show in safari!!! </h2>

<apple-pay-button buttonstyle="black" type="plain" locale="en" onclick="onApplePayButtonClicked()">123</apple-pay-button>

<script>
    function onApplePayButtonClicked() {

        if (!ApplePaySession) {
            return;
        }

        // Define ApplePayPaymentRequest
        const request = {
            "countryCode": "US",
            "currencyCode": "USD",
            "merchantCapabilities": [
                "supports3DS"
            ],
            "supportedNetworks": [
                "visa",
                "masterCard",
                "amex",
                "discover"
            ],
            "total": {
                "label": "付給 xxx 公司",
                "type": "final",
                "amount": "0.1"
            }
        };

        // Create ApplePaySession
        const session = new ApplePaySession(3, request);
        const failObject = {
            'status': ApplePaySession.STATUS_FAILURE
        }

        session.onvalidatemerchant = event => {
            const validationURL = event.validationURL;

            const failObject = {
                'status': ApplePaySession.STATUS_FAILURE
            }

            getApplePaySession(validationURL).then(function (response) {
                debugger

                let result = JSON.parse(response)
                session.completeMerchantValidation(result);
            }).then(function (response) {
                session.completeMerchantValidation(failObject)
            }).catch(err => {
                session.completeMerchantValidation(failObject)
            })
        };
    
session.onpaymentauthorized = event => {

            /*alert('onpaymentauthorized' + JSON.stringify(event.payment.token.paymentData))*/

            var paymentDataString =
                JSON.stringify(event.payment.token.paymentData);
            var paymentDataBase64 = btoa(paymentDataString);

            debugger
            let data = {
                amount: request.total.amount,
                paymentTokenObject: paymentDataBase64
            }

            paymentProcess(data).then(function (response) {
     
                if (response === true) {
                    /*alert('true')*/
                    const result = {
                        "status": ApplePaySession.STATUS_SUCCESS
                    };
                    session.completePayment(result);
                } else {
                    session.completePayment(failObject);
                }
                //let result = JSON.parse(response)
                //session.completeMerchantValidation(result);
            }).then(function (response) {
                session.completeMerchantValidation(failObject)
            }).catch(err => {
                session.completeMerchantValidation(failObject)
            })

            // Define ApplePayPaymentAuthorizationResult

        };    

        session.oncancel = event => {
            alert('oncancel')
            session.abort(); // maybe not*/            
        };

        session.begin();
    }

    // 驗證商戶
    function getApplePaySession(url) {

        return new Promise(function (resolve, reject) {

            var xhr = new XMLHttpRequest();
            xhr.open('POST', '/applepay/ValidateMerchant');
            xhr.onload = function () {
                if (this.status >= 200 && this.status < 300) {
                    resolve(JSON.parse(xhr.response));
                } else {
                    reject({
                        status: this.status,
                        statusText: xhr.statusText
                    });
                }
            };

            xhr.onerror = function () {
                reject({
                    status: this.status,
                    statusText: xhr.statusText
                });
            };

            xhr.setRequestHeader("Content-Type", "application/json");
            xhr.send(JSON.stringify({ validationUrl: url }));
        });
    }

    // 付款
    function paymentProcess(data) {
        return new Promise(function (resolve, reject) {

            var xhr = new XMLHttpRequest();
            xhr.open('POST', '/applepay/paymentProcess');
            xhr.onload = function () {
                if (this.status >= 200 && this.status < 300) {
                    debugger
                    resolve(JSON.parse(xhr.response));
                } else {
                    reject({
                        status: this.status,
                        statusText: xhr.statusText
                    });
                }
            };

            xhr.onerror = function () {
                reject({
                    status: this.status,
                    statusText: xhr.statusText
                });
            };

            xhr.setRequestHeader("Content-Type", "application/json");
            xhr.send(JSON.stringify(data));
        });
    }
</script>

後端程式 ( NET MVC)

 /// <summary>
      /// 商戶驗證
      /// </summary>
      [HttpPost]
      public JsonResult ValidateMerchant(VerifyMerchantRequest request) {
         string strResult = string.Empty;
         try {
            
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
            ServicePointManager.Expect100Continue = false;

            System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12;
            /* Merchant Identity憑證 */
            string certPath = Request.MapPath(@"~/App_Data/ApplePay.p12"); //Merchant Identifier憑證路徑
            string certPwd = "123"; //Merchant Identifier憑證密碼
            X509Certificate2 cert = new X509Certificate2(certPath, certPwd, X509KeyStorageFlags.MachineKeySet);

            /* 建立PayLoad */
            var payload = new {
               displayName = "letgo",  // 名稱
               initiative = "web", // 網頁
               initiativeContext = "adm.letgo.com.tw", // 域名
               merchantIdentifier = "merchant.letgo.com.tw.testPayment", // 商戶號
            };

            string strPayLoad = JsonConvert.SerializeObject(payload);

            /* 將Payload以POST方式拋送至Apple提供的validationURL */
            /* HTTP Request需以Merchant Identity憑證送出 */
            /* 驗證成功後,Apple將會回傳Merchant Session物件*/

            #region HTTP Web Result

            HttpWebRequest httpRequest = (HttpWebRequest)HttpWebRequest.Create(request.ValidationUrl);

            httpRequest.Method = WebRequestMethods.Http.Post;
            httpRequest.ContentType = "application/json";
            httpRequest.ContentLength = strPayLoad.Length;

            httpRequest.ClientCertificates.Add(cert);

            using (StreamWriter sw = new StreamWriter(httpRequest.GetRequestStream())) {
               sw.Write(strPayLoad);
               sw.Flush();
               sw.Close();
            }

            HttpWebResponse response = httpRequest.GetResponse() as HttpWebResponse;

            using (StreamReader sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8)) {
               strResult = sr.ReadToEnd();
               sr.Close();
            }

            #endregion HTTP Web Result
         }
         catch (Exception ex) {
         }
         finally {
         }

         /* 將Merchant Session物件回應至Client端*/
         return Json(strResult);
      }

      /// <summary>
      /// 付款
      /// </summary>
      /// <param name="paymentProcessRequest"></param>
      /// <returns></returns>
      [HttpPost]
      public async JsonResult PaymentProcess(PaymentProcessRequest)
      {
        // todo 和金流商串接,呼叫你的金流商付款 Api 
      }
   }

界接過程中遇到的問題

後端請求和蘋果在商戶驗證時,出現 The underlying connection was closed: An unexpected error occurred on a send 錯誤

錯誤的憑證蘋果的 Api gateway 不會回應你,請仔細檢查商戶或域名驗證、請求時帶的憑證,傳遞的 Payload 一定要正確。

和 Cybersource 建立訂單時,出現 Invalid_Request,並指定paymentInformation.fluidData.value 欄位錯誤


主因 Cybersource沒有提供 Apple Pay 的測試環境,請直接用正式環境,進行開發。

引入 Apple js 時,專案有使用 Typescript,出現 type script error

npm install @types/applepayjs --save --dev

十分重要!!! 憑證效期只有兩年

依據官方文件蘋果會通知憑證失效,但兩年請重新做一次 p12 及上傳金流商的 CSR image

apple pay 按鈕出來的了,但按了沒回應

  • 小數位一定要小於兩位
  • apple pay js 可能背景做了些什麼,一定得提前載入

apple pay 測試卡號

官方文件

參考資料

Cybersource 交易狀態碼

Apple Pay 官方網站

关于Apple Pay接入和开发,看这一篇就够了

立即富線上金流 Apple Pay 串接文件

Radial Payments & Fraud Documentation

院長的系統開發大小事

參考 Apple React 範例程式

綠界科技 Apple Pay 金流介接 - NET 範例程式

站內付 2.0 - 串接文件


Tags

Mark Ku

Mark Ku

Software Developer

8年以上豐富網站開發經驗,直播系統、POS系統、電子商務、平台網站、SEO、金流串接、DevOps、Infra 出身,帶過幾次團隊,目前專注於北美及德國市場電商網站開發團隊。

Expertise

前端(React)
後端(C#)
網路管理
DevOps
溝通
領導

Social Media

facebook github website

Related Posts

Amazon pay 串接筆記
Amazon pay 串接筆記
March 07, 2024
1 min

Quick Links

關於我

Social Media