時空背景
因工作需要美國及德國電商網站,需要界接 Apple Pay。
Apple Pay 原理
美國有多少人口使用 Apple Pay
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
apple pay 按鈕出來的了,但按了沒回應
- 小數位一定要小於兩位
- apple pay js 可能背景做了些什麼,一定得提前載入