diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 51e69becfe..157d784095 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -1045,6 +1045,7 @@ FD2046842CB833CD00166727 /* MockStoreKit2PurchaseIntentListenerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2046822CB833CD00166727 /* MockStoreKit2PurchaseIntentListenerDelegate.swift */; }; FD2E6C9F2C480FF000CB4BD7 /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = FD2E6C9E2C480FF000CB4BD7 /* OHHTTPStubs */; }; FD2E6CA12C48100900CB4BD7 /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD2E6CA02C48100900CB4BD7 /* OHHTTPStubsSwift */; }; + FD33CD4D2D034CBD000D13A4 /* CustomerCenterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD33CD4C2D034CBD000D13A4 /* CustomerCenterViewController.swift */; }; FD43D2FC2C41864000077235 /* TimeInterval+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD43D2FA2C4185B700077235 /* TimeInterval+Extensions.swift */; }; FD43D2FE2C41867600077235 /* TimeInterval+ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD43D2FD2C41867600077235 /* TimeInterval+ExtensionsTests.swift */; }; FD9F982D2BE28A7F0091A5BF /* MockNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B515526D44B2300BD2BD7 /* MockNotificationCenter.swift */; }; @@ -2292,6 +2293,8 @@ FD20467E2CB82F1700166727 /* StoreKit2PurchaseIntentListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2PurchaseIntentListener.swift; sourceTree = ""; }; FD2046802CB8333200166727 /* StoreKit2PurchaseIntentListenerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2PurchaseIntentListenerTests.swift; sourceTree = ""; }; FD2046822CB833CD00166727 /* MockStoreKit2PurchaseIntentListenerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreKit2PurchaseIntentListenerDelegate.swift; sourceTree = ""; }; + FD33CD4C2D034CBD000D13A4 /* CustomerCenterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterViewController.swift; sourceTree = ""; }; + FD33CD5F2D03500C000D13A4 /* CustomerCenterUIKitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterUIKitView.swift; sourceTree = ""; }; FD43D2FA2C4185B700077235 /* TimeInterval+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Extensions.swift"; sourceTree = ""; }; FD43D2FD2C41867600077235 /* TimeInterval+ExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+ExtensionsTests.swift"; sourceTree = ""; }; FDAADFCA2BE2A5BF00BD1659 /* MockAllTransactionsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAllTransactionsProvider.swift; sourceTree = ""; }; @@ -2963,6 +2966,7 @@ 2DD5006F2C519EB4009C19B7 /* AppList.swift */, 2DD500702C519EB4009C19B7 /* CustomPaywall.swift */, 2DD500712C519EB4009C19B7 /* CustomPaywallContent.swift */, + FD33CD5F2D03500C000D13A4 /* CustomerCenterUIKitView.swift */, 2DD500722C519EB4009C19B7 /* DebugView.swift */, 2DD500732C519EB4009C19B7 /* PaywallForID.swift */, 2DD500742C519EB4009C19B7 /* PaywallPresenter.swift */, @@ -3542,6 +3546,7 @@ 353756602C382C2800A1B8D6 /* Views */ = { isa = PBXGroup; children = ( + FD33CD4B2D034CA6000D13A4 /* UIKit Compatibility */, 3531DF872CFE138800D454BF /* ManageSubscriptionsButtonsView.swift */, 2D2AFE8E2C6A9D8700D1B0B4 /* CompatibilityContentUnavailableView.swift */, 2C4C36122C6FBA8B00AE959B /* CompatibilityTopBarTrailing.swift */, @@ -4959,6 +4964,14 @@ path = Attribution; sourceTree = ""; }; + FD33CD4B2D034CA6000D13A4 /* UIKit Compatibility */ = { + isa = PBXGroup; + children = ( + FD33CD4C2D034CBD000D13A4 /* CustomerCenterViewController.swift */, + ); + path = "UIKit Compatibility"; + sourceTree = ""; + }; FDAADFCD2BE2B82D00BD1659 /* Observer Mode */ = { isa = PBXGroup; children = ( @@ -6581,6 +6594,7 @@ 2C8EC6DB2CCC23B700D6CCF8 /* Template5Preview.swift in Sources */, 88B1BB022C813A3C001B7EE5 /* StackComponentView.swift in Sources */, 353756712C382C2800A1B8D6 /* ManageSubscriptionsPurchaseType.swift in Sources */, + FD33CD4D2D034CBD000D13A4 /* CustomerCenterViewController.swift in Sources */, 35C200AF2C39252D00B9778B /* FeedbackSurveyData.swift in Sources */, 353FDC0D2CA41CB20055F328 /* SubscriptionPeriod+Extensions.swift in Sources */, 3551E39D2C4A6A1400D27C25 /* TintedProgressView.swift in Sources */, diff --git a/RevenueCatUI/CustomerCenter/Views/UIKit Compatibility/CustomerCenterViewController.swift b/RevenueCatUI/CustomerCenter/Views/UIKit Compatibility/CustomerCenterViewController.swift new file mode 100644 index 0000000000..a842ae3637 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Views/UIKit Compatibility/CustomerCenterViewController.swift @@ -0,0 +1,45 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// CustomerCenterViewController.swift +// +// Created by Will Taylor on 12/6/24. + +import RevenueCat +import SwiftUI + +#if canImport(UIKit) && os(iOS) + +/// A UIKit ViewController for displaying a customer support common tasks +@available(iOS 15.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +public class CustomerCenterViewController: UIHostingController { + + /// Create a view controller to handle common customer support tasks + /// - Parameters: + /// - customerCenterActionHandler: An optional `CustomerCenterActionHandler` to handle actions + /// from the Customer Center. + public init( + customerCenterActionHandler: CustomerCenterActionHandler? = nil + ) { + let view = CustomerCenterView( + customerCenterActionHandler: customerCenterActionHandler + ) + super.init(rootView: view) + } + + @available(*, unavailable, message: "Use init(customerCenterActionHandler:mode:) instead.") + required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +#endif diff --git a/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj b/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj index 200d352a03..f8c6f07b2b 100644 --- a/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj +++ b/Tests/APITesters/AllAPITests/AllAPITests.xcodeproj/project.pbxproj @@ -126,6 +126,7 @@ 3502630E2CF61E9F00894270 /* SubscriptionInfoAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3502630D2CF61E9A00894270 /* SubscriptionInfoAPI.swift */; }; 35370AC52CFF8304004F0A64 /* RCSubscriptionInfoAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = 35370AC42CFF82F8004F0A64 /* RCSubscriptionInfoAPI.h */; }; 35370AC82CFF8317004F0A64 /* RCSubscriptionInfoAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 35370AC72CFF8312004F0A64 /* RCSubscriptionInfoAPI.m */; }; + FD33CD732D03587E000D13A4 /* CustomerCenterViewControllerAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD33CD722D035876000D13A4 /* CustomerCenterViewControllerAPI.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -352,6 +353,7 @@ 3502630D2CF61E9A00894270 /* SubscriptionInfoAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionInfoAPI.swift; sourceTree = ""; }; 35370AC42CFF82F8004F0A64 /* RCSubscriptionInfoAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCSubscriptionInfoAPI.h; sourceTree = ""; }; 35370AC72CFF8312004F0A64 /* RCSubscriptionInfoAPI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCSubscriptionInfoAPI.m; sourceTree = ""; }; + FD33CD722D035876000D13A4 /* CustomerCenterViewControllerAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterViewControllerAPI.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -594,6 +596,7 @@ 2D4C62222C5ADA2400A29FD2 /* RevenueCatUISwiftAPITester */ = { isa = PBXGroup; children = ( + FD33CD722D035876000D13A4 /* CustomerCenterViewControllerAPI.swift */, 2D4C622D2C5ADA6700A29FD2 /* PaywallViewAPI.swift */, 1E8A60802CDCF29D0034ACF3 /* RedeemWebPurchasesAPI.swift */, 2D4C622B2C5ADA6700A29FD2 /* PaywallViewControllerAPI.swift */, @@ -1050,6 +1053,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + FD33CD732D03587E000D13A4 /* CustomerCenterViewControllerAPI.swift in Sources */, 2D4C622F2C5ADA6700A29FD2 /* main.swift in Sources */, 2D4C62302C5ADA6700A29FD2 /* PaywallViewControllerAPI.swift in Sources */, 1E8A60812CDCF29D0034ACF3 /* RedeemWebPurchasesAPI.swift in Sources */, diff --git a/Tests/APITesters/AllAPITests/RevenueCatUISwiftAPITester/CustomerCenterViewControllerAPI.swift b/Tests/APITesters/AllAPITests/RevenueCatUISwiftAPITester/CustomerCenterViewControllerAPI.swift new file mode 100644 index 0000000000..2f78c0c0b3 --- /dev/null +++ b/Tests/APITesters/AllAPITests/RevenueCatUISwiftAPITester/CustomerCenterViewControllerAPI.swift @@ -0,0 +1,20 @@ +// +// CustomerCenterViewAPI.swift +// AllAPITests +// +// Created by Will Taylor on 12/6/24. +// + +import RevenueCat +import RevenueCatUI + + +#if canImport(UIKit) && os(iOS) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, *) +func checkCustomerCenterViewControllerAPI( + customerCenterActionHandler: CustomerCenterActionHandler? = nil +) { + let _ = CustomerCenterViewController() + let _ = CustomerCenterViewController(customerCenterActionHandler: customerCenterActionHandler) +} +#endif diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj index 46e213187a..27ffa48571 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ @@ -59,6 +59,7 @@ 88DFC1942BCF490400273B6D /* PaywallsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DFC1912BCF490400273B6D /* PaywallsResponse.swift */; }; 88DFC1972BCF4A5100273B6D /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DFC1952BCF4A4300273B6D /* MockData.swift */; }; FA29FBB22CCAA7A500DA1976 /* SnapshottingTests in Frameworks */ = {isa = PBXBuildFile; productRef = FA29FBB12CCAA7A500DA1976 /* SnapshottingTests */; }; + FD33CD622D0351BE000D13A4 /* CustomerCenterUIKitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD33CD612D0351BE000D13A4 /* CustomerCenterUIKitView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -151,6 +152,7 @@ 88DFC1922BCF490400273B6D /* OfferingsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferingsResponse.swift; sourceTree = ""; }; 88DFC1952BCF4A4300273B6D /* MockData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockData.swift; sourceTree = ""; }; FA29FBA82CCAA79800DA1976 /* PaywallsTesterTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PaywallsTesterTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + FD33CD612D0351BE000D13A4 /* CustomerCenterUIKitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterUIKitView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -287,6 +289,7 @@ 88DFC12C2BC7306100273B6D /* Views */ = { isa = PBXGroup; children = ( + FD33CD612D0351BE000D13A4 /* CustomerCenterUIKitView.swift */, 88DFC1872BCF39AF00273B6D /* Login */, 88DFC18F2BCF48B000273B6D /* AppList.swift */, 88B438092BDB0A28000AF27C /* PaywallForID.swift */, @@ -539,6 +542,7 @@ 88DFC1412BC73D9D00273B6D /* HTTPRequest.swift in Sources */, 88DFC1462BC7443B00273B6D /* ErrorResponse.swift in Sources */, 88DFC1402BC73D9D00273B6D /* HTTPStatusCode.swift in Sources */, + FD33CD622D0351BE000D13A4 /* CustomerCenterUIKitView.swift in Sources */, 4F34FF632A60AD9A00AADF11 /* AppContentView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/CustomerCenterUIKitView.swift b/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/CustomerCenterUIKitView.swift new file mode 100644 index 0000000000..67c2b61355 --- /dev/null +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/CustomerCenterUIKitView.swift @@ -0,0 +1,27 @@ +// +// CustomerCenterUIKitView.swift +// PaywallsTester +// +// Created by Will Taylor on 12/6/24. +// + + +import SwiftUI +import RevenueCat +import RevenueCatUI + +/// Allows us to display the CustomerCenterViewController in a SwiftUI app +struct CustomerCenterUIKitView: UIViewControllerRepresentable { + + let customerCenterActionHandler: (CustomerCenterAction) -> Void + + func makeUIViewController(context: Context) -> CustomerCenterViewController { + CustomerCenterViewController( + customerCenterActionHandler: self.customerCenterActionHandler + ) + } + + func updateUIViewController(_ uiViewController: CustomerCenterViewController, context: Context) { + // No updates needed + } +} diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift b/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift index 3eaa9f7dc3..6a4371fb04 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester/UI/Views/SamplePaywallsList.swift @@ -89,6 +89,10 @@ struct SamplePaywallsList: View { ) case .customerCenter: CustomerCenterView(customerCenterActionHandler: self.handleCustomerCenterAction) + case .uiKitCustomerCenter: + CustomerCenterUIKitView( + customerCenterActionHandler: self.handleCustomerCenterAction + ) #if PAYWALL_COMPONENTS case .componentPaywall(let data): PaywallView(configuration: .init( @@ -162,6 +166,12 @@ struct SamplePaywallsList: View { TemplateLabel(name: "SwiftUI", icon: "person.fill.questionmark") } + Button { + self.display = .uiKitCustomerCenter + } label: { + TemplateLabel(name: "UIKit Customer Center", icon: "person.fill.questionmark") + } + Button { self.presentingCustomerCenter = true } label: { @@ -263,6 +273,7 @@ private extension SamplePaywallsList { case missingPaywall case unrecognizedPaywall case customerCenter + case uiKitCustomerCenter #if PAYWALL_COMPONENTS case componentPaywall(PaywallComponentsData) #endif @@ -296,6 +307,8 @@ extension SamplePaywallsList.Display: Identifiable { case .componentPaywall: return "component-paywall" #endif + case .uiKitCustomerCenter: + return "customer-center-uikit" } }