kiwix-apple/Model/Payment.swift
Balazs Perlaki-Horvath 6ae88f6e53 Add docs to Payment
2024-11-30 20:03:58 +01:00

210 lines
9.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// This file is part of Kiwix for iOS & macOS.
//
// Kiwix is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// any later version.
//
// Kiwix is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kiwix; If not, see https://www.gnu.org/licenses/.
import Foundation
import PassKit
import SwiftUI
import Combine
import StripeApplePay
import os
/// Payment processing based on:
/// Apple-Pay button:
/// https://developer.apple.com/documentation/passkit_apple_pay_and_wallet/apple_pay#2872687
/// as described in: Whats new in Wallet and Apple Pay from WWDC 2022
/// (https://developer.apple.com/videos/play/wwdc2022/10041/)
///
/// Combined with Stripe's lightweight Apple Pay framework
/// https://github.com/stripe/stripe-ios/blob/master/StripeApplePay/README.md
/// based on the App Clip example project:
/// https://github.com/stripe/stripe-ios/tree/master/Example/AppClipExample
///
/// Whereas the Stripe SDK is based on the older
/// PKPaymentAuthorizationController
/// https://developer.apple.com/documentation/passkit_apple_pay_and_wallet/apple_pay#2870963
///
/// The Stripe SDK has been brought up to date (with 2022 WWDC changes)
/// and modified to be compatible with macOS as well, see SPM dependencies
/// https://github.com/CodeLikeW/stripe-apple-pay
/// https://github.com/CodeLikeW/stripe-core
struct Payment {
/// Decides if the Thank You pop up should be shown
/// - Returns: `True` only once
@MainActor
static func shouldShowThanks() -> Bool {
// make sure `true` is "read only once"
let value = Self.showThanks
Self.showThanks = false
return value
}
@MainActor
static private var showThanks: Bool = false
let completeSubject = PassthroughSubject<Void, Never>()
static let merchantSessionURL = URL(string: "https://apple-pay-gateway.apple.com" )!
static let merchantId = "merchant.org.kiwix.apple"
static let paymentSubscriptionManagingURL = "https://www.kiwix.org"
static let supportedNetworks: [PKPaymentNetwork] = [
.amex,
.discover,
.electron,
.mada,
.maestro,
.masterCard,
.visa
]
static let capabilities: PKMerchantCapability = [.threeDSecure, .credit, .debit, .emv]
/// NOTE: consider that these currencies support double precision, eg: 5.25 USD.
/// Revisit `SelectedAmount`, and `SelectedPaymentAmount`
/// before adding a zero-decimal currency such as: ¥100
static let currencyCodes = ["USD", "EUR", "CHF"]
static let defaultCurrencyCode = "USD"
private static let minimumAmount: Double = 5
/// The Sripe `amount` value supports up to eight digits
/// (e.g., a value of 99999999 for a USD charge of $999,999.99).
/// see: https://docs.stripe.com/api/payment_intents/object#payment_intent_object-amount
static let maximumAmount: Int = 99999999
static func isInValidRange(amount: Double?) -> Bool {
guard let amount else { return false }
return minimumAmount <= amount && amount <= Double(maximumAmount)*100.0
}
static let oneTimes: [AmountOption] = [
.init(value: 10),
.init(value: 34, isAverage: true),
.init(value: 50)
]
static let monthlies: [AmountOption] = [
.init(value: 5),
.init(value: 8, isAverage: true),
.init(value: 10)
]
/// Checks Apple Pay capabilities, and returns the button label accrodingly
/// Setup button if no cards added yet,
/// nil if Apple Pay is not supported
/// or donation button, if all is OK
static func paymentButtonType() -> PayWithApplePayButtonLabel? {
if PKPaymentAuthorizationController.canMakePayments() {
return PayWithApplePayButtonLabel.donate
}
if PKPaymentAuthorizationController.canMakePayments(
usingNetworks: Payment.supportedNetworks,
capabilities: Payment.capabilities) {
return PayWithApplePayButtonLabel.setUp
}
return nil
}
func donationRequest(for selectedAmount: SelectedAmount) -> PKPaymentRequest {
let request = PKPaymentRequest()
request.merchantIdentifier = Self.merchantId
request.merchantCapabilities = Self.capabilities
request.countryCode = "CH"
request.currencyCode = selectedAmount.currency
request.supportedNetworks = Self.supportedNetworks
request.requiredBillingContactFields = [.emailAddress]
let recurring: PKRecurringPaymentRequest? = if selectedAmount.isMonthly {
PKRecurringPaymentRequest(paymentDescription: "payment.description.label".localized,
regularBilling: .init(label: "payment.monthly_support.label".localized,
amount: NSDecimalNumber(value: selectedAmount.value),
type: .final),
managementURL: URL(string: Self.paymentSubscriptionManagingURL)!)
} else {
nil
}
request.recurringPaymentRequest = recurring
request.paymentSummaryItems = [
PKPaymentSummaryItem(
label: "payment.summary.title".localized,
amount: NSDecimalNumber(value: selectedAmount.value),
type: .final
)
]
return request
}
func onPaymentAuthPhase(selectedAmount: SelectedAmount,
phase: PayWithApplePayButtonPaymentAuthorizationPhase) {
switch phase {
case .willAuthorize:
os_log("onPaymentAuthPhase: .willAuthorize")
case .didAuthorize(let payment, let resultHandler):
os_log("onPaymentAuthPhase: .didAuthorize")
// call our server to get payment / setup intent and return the client.secret
Task { @MainActor [resultHandler] in
let paymentServer = StripeKiwix(endPoint: URL(string: "https://api.donation.kiwix.org/v1/stripe")!,
payment: payment)
do {
let publicKey = try await paymentServer.publishableKey()
StripeAPI.defaultPublishableKey = publicKey
} catch let serverError {
resultHandler(.init(status: .failure, errors: [serverError]))
return
}
// we should update the return path for confirmations
// see: https://github.com/kiwix/kiwix-apple/issues/1032
let stripe = StripeApplePaySimple()
let result = await stripe.complete(payment: payment,
returnURLPath: nil,
usingClientSecretProvider: {
await paymentServer.clientSecretForPayment(selectedAmount: selectedAmount)
})
// calling any UI refreshing state / subject from here
// will block the UI in the payment state forever
// therefore it's defered via static showThanks
Self.showThanks = result.status == .success
resultHandler(result)
os_log("onPaymentAuthPhase: .didAuthorize: \(result.status == .success)")
}
case .didFinish:
os_log("onPaymentAuthPhase: .didFinish")
completeSubject.send(())
@unknown default:
os_log("onPaymentAuthPhase: @unknown default")
}
}
@available(macOS 13.0, *)
func onMerchantSessionUpdate() async -> PKPaymentRequestMerchantSessionUpdate {
var request = URLRequest(url: Self.merchantSessionURL)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200..<300).contains(httpResponse.statusCode),
let dict = try JSONSerialization.jsonObject(with: data,
options: .allowFragments) as? [String: Any] else {
throw MerchantSessionError.invalidStatus
}
let session = PKPaymentMerchantSession(dictionary: dict)
return .init(status: .success, merchantSession: session)
} catch let error {
os_log("Merchant session not established: %@", type: .debug, error.localizedDescription)
return .init(status: .failure, merchantSession: nil)
}
}
}
private enum MerchantSessionError: Error {
case invalidStatus
}