Skip to content

Commit

Permalink
Merge pull request #57 from AppSci/feature/ApplePay
Browse files Browse the repository at this point in the history
Feature/apple pay
  • Loading branch information
yehorkyrylov authored Sep 9, 2022
2 parents ec2589b + 21efd80 commit 3444a54
Show file tree
Hide file tree
Showing 13 changed files with 673 additions and 48 deletions.
82 changes: 79 additions & 3 deletions Sources/PandaSDK/ConfiguredPanda.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@

import Foundation
import UIKit
import Combine
import PassKit

protocol VerificationClient {
func verifySubscriptions(user: PandaUser, receipt: String, source: PaymentSource?, retries: Int, callback: @escaping (Result<ReceiptVerificationResult, Error>) -> Void)
func verifyApplePayRequest(user: PandaUser, paymentData: Data, productId: String, webAppId: String, callback: @escaping (Result<ApplePayResult, Error>) -> Void)
}

final public class Panda: PandaProtocol, ObserverSupport {
Expand All @@ -24,6 +27,7 @@ final public class Panda: PandaProtocol, ObserverSupport {
}

let networkClient: NetworkClient
let applePayPaymentHandler: ApplePayPaymentHandler
let cache: ScreenCache = ScreenCache()
let user: PandaUser
let appStoreClient: AppStoreClient
Expand All @@ -47,13 +51,58 @@ final public class Panda: PandaProtocol, ObserverSupport {
let device = deviceStorage.fetch() ?? DeviceSettings.default
return device.customUserId
}

init(user: PandaUser, networkClient: NetworkClient, appStoreClient: AppStoreClient) {
public var webAppId: String?
public var applePayOutputPublisher: AnyPublisher<ApplePayResult, Error>?

init(
user: PandaUser,
networkClient: NetworkClient,
appStoreClient: AppStoreClient,
applePayPaymentHandler: ApplePayPaymentHandler,
webAppId: String?
) {
self.user = user
self.networkClient = networkClient
self.appStoreClient = appStoreClient
self.verificationClient = networkClient
self.applePayPaymentHandler = applePayPaymentHandler
self.pandaUserId = user.id
self.webAppId = webAppId
bindApplePayMessages()
}

private func bindApplePayMessages() {
self.applePayOutputPublisher = applePayPaymentHandler.outputPublisher
.flatMap { message in
Future<ApplePayResult, Error> { [weak self] promise in
switch message {
case .failedToPresentPayment:
promise(.failure(ApplePayVerificationError.init(message: "failed to present apple pay screen")))
case let .paymentFinished(status, productId, paymentData):
guard
let self = self,
let webAppId = self.webAppId,
status == PKPaymentAuthorizationStatus.success
else {
promise(.failure(ApplePayVerificationError.init(message: "Payment finished unsuccessfully")))
return
}

self.verificationClient.verifyApplePayRequest(
user: self.user,
paymentData: paymentData,
productId: productId,
webAppId: webAppId) { result in
DispatchQueue.main.async { [weak self] in
self?.viewControllers.forEach({ $0.value?.tryAutoDismiss()})
promise(result)
}
}
}
}
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}

internal func configureAppStoreClient() {
Expand All @@ -78,7 +127,7 @@ final public class Panda: PandaProtocol, ObserverSupport {
observers.removeValue(forKey: ObjectIdentifier(observer))
}

public func configure(apiKey: String, isDebug: Bool, callback: ((Bool) -> Void)?) {
public func configure(apiKey: String, isDebug: Bool, applePayConfiguration: ApplePayConfiguration?, webAppId: String?, callback: ((Bool) -> Void)?) {
pandaLog("Already configured")
callback?(true)
}
Expand Down Expand Up @@ -357,6 +406,33 @@ final public class Panda: PandaProtocol, ObserverSupport {
self.send(feedback: text, at: screenId)
}
}
viewModel.onApplePayPurchase = { [applePayPaymentHandler, weak self] billingID, source, screenId, screenName, _ in
guard let billingID = billingID else {
pandaLog("Missing productId with source: \(source)")
return
}
pandaLog("purchaseStarted: \(billingID) \(screenName) \(screenId)")
self?.send(event: .purchaseStarted(screenId: screenId, screenName: screenName, productId: billingID, source: entryPoint))

self?.networkClient.getBillingPlan(
bilingID: billingID,
callback: { result in
switch result {
case let .success(billingPlan):
applePayPaymentHandler.startPayment(
with: billingPlan.getLabelForApplePayment(),
price: billingPlan.getPrice(),
currency: billingPlan.currency,
productId: billingPlan.id,
countryCode: billingPlan.countryCode
)
case let .failure(error):
pandaLog("Failed get billingPlan Error: \(error)")
}
}
)

}
viewModel.onPurchase = { [appStoreClient, weak self] productId, source, _, screenId, screenName, course in
guard let productId = productId else {
pandaLog("Missing productId with source: \(source)")
Expand Down
34 changes: 34 additions & 0 deletions Sources/PandaSDK/Extensions/Extension+Float.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// File.swift
//
//
// Created by Yegor Kyrylov on 08.09.2022.
//

import Foundation

public extension Float {

/// Returns rounded number that will have 1 or 2 digits after the dot. Example: 13.21
///
/// - Returns: maths rounded number to hundredths
func roundedToHundredths() -> Float {
return Float((100 * self).rounded() / 100)
}

}

public struct MonetaryAmount: Equatable {
public let amountCents: Int
public let amountDollars: Float

public init(amountCents: Int) {
self.init(amountDollars: Float(amountCents) / 100)
}

public init(amountDollars: Float) {
let adjustedAmount = amountDollars.roundedToHundredths()
self.amountCents = Int((adjustedAmount * 100).rounded())
self.amountDollars = adjustedAmount
}
}
47 changes: 47 additions & 0 deletions Sources/PandaSDK/Helpers/CurrencyHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// File.swift
//
//
// Created by Yegor Kyrylov on 09.09.2022.
//

import Foundation

enum CurrencyHelper {
static func getSymbolForCurrencyCode(code: String) -> String {
var candidates: [String] = []
let locales: [String] = Locale.availableIdentifiers
for localeID in locales {
guard let symbol = findMatchingSymbol(localeID: localeID, currencyCode: code) else {
continue
}
if symbol.count == 1 {
return symbol
}
candidates.append(symbol)
}
let sorted = sortAscByLength(list: candidates)
if sorted.count < 1 {
return ""
}
return sorted[0]
}

private static func findMatchingSymbol(localeID: String, currencyCode: String) -> String? {
let locale = Locale(identifier: localeID as String)
guard let code = locale.currencyCode else {
return nil
}
if code != currencyCode {
return nil
}
guard let symbol = locale.currencySymbol else {
return nil
}
return symbol
}

private static func sortAscByLength(list: [String]) -> [String] {
return list.sorted(by: { $0.count < $1.count })
}
}
16 changes: 16 additions & 0 deletions Sources/PandaSDK/InApp/ApplePayConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// ApplePayConfiguration.swift
//
//
// Created by Roman Mishchenko on 02.06.2022.
//

import Foundation

public struct ApplePayConfiguration {
let merchantIdentifier: String

public init(merchantIdentifier: String) {
self.merchantIdentifier = merchantIdentifier
}
}
110 changes: 110 additions & 0 deletions Sources/PandaSDK/InApp/ApplePayPaymentHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//
// ApplePayPaymentHandler.swift
//
//
// Created by Roman Mishchenko on 02.06.2022.
//

import Foundation
import PassKit
import Combine

public enum ApplePayPaymentHandlerOutputMessage {
case failedToPresentPayment
case paymentFinished(_ status: PKPaymentAuthorizationStatus, _ productId: String, _ paymentData: Data)
}

final class ApplePayPaymentHandler: NSObject {

private var paymentController: PKPaymentAuthorizationController?
private var paymentSummaryItems = [PKPaymentSummaryItem]()
private var paymentStatus = PKPaymentAuthorizationStatus.failure
private var productId: String? = nil
private var paymentData: Data? = nil
private let configuration: ApplePayConfiguration

let outputPublisher: AnyPublisher<ApplePayPaymentHandlerOutputMessage, Error>
private let outputSubject = PassthroughSubject<ApplePayPaymentHandlerOutputMessage, Error>()

init(configuration: ApplePayConfiguration) {
self.configuration = configuration
self.outputPublisher = outputSubject.eraseToAnyPublisher()
}

static let supportedNetworks: [PKPaymentNetwork] = [
.amex,
.masterCard,
.visa,
.discover
]

class func applePayStatus() -> (canMakePayments: Bool, canSetupCards: Bool) {
return (PKPaymentAuthorizationController.canMakePayments(),
PKPaymentAuthorizationController.canMakePayments(usingNetworks: supportedNetworks))
}

func startPayment(with label: String, price: String?, currency: String?, productId: String?, countryCode: String?) {
guard
let price = price,
let currency = currency,
let productId = productId,
let countryCode = countryCode
else {
self.outputSubject.send(.failedToPresentPayment)
return
}

let product = PKPaymentSummaryItem(label: label, amount: NSDecimalNumber(string: price), type: .final)

paymentSummaryItems = [product]

// Create a payment request.
let paymentRequest = PKPaymentRequest()
paymentRequest.paymentSummaryItems = paymentSummaryItems
paymentRequest.merchantIdentifier = configuration.merchantIdentifier
paymentRequest.merchantCapabilities = .capability3DS

paymentRequest.countryCode = countryCode
paymentRequest.currencyCode = currency
paymentRequest.supportedNetworks = ApplePayPaymentHandler.supportedNetworks

// Display the payment request.
paymentController = PKPaymentAuthorizationController(paymentRequest: paymentRequest)
paymentController?.delegate = self

paymentController?.present(completion: { presented in
if !presented {
self.outputSubject.send(.failedToPresentPayment)
} else {
self.productId = productId
}
})
}
}

extension ApplePayPaymentHandler: PKPaymentAuthorizationControllerDelegate {
func paymentAuthorizationController(
_ controller: PKPaymentAuthorizationController,
didAuthorizePayment payment: PKPayment,
handler completion: @escaping (PKPaymentAuthorizationResult) -> Void
) {
paymentStatus = .success
paymentData = payment.token.paymentData
completion(PKPaymentAuthorizationResult(status: .success, errors: []))
}

func paymentAuthorizationControllerDidFinish(_ controller: PKPaymentAuthorizationController) {
controller.dismiss {
// The payment sheet doesn't automatically dismiss once it has finished. Dismiss the payment sheet.
DispatchQueue.main.async {
guard
let paymentData = self.paymentData,
let productId = self.productId
else {
return
}
self.outputSubject.send(.paymentFinished(self.paymentStatus, productId, paymentData))
}
}
}
}
47 changes: 47 additions & 0 deletions Sources/PandaSDK/Models/ApplePayPayment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// ApplePayPayment.swift
//
//
// Created by Roman Mishchenko on 02.06.2022.
//

import Foundation

struct ApplePayPaymentInfo: Codable {
let header: PaymnetHeader
let data: String
let signature: String
let version: String
}

struct PaymnetHeader: Codable {
let ephemeralPublicKey: String
let publicKeyHash: String
let transactionId: String
}

struct ApplePayPayment: Codable {
let data: String
let ephemeralPublicKey: String
let publicKeyHash: String
let transactionId: String
let signature: String
let version: String
let sandbox: Bool
let webAppId: String
let productId: String
let userId: String

enum CodingKeys: String, CodingKey {
case data
case ephemeralPublicKey = "ephemeral_public_key"
case publicKeyHash = "public_key_hash"
case transactionId = "transaction_id"
case signature
case version
case sandbox
case webAppId = "web_app_id"
case productId = "product_id"
case userId = "user_id"
}
}
Loading

0 comments on commit 3444a54

Please sign in to comment.