diff --git a/Sources/PandaSDK/ConfiguredPanda.swift b/Sources/PandaSDK/ConfiguredPanda.swift index 264378e..5af436f 100644 --- a/Sources/PandaSDK/ConfiguredPanda.swift +++ b/Sources/PandaSDK/ConfiguredPanda.swift @@ -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) -> Void) + func verifyApplePayRequest(user: PandaUser, paymentData: Data, productId: String, webAppId: String, callback: @escaping (Result) -> Void) } final public class Panda: PandaProtocol, ObserverSupport { @@ -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 @@ -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? + + 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 { [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() { @@ -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) } @@ -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)") diff --git a/Sources/PandaSDK/Extensions/Extension+Float.swift b/Sources/PandaSDK/Extensions/Extension+Float.swift new file mode 100644 index 0000000..812d066 --- /dev/null +++ b/Sources/PandaSDK/Extensions/Extension+Float.swift @@ -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 + } +} diff --git a/Sources/PandaSDK/Helpers/CurrencyHelper.swift b/Sources/PandaSDK/Helpers/CurrencyHelper.swift new file mode 100644 index 0000000..ace584f --- /dev/null +++ b/Sources/PandaSDK/Helpers/CurrencyHelper.swift @@ -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 }) + } +} diff --git a/Sources/PandaSDK/InApp/ApplePayConfiguration.swift b/Sources/PandaSDK/InApp/ApplePayConfiguration.swift new file mode 100644 index 0000000..3c58c66 --- /dev/null +++ b/Sources/PandaSDK/InApp/ApplePayConfiguration.swift @@ -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 + } +} diff --git a/Sources/PandaSDK/InApp/ApplePayPaymentHandler.swift b/Sources/PandaSDK/InApp/ApplePayPaymentHandler.swift new file mode 100644 index 0000000..9445000 --- /dev/null +++ b/Sources/PandaSDK/InApp/ApplePayPaymentHandler.swift @@ -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 + private let outputSubject = PassthroughSubject() + + 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)) + } + } + } +} diff --git a/Sources/PandaSDK/Models/ApplePayPayment.swift b/Sources/PandaSDK/Models/ApplePayPayment.swift new file mode 100644 index 0000000..e1d86e4 --- /dev/null +++ b/Sources/PandaSDK/Models/ApplePayPayment.swift @@ -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" + } +} diff --git a/Sources/PandaSDK/Models/BillingPlan.swift b/Sources/PandaSDK/Models/BillingPlan.swift new file mode 100644 index 0000000..0d7d551 --- /dev/null +++ b/Sources/PandaSDK/Models/BillingPlan.swift @@ -0,0 +1,95 @@ +// +// File.swift +// +// +// Created by Yegor Kyrylov on 07.09.2022. +// + +import Foundation + +struct BillingPlan: Codable { + let id: String? + let countryCode: String? + let productID: String? + let currency: String? + let amount: Int? + let firstPayment: Int? + let secondPayment: Int? + let paymentMode: String? + let subscriptionType: String? + let trialDescription: String? + let billingPeriod: String? + let billingPeriodInDays: Int? + let orderDescription: String? + let hasTrial: Bool? + let durationTrialInDays: Int? + + enum CodingKeys: String, CodingKey { + case id + case amount + case billingPeriod = "billing_period" + case billingPeriodInDays = "billing_period_in_days" + case currency + case durationTrialInDays = "duration_trial_in_days" + case firstPayment = "first_payment" + case hasTrial = "has_trial" + case orderDescription = "order_description" + case paymentMode = "payment_mode" + case productID = "product_id" + case secondPayment = "second_payment" + case subscriptionType = "subscription_type" + case trialDescription = "trial_description" + case countryCode = "country_code" + } + + func getPrice() -> String { + guard + let price = firstPayment + else { + return "0" + } + + return MonetaryAmount(amountCents: price).amountDollars.description + } + + func getLabelForApplePayment() -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "MM/dd/yyyy" + + var text = "" + + if let trial = hasTrial { + + if let trialDaysDuration = durationTrialInDays, + trial { + text.append("1 week trial with one lesson") + + if let secondPayment = secondPayment { + + let currentDate = Date() + var dateComponent = DateComponents() + dateComponent.day = trialDaysDuration + + if let futureDate = Calendar.current.date(byAdding: dateComponent, to: currentDate) { + + text.append("\nstarting \(dateFormatter.string(from: futureDate)) - \(MonetaryAmount(amountCents: secondPayment).amountDollars.description)") + + if let currency = currency { + let currencySymbol = CurrencyHelper.getSymbolForCurrencyCode(code: currency) + + if !currencySymbol.isEmpty { + text.append(" \(currencySymbol)") + } + } + } + } + } else { + text.append(subscriptionType ?? "") + } + } else { + text.append(subscriptionType ?? "") + } + + return text + } +} diff --git a/Sources/PandaSDK/Networking/NetworkClient.swift b/Sources/PandaSDK/Networking/NetworkClient.swift index 526c13f..7fef339 100644 --- a/Sources/PandaSDK/Networking/NetworkClient.swift +++ b/Sources/PandaSDK/Networking/NetworkClient.swift @@ -71,6 +71,14 @@ public struct ReceiptVerificationResult: Codable { let active: Bool } +public struct ApplePayResult: Codable { + let transactionID: String + + enum CodingKeys: String, CodingKey { + case transactionID = "TransactionID" + } +} + public enum SubscriptionAPIStatus: String, Codable { case success = "ok" case empty @@ -252,6 +260,52 @@ internal class NetworkClient: VerificationClient { ) networkLoader.loadData(with: request, timeout: nil, completion: callback) } + + func getBillingPlan( + bilingID: String, + callback: @escaping (Result) -> Void + ) { + let request = createRequest( + path: "/v1/billing-plans/\(bilingID)", + method: .get + ) + networkLoader.loadData(with: request, timeout: nil, completion: callback) + } + + func verifyApplePayRequest( + user: PandaUser, + paymentData: Data, + productId: String, + webAppId: String, + callback: @escaping (Result) -> Void + ) { + let decoder = JSONDecoder() + guard + let paymentInfo = try? decoder.decode(ApplePayPaymentInfo.self, from: paymentData) + else { + callback(.failure(ApplePayVerificationError.init(message: "ApplePayPaymentInfo decoding failed"))) + return + } + let payment = ApplePayPayment( + data: paymentInfo.data, + ephemeralPublicKey: paymentInfo.header.ephemeralPublicKey, + publicKeyHash: paymentInfo.header.publicKeyHash, + transactionId: paymentInfo.header.transactionId, + signature: paymentInfo.signature, + version: paymentInfo.version, + sandbox: isDebug, + webAppId: webAppId, + productId: productId, + userId: user.id + ) + let request = createRequest( + path: "/v1/solid/ios", + method: .post, + body: payment + ) + networkLoader.loadData(with: request, timeout: nil, completion: callback) + } + func getSubscriptionStatus( user: PandaUser, @@ -380,6 +434,20 @@ internal class NetworkClient: VerificationClient { self.verifySubscriptionsRequest(user: user, receipt: receipt, screenId: source?.screenId, callback: onComplete) }, completion: callback) } + + func verifyApplePay( + user: PandaUser, + paymentData: Data, + productId: String, + webAppId: String, + retries: Int = 1, + callback: @escaping (Result) -> Void + ) { + retry(retries, task: { completion in + self.verifyApplePayRequest(user: user, paymentData: paymentData, productId: productId, webAppId: webAppId, callback: completion) + }, completion: callback) + + } func registerUser( retries: Int = 2, diff --git a/Sources/PandaSDK/Networking/NetworkLoader.swift b/Sources/PandaSDK/Networking/NetworkLoader.swift index e00a0d9..aa8403a 100644 --- a/Sources/PandaSDK/Networking/NetworkLoader.swift +++ b/Sources/PandaSDK/Networking/NetworkLoader.swift @@ -21,6 +21,10 @@ struct ApiError: Error { let message: String? } +struct ApplePayVerificationError: Error { + let message: String +} + private struct ApiErrorMessage: Codable { let message: String } diff --git a/Sources/PandaSDK/Panda.swift b/Sources/PandaSDK/Panda.swift index 841f217..ba99d68 100644 --- a/Sources/PandaSDK/Panda.swift +++ b/Sources/PandaSDK/Panda.swift @@ -8,8 +8,9 @@ import Foundation import UIKit +import Combine -public protocol PandaProtocol: class { +public protocol PandaProtocol: AnyObject { /** Initializes PandaSDK. You should call it in `func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool`. All Panda funcs must be called after Panda is configured @@ -20,6 +21,8 @@ public protocol PandaProtocol: class { func configure( apiKey: String, isDebug: Bool, + applePayConfiguration: ApplePayConfiguration?, + webAppId: String?, callback: ((Bool) -> Void)? ) @@ -33,6 +36,16 @@ public protocol PandaProtocol: class { */ var pandaUserId: String? {get} + /** + Returns Web Panda App Id + */ + var webAppId: String? { get } + + /** + Returns publisher that produces apple pay result + */ + var applePayOutputPublisher: AnyPublisher? { get } + /** Returns Current Panda custom user id or nil in cases of abscence or Panda not configured */ @@ -177,6 +190,7 @@ public protocol PandaProtocol: class { func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) -> Bool // MARK: - Handle Purchases + /** Purchase product callback. Callback for successful purchase in Panda purchase screen - you can validate & do you own setup in this callback @@ -286,7 +300,14 @@ public extension Panda { extension Panda { - static func configure(apiKey: String, isDebug: Bool = true, unconfigured: UnconfiguredPanda?, callback: @escaping (Result) -> Void) { + static func configure( + apiKey: String, + isDebug: Bool = true, + applePayConfiguration: ApplePayConfiguration? = nil, + webAppId: String? = nil, + unconfigured: UnconfiguredPanda?, + callback: @escaping (Result) -> Void + ) { if notificationDispatcher == nil { notificationDispatcher = NotificationDispatcher() } @@ -300,22 +321,56 @@ extension Panda { let userStorage: Storage = CodableStorageFactory.keychain() if let user = userStorage.fetch() { - callback(.success(create(user: user, networkClient: networkClient, appStoreClient: appStoreClient, unconfigured: unconfigured))) + callback( + .success( + create( + user: user, + networkClient: networkClient, + appStoreClient: appStoreClient, + unconfigured: unconfigured, + applePayConfiguration: applePayConfiguration, + webAppId: webAppId + ) + ) + ) return } networkClient.registerUser() { (result) in switch result { case .success(let user): userStorage.store(user) - callback(.success(create(user: user, networkClient: networkClient, appStoreClient: appStoreClient, unconfigured: unconfigured))) + callback( + .success( + create( + user: user, + networkClient: networkClient, + appStoreClient: appStoreClient, + unconfigured: unconfigured, + applePayConfiguration: applePayConfiguration, + webAppId: webAppId + ) + ) + ) case .failure(let error): callback(.failure(error)) } } } - static private func create(user: PandaUser, networkClient: NetworkClient, appStoreClient: AppStoreClient, unconfigured: UnconfiguredPanda?) -> Panda { - let panda = Panda(user: user, networkClient: networkClient, appStoreClient: appStoreClient) + static private func create( + user: PandaUser, + networkClient: NetworkClient, + appStoreClient: AppStoreClient, + unconfigured: UnconfiguredPanda?, + applePayConfiguration: ApplePayConfiguration?, + webAppId: String? + ) -> Panda { + let panda = Panda( + user: user, + networkClient: networkClient, + appStoreClient: appStoreClient, + applePayPaymentHandler: .init(configuration: applePayConfiguration ?? .init(merchantIdentifier: "")), webAppId: webAppId ?? "" + ) if let unconfigured = unconfigured { panda.copyCallbacks(from: unconfigured) panda.addViewControllers(controllers: unconfigured.viewControllers) diff --git a/Sources/PandaSDK/UnconfiguredPanda.swift b/Sources/PandaSDK/UnconfiguredPanda.swift index 2dee01b..f84eda6 100644 --- a/Sources/PandaSDK/UnconfiguredPanda.swift +++ b/Sources/PandaSDK/UnconfiguredPanda.swift @@ -7,8 +7,10 @@ import Foundation import UIKit +import Combine final class UnconfiguredPanda: PandaProtocol, ObserverSupport { + var onPurchase: ((String) -> Void)? var onRestorePurchases: (([String]) -> Void)? var onError: ((Error) -> Void)? @@ -26,12 +28,17 @@ final class UnconfiguredPanda: PandaProtocol, ObserverSupport { var pandaFacebookId: PandaFacebookId = .empty var capiConfig: CAPIConfig? var pandaUserProperties = Set() + var webAppId: String? + + var applePayOutputPublisher: AnyPublisher? private static let configError = "Please, configure Panda, by calling Panda.configure(\"\") and wait, until you get `callback(true)`" struct LastConfigurationAttempt { var apiKey: String var isDebug: Bool + var applePayConfiguration: ApplePayConfiguration? + var webAppId: String? } private var lastConfigurationAttempt: LastConfigurationAttempt? @@ -44,9 +51,15 @@ final class UnconfiguredPanda: PandaProtocol, ObserverSupport { observers.removeValue(forKey: ObjectIdentifier(observer)) } - func configure(apiKey: String, isDebug: Bool = true, callback: ((Bool) -> Void)?) { - lastConfigurationAttempt = LastConfigurationAttempt(apiKey: apiKey, isDebug: isDebug) - Panda.configure(apiKey: apiKey, isDebug: isDebug, unconfigured: self, callback: { result in + func configure(apiKey: String, isDebug: Bool = true, applePayConfiguration: ApplePayConfiguration? = nil, webAppId: String?, callback: ((Bool) -> Void)?) { + lastConfigurationAttempt = LastConfigurationAttempt(apiKey: apiKey, isDebug: isDebug, applePayConfiguration: applePayConfiguration) + Panda.configure( + apiKey: apiKey, + isDebug: isDebug, + applePayConfiguration: applePayConfiguration, + webAppId: webAppId, + unconfigured: self, + callback: { result in DispatchQueue.main.async { switch result { case .failure: @@ -64,7 +77,13 @@ final class UnconfiguredPanda: PandaProtocol, ObserverSupport { callback(.failure(Errors.notConfigured)) return } - Panda.configure(apiKey: configAttempt.apiKey, isDebug: configAttempt.isDebug, unconfigured: self) { [viewControllers] (result) in + Panda.configure( + apiKey: configAttempt.apiKey, + isDebug: configAttempt.isDebug, + applePayConfiguration: configAttempt.applePayConfiguration, + webAppId: configAttempt.webAppId, + unconfigured: self + ) { [viewControllers] (result) in if case .failure = result { viewControllers.forEach { $0.value?.onFinishLoad() } } @@ -167,6 +186,26 @@ final class UnconfiguredPanda: PandaProtocol, ObserverSupport { viewModel.onSurvey = { value, screenId, screenName in pandaLog("Survey: \(value)") } + viewModel.onApplePayPurchase = { [weak self] bilingID, source, screenId, screenName, viewController in + guard let bilingID = bilingID else { + pandaLog("Missing productId with source: \(source)") + return + } + self?.reconfigure(callback: { (result) in + switch result { + case .success: + pandaLog("Reconfigured") + viewController.viewModel?.onApplePayPurchase?(bilingID, source, screenId, screenName, viewController) + case .failure(let error): + pandaLog("Reconfigured error: \(error)") + DispatchQueue.main.async { + viewController.showInternetConnectionAlert() + self?.viewControllers.forEach { $0.value?.onFinishLoad() } + self?.onError?(error) + } + } + }) + } viewModel.onPurchase = { [weak self] productId, source, view, screenId, screenName, course in guard let productId = productId else { pandaLog("Missing productId with source: \(source)") diff --git a/Sources/PandaSDK/Views/WebViewController.swift b/Sources/PandaSDK/Views/WebViewController.swift index 59e1ea4..14258f6 100644 --- a/Sources/PandaSDK/Views/WebViewController.swift +++ b/Sources/PandaSDK/Views/WebViewController.swift @@ -146,36 +146,45 @@ final class WebViewController: UIViewController, WKScriptMessageHandler { pandaLog("JavaScript messageHandler: `\(message.name)` is sending a message:") if message.name == PandaJSMessagesNames.onPurchase.rawValue { - if let data = message.body as? [String: String], - let productID = data["productID"] { - onStartLoad() - viewModel?.onPurchase( - productID, - "WKScriptMessage", - self, - viewModel?.screenData.id.string ?? "", - viewModel?.screenData.name ?? "", - data["course"] - ) - - if let urlString = data["url"], - let url = URL(string: urlString), - let type = data["type"], - type == "external" { - onPurchaseCmpld = { - UIApplication.shared.open(url) + if let data = message.body as? [String: String] { + + if let pandaID = data["pandaID"] { + viewModel?.onApplePayPurchase( + pandaID, + "WKScriptMessage", + viewModel?.screenData.id.string ?? "", + viewModel?.screenData.name ?? "", + self + ) + } else if let productID = data["productID"] { + onStartLoad() + viewModel?.onPurchase( + productID, + "WKScriptMessage", + self, + viewModel?.screenData.id.string ?? "", + viewModel?.screenData.name ?? "", + data["course"] + ) + + if let urlString = data["url"], + let url = URL(string: urlString), + let type = data["type"], + type == "external" { + onPurchaseCmpld = { + UIApplication.shared.open(url) + } } - } - if let type = data["type"], - type == "moveNext" { - isAutoDismissable = false - onPurchaseCmpld = { [weak self] in - self?.moveNext() + if let type = data["type"], + type == "moveNext" { + isAutoDismissable = false + onPurchaseCmpld = { [weak self] in + self?.moveNext() + } } } } - } if message.name == PandaJSMessagesNames.logHandler.rawValue { @@ -397,17 +406,29 @@ extension WebViewController: WKNavigationDelegate { switch action { case "purchase": - onStartLoad() let productID = urlComps.queryItems?.first(where: { $0.name == "product_id" })?.value let course = urlComps.queryItems?.first(where: { $0.name == "course" })?.value - viewModel?.onPurchase( - productID, - url.lastPathComponent, - self, - screenID, - screenName, - course - ) + + if let pandaID = urlComps.queryItems?.first(where: { $0.name == "pandaID"})?.value { + viewModel?.onApplePayPurchase( + pandaID, + url.lastPathComponent, + viewModel?.screenData.id.string ?? "", + viewModel?.screenData.name ?? "", + self + ) + } else { + onStartLoad() + viewModel?.onPurchase( + productID, + url.lastPathComponent, + self, + screenID, + screenName, + course + ) + } + return false case "restore": onStartLoad() diff --git a/Sources/PandaSDK/Views/WebViewModel.swift b/Sources/PandaSDK/Views/WebViewModel.swift index 5c1766c..775c58f 100644 --- a/Sources/PandaSDK/Views/WebViewModel.swift +++ b/Sources/PandaSDK/Views/WebViewModel.swift @@ -11,6 +11,7 @@ import StoreKit protocol WebViewModelProtocol { var onPurchase: ((_ product: String?, _ source: String, _ viewController: WebViewController, _ screenId: String, _ screenName: String, _ course: String?) -> Void)! { get set } + var onApplePayPurchase: ((_ bilingID: String?, _ source: String, _ screenId: String, _ screenName: String, _ viewController: WebViewController) -> Void)! { get set } var onViewWillAppear: ((_ screenId: String?, _ screenName: String?) -> Void)? { get set } var onViewDidAppear: ((_ screenId: String?, _ screenName: String?, _ course: String?) -> Void)? { get set } var onDidFinishLoading: ((_ screenId: String?, _ screenName: String?, _ course: String?) -> Void)? { get set } @@ -31,8 +32,10 @@ protocol WebViewModelProtocol { } final class WebViewModel: WebViewModelProtocol { + // MARK: - Properties - @objc var onPurchase: ((_ product: String?, _ source: String, _ viewController: WebViewController, _ sceenId: String, _ screenName: String, _ course: String?) -> Void)! + @objc var onPurchase: ((_ product: String?, _ source: String, _ viewController: WebViewController, _ screenId: String, _ screenName: String, _ course: String?) -> Void)! + @objc var onApplePayPurchase: ((String?, String, String, String, WebViewController) -> Void)! var onViewWillAppear: ((_ screenId: String?, _ screenName: String?) -> Void)? var onViewDidAppear: ((_ screenId: String?, _ screenName: String?, _ course: String?) -> Void)? var onDidFinishLoading: ((_ screenId: String?, _ screenName: String?, _ course: String?) -> Void)? @@ -62,6 +65,7 @@ final class WebViewModel: WebViewModelProtocol { self.screenData = screenData self.payload = payload setupObserver() + setupApplePayObserver() } // MARK: - Public @@ -86,6 +90,15 @@ extension WebViewModel { ) } + private func setupApplePayObserver() { + NotificationCenter.default.addObserver( + self, + selector: #selector(getter: onApplePayPurchase), + name: NSNotification.Name(rawValue: "SubscriptionBooster.onPurchase"), + object: nil + ) + } + private func reloadWithDefaultScreenData(_ screenData: ScreenData) { self.screenData = screenData }