iOS Stripe 支付(綁卡,Apple Pay)、退款、提現(xiàn)集成總結(jié)

image-20201115202529056

前言

Stripe 是一家國外的提供支付服務(wù)的平臺,可以讓商戶在自己的應(yīng)用和網(wǎng)站方便快捷地集成信用卡支付、第三方(Apple Pay、Alipay、微信pay、三星pay、微軟pay 等)支付方式等服務(wù),具體參考官網(wǎng)介紹。

image-20201115163806075

本文主要記錄總結(jié)一下在 iOS 平臺如何集成 Stripe 相關(guān)的服務(wù)。主要包含一下內(nèi)容:

  • 申請賬號,獲得測試秘鑰和生產(chǎn)秘鑰
  • 信用卡支付
    • 簡單支付
    • 綁卡支付
  • Apple Pay
  • 模擬測試
  • 提現(xiàn)
  • Stripe 管理后臺介紹
  • 注意事項(xiàng)

申請賬號

Stripe 官網(wǎng) -> 管理平臺 -> 注冊賬號 -> 登錄 -> 獲得測試秘鑰和生產(chǎn)秘鑰

image-20201115164323411

信用卡支付

集成 Stripe iOS SDK ,參考 github 上的項(xiàng)目 https://github.com/stripe/stripe-ios 和支付文檔 https://stripe.com/docs/connect/creating-a-payments-page

pod 'Stripe'

簡單支付

簡單支付直接可以使用 Stripe 封裝好的一個(gè) STPPaymentCardTextField 輸入框,添加支付頁面的 UI 上,Stripe 處理好了信用卡格式校驗(yàn),安全傳輸敏感數(shù)據(jù)等事情。

參考 https://github.com/stripe-samples/accept-a-card-payment,有兩種模式,帶 webhook 的和不帶 webhook 的,區(qū)別如下:

image-20201115180303520
// 1. 初始化填寫信用卡的輸入框
lazy var paymentCardTextField: STPPaymentCardTextField = {
    let textField = STPPaymentCardTextField()
    textField.isHidden = false
    textField.delegate = self
    
    // 可定制 TextField 的主題外觀
    textField.backgroundColor = STPTheme.default().secondaryBackgroundColor
    textField.textColor = STPTheme.default().primaryForegroundColor
    textField.placeholderColor = STPTheme.default().secondaryForegroundColor
    textField.borderColor = UIColor.gxd_themeColor()
    textField.borderWidth = 1.0
    textField.textErrorColor = STPTheme.default().errorColor
    textField.postalCodeEntryEnabled = true

    return textField
}()

// 2. 設(shè)置可發(fā)布的秘鑰:從 Stripe 管理后臺拿到,也可以又服務(wù)器保存,然后下發(fā)到客戶端
Stripe.setDefaultPublishableKey("\(PublishableKey)")

// 3. 向服務(wù)端發(fā)起一個(gè)預(yù)支付請求,服務(wù)端通過調(diào)用 Stripe 的 API 生成 paymentIntent對象,下發(fā) paymentIntentClientSecret 到客戶端
func startCheckout() {
    // Create a PaymentIntent by calling the sample server's /create-payment-intent endpoint.
    let url = URL(string: BackendUrl + "create-payment-intent")!
    let json: [String: Any] = [
        "currency": "usd",
        "items": [
            ["id": "photo_subscription"]
        ]
    ]
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try? JSONSerialization.data(withJSONObject: json)
    let task = URLSession.shared.dataTask(with: request, completionHandler: { [weak self] (data, response, error) in
        guard let response = response as? HTTPURLResponse,
            response.statusCode == 200,
            let data = data,
            let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any],
            let clientSecret = json["clientSecret"] as? String,
            let publishableKey = json["publishableKey"] as? String else {
                let message = error?.localizedDescription ?? "Failed to decode response from server."
                self?.displayAlert(title: "Error loading page", message: message)
                return
        }
        print("Created PaymentIntent")
        self?.paymentIntentClientSecret = clientSecret
        // Configure the SDK with your Stripe publishable key so that it can make requests to the Stripe API
        // For added security, our sample app gets the publishable key from the server
        Stripe.setDefaultPublishableKey(publishableKey)
    })
    task.resume()
}

// 4. 收集用戶輸入的信用卡信息,客戶端拿到這個(gè) clientSecret,發(fā)起支付請求
guard let paymentIntentClientSecret = paymentIntentClientSecret else {
    return;
}

// Collect card details
let cardParams = cardTextField.cardParams
let paymentMethodParams = STPPaymentMethodParams(card: cardParams, billingDetails: nil, metadata: nil)
let paymentIntentParams = STPPaymentIntentParams(clientSecret: paymentIntentClientSecret)
paymentIntentParams.paymentMethodParams = paymentMethodParams

// Submit the payment
let paymentHandler = STPPaymentHandler.shared()
paymentHandler.confirmPayment(withParams: paymentIntentParams, authenticationContext: self) { (status, paymentIntent, error) in
    switch (status) {
    case .failed:
        self.displayAlert(title: "Payment failed", message: error?.localizedDescription ?? "")
        break
    case .canceled:
        self.displayAlert(title: "Payment canceled", message: error?.localizedDescription ?? "")
        break
    case .succeeded:
        self.displayAlert(title: "Payment succeeded", message: paymentIntent?.description ?? "", restartDemo: true)
        break
    @unknown default:
        fatalError()
        break
    }
}

// 有些卡需要用戶二次驗(yàn)證授權(quán),提供一個(gè)代理,彈出讓用戶確認(rèn)驗(yàn)證的控制器
extension CheckoutViewController: STPAuthenticationContext {
    func authenticationPresentingViewController() -> UIViewController {
        return self
    }
}

綁卡支付

綁卡支付要必要每次用戶都需要輸入一遍卡號的問題,就是記錄用戶之前支付過的卡,和某個(gè)具體用戶關(guān)聯(lián)起來,以后該用戶發(fā)起支付時(shí)候就可以選擇自己以前綁定過的卡。通過 Stripe 提供的 API,用戶可以對信用卡做新增、刪除、修改等操作。

這些在 Stripe iOS SDK 提供的 UI 素材里面已經(jīng)默認(rèn)實(shí)現(xiàn)了。服務(wù)端借助 heroku ,可以將 https://github.com/stripe/example-mobile-backend/tree/v18.1.0 后端服務(wù)替換我們自己在 Stripe 上新建的賬號秘鑰,部署運(yùn)行。

image-20201115181042041
image-20201115181158482
image-20201115181525408

我們設(shè)置的 key 將會在服務(wù)端代碼里面用來調(diào)用 Stripe 的 API。實(shí)例代碼服務(wù)端是用 Ruby 寫的,代碼在這里:https://github.com/stripe/example-mobile-backend/blob/v18.1.0/web.rb, 服務(wù)端可以參考一下里面的邏輯。

客戶端集成流程如下:

一、向服務(wù)端請求用戶的綁卡信息
// 請求服務(wù)端,查詢這個(gè)用戶是否有創(chuàng)建過 Stripe Customer 用戶,如果有,返回該 Stripe Customer 用戶的綁卡信息(綁了多少張卡,選中的支付方式是哪個(gè)),沒有就創(chuàng)建一個(gè)新的
// 這是個(gè)代理方法,是由 Stripe 代理
func createCustomerKey(withAPIVersion apiVersion: String, completion: @escaping STPJSONResponseCompletionBlock) {
    let url = self.baseURL.appendingPathComponent("ephemeral_keys")
    var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)!
    urlComponents.queryItems = [URLQueryItem(name: "api_version", value: apiVersion)]
    var request = URLRequest(url: urlComponents.url!)
    request.httpMethod = "POST"
    let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
        guard let response = response as? HTTPURLResponse,
            response.statusCode == 200,
            let data = data,
            let json = ((try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any]) as [String : Any]??) else {
            completion(nil, error)
            return
        }
        completion(json, nil)
    })
    task.resume()
}
image-20201115182952085

通過向服務(wù)端拿到的 Stripe iOS SDK 能識別的用戶信息,就能夠獲取該用戶所關(guān)聯(lián)的支付方式相關(guān)信息(包含綁定的卡和其他第三方支付方式)

image-20201115183323087
二、客戶端發(fā)起預(yù)支付請求
func createPaymentIntent(products: [Product], shippingMethod: PKShippingMethod?, country: String? = nil, completion: @escaping ((Result<String, Error>) -> Void)) {
    let url = self.baseURL.appendingPathComponent("create_payment_intent")
    var params: [String: Any] = [
        "metadata": [
            // example-mobile-backend allows passing metadata through to Stripe
            "payment_request_id": "B3E611D1-5FA1-4410-9CEC-00958A5126CB",
        ],
    ]
    params["products"] = products.map({ (p) -> String in
        return p.emoji
    })
    if let shippingMethod = shippingMethod {
        params["shipping"] = shippingMethod.identifier
    }
    params["country"] = country
    let jsonData = try? JSONSerialization.data(withJSONObject: params)
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = jsonData
    let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
        guard let response = response as? HTTPURLResponse,
            response.statusCode == 200,
            let data = data,
            let json = ((try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any]) as [String : Any]??),
            let secret = json?["secret"] as? String else {
                completion(.failure(error ?? APIError.unknown))
                return
        }
        completion(.success(secret))
    })
    task.resume()
}
image-20201115183816384

就是客戶端簡單支付里面服務(wù)端下發(fā)的 paymentIntentClientSecret,客戶端拿到這個(gè),就可以發(fā)起支付請求了。

// 1.-------------------------- 初始化 STPPaymentContext 上下文 --------------------------

// 設(shè)置服務(wù)端的請求的地址
MyAPIClient.sharedClient.baseURLString = self.backendBaseURL


// 設(shè)置可發(fā)布的秘鑰
Stripe.setDefaultPublishableKey(self.stripePublishableKey)

// 支付相關(guān)配置
let config = STPPaymentConfiguration.shared()
// 設(shè)置 Apple Pay 配置的商戶 ID
config.appleMerchantIdentifier = self.appleMerchantID

// 設(shè)置支付方式額外選項(xiàng),是一個(gè)選項(xiàng)枚舉,指定支付方式包含哪些(只是信用卡、還是既有信用卡還寶行 Apple Pay)
// 注意的是:就算這里制定了 Apple Pay,如果 Apple Pay 的相關(guān)配置沒有檢測通過,真實(shí)出現(xiàn)的時(shí)候也是沒有 Apple Pay 這種支付方式的
config.additionalPaymentOptions = settings.additionalPaymentOptions
// 是否支持可以掃描添加卡
config.cardScanningEnabled = true

// 支付的幣種
self.paymentCurrency = settings.currency

let customerContext = STPCustomerContext(keyProvider: MyAPIClient.sharedClient)
let paymentContext = STPPaymentContext(customerContext: customerContext,
                                       configuration: config,
                                       theme: settings.theme)

paymentContext.paymentCurrency = self.paymentCurrency


// 2.------------------ 發(fā)起支付 ------------------------------------
self.paymentContext.requestPayment()


// 3. -- ------------------------- STPPaymentContextDelegate 回調(diào) -----------------
func paymentContext(_ paymentContext: STPPaymentContext, didCreatePaymentResult paymentResult: STPPaymentResult, completion: @escaping STPPaymentStatusBlock) {
     // Create the PaymentIntent on the backend
     // To speed this up, create the PaymentIntent earlier in the checkout flow and update it as necessary (e.g. when the cart subtotal updates or when shipping fees and taxes are calculated, instead of re-creating a PaymentIntent for every payment attempt.
    

}

// 支付完成回調(diào)
func paymentContext(_ paymentContext: STPPaymentContext, didFinishWith status: STPPaymentStatus, error: Error?) {

}

// 支付上下文發(fā)生改變回調(diào)
func paymentContextDidChange(_ paymentContext: STPPaymentContext) {

}

// 支付錯(cuò)誤回調(diào)
func paymentContext(_ paymentContext: STPPaymentContext, didFailToLoadWithError error: Error) {

}

Apple Pay

Apple Pay 只是支付方式的一種,Stripe 和綁卡支付封裝在一起,只需要做一些相關(guān)的配置。參考 https://stripe.com/docs/apple-pay,相關(guān)需要配置的地方如下:

一、Stripe 后臺下載 cert 文件

image-20200519164345682

下載完:

image-20200519164533332

二、蘋果開發(fā)者網(wǎng)站,新建 Merchant IDs 類型 Identifiers

上傳第一步中從 Stripe 后臺獲取到的 certSingingRequest 文件

image-20200519164839049
image-20200519165315131

下載下來:

image-20200519165436648

三、上傳上一步中下載好的證書

image-20200519165609427

四、Xcode -> workspace -> project -> target -> Singing & Capabilities 中新增 Apple Pay

image-20200519165840480

五、Apple develop 后臺,將需要支持 Apple pay 的應(yīng)用 ID,編輯勾選 Apple Pay 能力,將第二步中新增的 Merchant ID 編輯進(jìn)去

image-20200519170621588

注意事項(xiàng):

  1. 測試模式下(即 Stripe 后臺使用的是測試模式可發(fā)布的秘鑰),需在沙盒環(huán)境下模擬,添加沙盒測試員;
  2. 如果配置的支付環(huán)境證書是在國外,那么測試的時(shí)候需要將測試手機(jī)的手機(jī) Region 設(shè)置成該國家,不然會一直 processing 轉(zhuǎn)圈支付失??;

提現(xiàn)

提現(xiàn)是商戶平臺的客戶向該商戶發(fā)起提現(xiàn)申請,商戶通過 Stripe 提供的 API 從商戶賬戶里面的錢轉(zhuǎn)賬到商戶平臺的客戶,但是客戶也需要有一個(gè) Stripe 的賬號。流程大概如下:

一、商戶去到 Stripe 管理后臺設(shè)置賬號類型

有三種類型:標(biāo)準(zhǔn)版、Express、Custom,不同的賬號類型區(qū)別如下:

<img src="https://image-1254431338.cos.ap-guangzhou.myqcloud.com/2020-11-15-111431.png" alt="image-20201115191430891" style="zoom:50%;" />

<img src="https://image-1254431338.cos.ap-guangzhou.myqcloud.com/2020-11-15-111712.png" alt="image-20201115191712038" style="zoom:50%;" />

二、商戶去到 Stripe 管理后臺設(shè)置用戶注冊 Stripe 的 OAuth 賬戶類型和重定向地址

<img src="https://image-1254431338.cos.ap-guangzhou.myqcloud.com/2020-11-15-121817.png" alt="image-20201115201817482" style="zoom:50%;" />

三、客戶端向服務(wù)端發(fā)起提現(xiàn)請求

  1. 判斷用戶是否已經(jīng)授權(quán)過,如果已經(jīng)授權(quán)過,就已經(jīng)存在 Stripe Account,直接將需要提現(xiàn)的金額上報(bào)給服務(wù)端,由服務(wù)端直接向該 Stripe 賬戶轉(zhuǎn)賬就行;
  2. 如果用戶沒有授權(quán),用戶需要走 OAuth 授權(quán)注冊流程,彈出授權(quán)網(wǎng)頁,讓用戶去注冊 Stripe 賬號,綁定自己需要提現(xiàn)的信用卡信息;
  3. 用戶注冊完成后會回調(diào)之前在 Stripe 管理后臺配置的重定向地址,并攜帶回到參數(shù) code 和 state;
  4. 客戶端拿到重定向地址后面回調(diào)回來的參數(shù) code 和 state,上報(bào)給服務(wù)端,服務(wù)端就能通過 code 查到剛才注冊用戶的 Stripe 賬號信息,服務(wù)端向該用戶轉(zhuǎn)賬,即用戶完成提現(xiàn);
// 1. 拼接授權(quán) URl 
// clientId 是商戶在 Stripe 管理后臺的唯一 id,和 redirectUri 一樣都是在商戶 Stripe 管理后臺可以查到的
let authorizeURL = "https://connect.stripe.com/express/oauth/authorize" + "?" + "redirect_uri=\(redirectUri)&" + "client_id=\(clientId)&" + "state=\(state)&" + "stripe_user[business_type]=individual"



// 2. 根據(jù)WebView對于即將跳轉(zhuǎn)的HTTP請求頭信息和相關(guān)信息來決定是否跳轉(zhuǎn),獲取用戶注冊授權(quán)完成回傳的 code 和 state
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    guard let urlString = navigationAction.request.url?.absoluteString,
        let redirectUrl = model?.redirectUri else {

        decisionHandler(WKNavigationActionPolicy.cancel)
        return
    }

    if urlString.hasPrefix(redirectUrl) {
        print("發(fā)生重定向請求:\(urlString)")

        // 截取出回調(diào)后面的參數(shù) code,state
        guard let queryParames = navigationAction.request.url?.queryParameters else {
            decisionHandler(WKNavigationActionPolicy.cancel)
            return
        }

        let code = queryParames.filter{ $0.key == "code" }.first?.value ?? ""
        let state = queryParames.filter{ $0.key == "state" }.first?.value ?? ""

        decisionHandler(WKNavigationActionPolicy.cancel)
    } else {
        decisionHandler(WKNavigationActionPolicy.allow)
    }
}

Stripe 后臺

支付記錄,查看詳細(xì)的支付信息

image-20201115195926440

參考鏈接

  1. https://stripe.com/docs/apple-pay
  2. https://developer.apple.com/apple-pay/sandbox-testing/
  3. https://github.com/stripe/example-mobile-backend/tree/v18.1.0
  4. https://github.com/stripe/stripe-ios
  5. https://github.com/stripe-samples/accept-a-card-payment
  6. https://stripe.com/docs/connect/express-accounts
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容