From a20f48cd1058fa04efaf497403f85afeac7e87da Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Wed, 2 Aug 2023 17:40:17 -0700 Subject: [PATCH] `Paywalls`: UIKit `PaywallViewController` (#2934) --- RevenueCatUI/PaywallView.swift | 2 +- .../UIKit/PaywallViewController.swift | 76 +++++++++++++++++++ .../project.pbxproj | 4 + .../SwiftAPITester/PaywallViewAPI.swift | 1 + .../PaywallViewControllerAPI.swift | 26 +++++++ 5 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 RevenueCatUI/UIKit/PaywallViewController.swift create mode 100644 Tests/APITesters/RevenueCatUIAPITester/SwiftAPITester/PaywallViewControllerAPI.swift diff --git a/RevenueCatUI/PaywallView.swift b/RevenueCatUI/PaywallView.swift index 131e1ea9d9..c1608c251f 100644 --- a/RevenueCatUI/PaywallView.swift +++ b/RevenueCatUI/PaywallView.swift @@ -35,7 +35,7 @@ public struct PaywallView: View { /// - Note: if `offering` does not have a current paywall, or it fails to load due to invalid data, /// a default paywall will be displayed. /// - Warning: `Purchases` must have been configured prior to displaying it. - public init(offering: Offering, mode: PaywallViewMode = .default) { + public init(offering: Offering?, mode: PaywallViewMode = .default) { self.init( offering: offering, mode: mode, diff --git a/RevenueCatUI/UIKit/PaywallViewController.swift b/RevenueCatUI/UIKit/PaywallViewController.swift new file mode 100644 index 0000000000..93b06b0139 --- /dev/null +++ b/RevenueCatUI/UIKit/PaywallViewController.swift @@ -0,0 +1,76 @@ +// +// PaywallViewController.swift +// +// +// Created by Nacho Soto on 8/1/23. +// + +#if canImport(UIKit) + +import RevenueCat +import SwiftUI +import UIKit + +/// A view controller for displaying `PaywallData` for an `Offering`. +/// +/// - Seealso: ``PaywallView`` for `SwiftUI`. +@available(iOS 16.0, macOS 13.0, tvOS 16.0, *) +@objc(RCPaywallViewController) +public final class PaywallViewController: UIViewController { + + /// See ``PaywallViewControllerDelegate`` for receiving purchase events. + public weak var delegate: PaywallViewControllerDelegate? + + private let offering: Offering? + + // swiftlint:disable:next missing_docs + public init(offering: Offering? = nil) { + self.offering = offering + + super.init(nibName: nil, bundle: nil) + } + + // swiftlint:disable:next missing_docs + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private lazy var hostingController: UIHostingController = { + let view = PaywallView(offering: self.offering) + .onPurchaseCompleted { [weak self] customerInfo in + guard let self = self else { return } + self.delegate?.paywallViewController(self, didFinishPurchasingWith: customerInfo) + } + + return .init(rootView: AnyView(view)) + }() + + public override func loadView() { + super.loadView() + + self.addChild(self.hostingController) + self.view.addSubview(self.hostingController.view) + self.hostingController.didMove(toParent: self) + } + + public override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + + self.hostingController.view.frame = self.view.bounds + } + +} + +/// Delegate for ``PaywallViewController``. +@available(iOS 16.0, macOS 13.0, tvOS 16.0, *) +@objc(RCPaywallViewControllerDelegate) +public protocol PaywallViewControllerDelegate: AnyObject { + + /// Notifies that a purchase has completed in a ``PaywallViewController``. + @objc(paywallViewController:didFinishPurchasingWithCustomerInfo:) + func paywallViewController(_ controller: PaywallViewController, + didFinishPurchasingWith customerInfo: CustomerInfo) + +} + +#endif diff --git a/Tests/APITesters/RevenueCatUIAPITester/RevenueCatUISwiftAPITester.xcodeproj/project.pbxproj b/Tests/APITesters/RevenueCatUIAPITester/RevenueCatUISwiftAPITester.xcodeproj/project.pbxproj index d6e9cc5e8c..94c8cf4cce 100644 --- a/Tests/APITesters/RevenueCatUIAPITester/RevenueCatUISwiftAPITester.xcodeproj/project.pbxproj +++ b/Tests/APITesters/RevenueCatUIAPITester/RevenueCatUISwiftAPITester.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 4F592A502A1FDC6F00851F36 /* PaywallViewAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F592A4F2A1FDC6F00851F36 /* PaywallViewAPI.swift */; }; 4FD737882A61AD71002AFE75 /* RevenueCatUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4FD737872A61AD71002AFE75 /* RevenueCatUI */; }; 4FD7378A2A61ADF8002AFE75 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = 4FD737892A61ADF8002AFE75 /* RevenueCat */; }; + 57028D4E2A79325200DB1D9A /* PaywallViewControllerAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57028D4D2A79325200DB1D9A /* PaywallViewControllerAPI.swift */; }; 5738F42127866F8F0096D623 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C626EBE7EA007DDB75 /* main.swift */; }; /* End PBXBuildFile section */ @@ -31,6 +32,7 @@ 2C396F5B281C64AF00669657 /* AdServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdServices.framework; path = System/Library/Frameworks/AdServices.framework; sourceTree = SDKROOT; }; 2DD778D0270E233F0079CBD4 /* SwiftAPITester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftAPITester.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4F592A4F2A1FDC6F00851F36 /* PaywallViewAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallViewAPI.swift; sourceTree = ""; }; + 57028D4D2A79325200DB1D9A /* PaywallViewControllerAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallViewControllerAPI.swift; sourceTree = ""; }; A5D614C626EBE7EA007DDB75 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -77,6 +79,7 @@ isa = PBXGroup; children = ( 4F592A4F2A1FDC6F00851F36 /* PaywallViewAPI.swift */, + 57028D4D2A79325200DB1D9A /* PaywallViewControllerAPI.swift */, A5D614C626EBE7EA007DDB75 /* main.swift */, ); path = SwiftAPITester; @@ -159,6 +162,7 @@ buildActionMask = 2147483647; files = ( 5738F42127866F8F0096D623 /* main.swift in Sources */, + 57028D4E2A79325200DB1D9A /* PaywallViewControllerAPI.swift in Sources */, 4F592A502A1FDC6F00851F36 /* PaywallViewAPI.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Tests/APITesters/RevenueCatUIAPITester/SwiftAPITester/PaywallViewAPI.swift b/Tests/APITesters/RevenueCatUIAPITester/SwiftAPITester/PaywallViewAPI.swift index f6eaaaa20f..cbf78e141a 100644 --- a/Tests/APITesters/RevenueCatUIAPITester/SwiftAPITester/PaywallViewAPI.swift +++ b/Tests/APITesters/RevenueCatUIAPITester/SwiftAPITester/PaywallViewAPI.swift @@ -25,6 +25,7 @@ struct App: View { var content: some View { PaywallView() PaywallView(mode: .fullScreen) + PaywallView(offering: nil) PaywallView(offering: self.offering) PaywallView(offering: self.offering, mode: .fullScreen) } diff --git a/Tests/APITesters/RevenueCatUIAPITester/SwiftAPITester/PaywallViewControllerAPI.swift b/Tests/APITesters/RevenueCatUIAPITester/SwiftAPITester/PaywallViewControllerAPI.swift new file mode 100644 index 0000000000..9edd12401c --- /dev/null +++ b/Tests/APITesters/RevenueCatUIAPITester/SwiftAPITester/PaywallViewControllerAPI.swift @@ -0,0 +1,26 @@ +// +// PaywallViewControllerAPI.swift +// SwiftAPITester +// +// Created by Nacho Soto on 8/1/23. +// + +import RevenueCat +import RevenueCatUI +import SwiftUI + +@available(iOS 16.0, macOS 13.0, tvOS 16.0, *) +func paywallViewControllerAPI(_ delegate: Delegate, _ offering: Offering?) { + let controller = PaywallViewController() + controller.delegate = delegate + + let _: UIViewController = PaywallViewController(offering: offering) +} + +@available(iOS 16.0, macOS 13.0, tvOS 16.0, *) +final class Delegate: PaywallViewControllerDelegate { + + func paywallViewController(_ controller: PaywallViewController, + didFinishPurchasingWith customerInfo: CustomerInfo) {} + +}