diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 104d45e08e..977f973f5f 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -241,6 +241,7 @@ 4FA696BD2A0020A000D228B1 /* MainThreadMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA696BC2A0020A000D228B1 /* MainThreadMonitor.swift */; }; 4FCBA84F2A15391B004134BD /* SnapshotTesting+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576C8A9127D27DDD0058FA6E /* SnapshotTesting+Extensions.swift */; }; 4FCBA8512A153940004134BD /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 4FCBA8502A153940004134BD /* SnapshotTesting */; }; + 4FCEEA5E2A379B80002C2112 /* DebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCEEA5D2A379B80002C2112 /* DebugViewController.swift */; }; 4FCEEA612A379CF9002C2112 /* DebugViewSwiftUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCEEA602A379CF9002C2112 /* DebugViewSwiftUITests.swift */; }; 4FCEEA632A37A2E9002C2112 /* ImageSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCEEA622A37A2E9002C2112 /* ImageSnapshot.swift */; }; 4FD291BE2A1E9A2E0098D1B9 /* StoreKit2TransactionFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD291BD2A1E9A2E0098D1B9 /* StoreKit2TransactionFetcherTests.swift */; }; @@ -934,6 +935,7 @@ 4FA696A329FC43C600D228B1 /* ReceiptParserTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "ReceiptParserTests-Info.plist"; sourceTree = ""; }; 4FA696BC2A0020A000D228B1 /* MainThreadMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainThreadMonitor.swift; sourceTree = ""; }; 4FCBA8522A1539D0004134BD /* __Snapshots__ */ = {isa = PBXFileReference; lastKnownFileType = folder; path = __Snapshots__; sourceTree = ""; }; + 4FCEEA5D2A379B80002C2112 /* DebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugViewController.swift; sourceTree = ""; }; 4FCEEA602A379CF9002C2112 /* DebugViewSwiftUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugViewSwiftUITests.swift; sourceTree = ""; }; 4FCEEA622A37A2E9002C2112 /* ImageSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSnapshot.swift; sourceTree = ""; }; 4FD291BD2A1E9A2E0098D1B9 /* StoreKit2TransactionFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2TransactionFetcherTests.swift; sourceTree = ""; }; @@ -2115,6 +2117,7 @@ 4F6BEDD82A26B55C00CD9322 /* DebugViewModel.swift */, 4F6BEDDF2A26B65900CD9322 /* DebugViewSheetPresentation.swift */, 4F6BEDE12A26B69500CD9322 /* DebugContentViews.swift */, + 4FCEEA5D2A379B80002C2112 /* DebugViewController.swift */, ); path = DebugUI; sourceTree = ""; @@ -3202,6 +3205,7 @@ B3B5FBBF269E081E00104A0C /* InMemoryCachedObject.swift in Sources */, 579B67F428C5326A0094F7E8 /* PaymentQueueWrapper.swift in Sources */, B3DDB55926854865008CCF23 /* PurchaseOwnershipType.swift in Sources */, + 4FCEEA5E2A379B80002C2112 /* DebugViewController.swift in Sources */, 579415D529368AB200218FBC /* ReceiptStrings.swift in Sources */, 57A0FBF02749C0C2009E2FC3 /* Atomic.swift in Sources */, 2DC5623224EC63730031F69B /* TransactionsFactory.swift in Sources */, diff --git a/Sources/Support/DebugUI/DebugContentViews.swift b/Sources/Support/DebugUI/DebugContentViews.swift index c371e6121f..15f8e4ef5c 100644 --- a/Sources/Support/DebugUI/DebugContentViews.swift +++ b/Sources/Support/DebugUI/DebugContentViews.swift @@ -45,6 +45,8 @@ struct DebugSwiftUIRootView: View { } } + static let cornerRadius: CGFloat = 24 + } private enum DebugViewPath: Hashable { diff --git a/Sources/Support/DebugUI/DebugView.swift b/Sources/Support/DebugUI/DebugView.swift index 07adc5738b..9d72d53400 100644 --- a/Sources/Support/DebugUI/DebugView.swift +++ b/Sources/Support/DebugUI/DebugView.swift @@ -60,7 +60,7 @@ public extension View { ], isPresented: isPresented, largestUndimmedIdentifier: .fraction(0.6), - cornerRadius: 10, + cornerRadius: DebugSwiftUIRootView.cornerRadius, content: { DebugSwiftUIRootView() } diff --git a/Sources/Support/DebugUI/DebugViewController.swift b/Sources/Support/DebugUI/DebugViewController.swift new file mode 100644 index 0000000000..eb97acec52 --- /dev/null +++ b/Sources/Support/DebugUI/DebugViewController.swift @@ -0,0 +1,88 @@ +// +// 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 +// +// DebugViewController.swift +// +// Created by Nacho Soto on 6/12/23. + +import Foundation + +#if DEBUG && os(iOS) && swift(>=5.8) + +import SwiftUI +import UIKit + +/// A view controller which allows debugging the current SDK setup. +/// +/// You can present this yourself, or use `UIViewController.presentDebugRevenueCatOverlay` +/// for a default presentation using `UISheetPresentationController`. +/// +/// - Seealso: `View.debugRevenueCatOverlay` for `SwiftUI`. +@available(iOS 16.0, *) +@objc(RCDebugViewController) +public final class DebugViewController: UIViewController { + + private lazy var hostingController: UIHostingController = { + .init(rootView: .init()) + }() + + // swiftlint:disable:next missing_docs + public init() { + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + 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 + } + +} + +@available(iOS 16.0, *) +extension UIViewController { + + /// Presents a bottom sheet overlay on top of the current view controller + /// which allows debugging the current SDK setup. + /// + /// - Seealso: `DebugViewController`. + /// - Seealso: `View.debugRevenueCatOverlay` for `SwiftUI`. + @objc(rc_presentDebugRevenueCatOverlayAnimated:) + public func presentDebugRevenueCatOverlay(animated: Bool = true) { + let controller = DebugViewController() + + if let sheet = controller.sheetPresentationController { + sheet.detents = [ + .custom(resolver: { context in context.maximumDetentValue * 0.2 }), + .medium(), + .large() + ] + sheet.largestUndimmedDetentIdentifier = .medium + sheet.preferredCornerRadius = DebugSwiftUIRootView.cornerRadius + sheet.prefersGrabberVisible = true + } + + self.present(controller, animated: animated) + } + +} + +#endif diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester.xcodeproj/project.pbxproj b/Tests/APITesters/ObjCAPITester/ObjCAPITester.xcodeproj/project.pbxproj index 668a65de57..d124387dca 100644 --- a/Tests/APITesters/ObjCAPITester/ObjCAPITester.xcodeproj/project.pbxproj +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 2DD77914270E23870079CBD4 /* RCOfferingAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614DF26EBE84F007DDB75 /* RCOfferingAPI.m */; }; 2DD77915270E23870079CBD4 /* RCPackageAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614DE26EBE84F007DDB75 /* RCPackageAPI.m */; }; 2DD77916270E23870079CBD4 /* RCTransactionAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A5D614F426EBE84F007DDB75 /* RCTransactionAPI.m */; }; + 4F34093B2A37E5930050EA0E /* RCOtherAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F34093A2A37E5930050EA0E /* RCOtherAPI.m */; }; 570FAF502864ECB000D3C769 /* RCNonSubscriptionTransactionAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 570FAF4F2864ECB000D3C769 /* RCNonSubscriptionTransactionAPI.m */; }; 5738F428278672070096D623 /* RCStoreProductDiscountAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 5738F427278672070096D623 /* RCStoreProductDiscountAPI.m */; }; 5738F42D278674710096D623 /* RCSubscriptionPeriodAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 5738F42C278674710096D623 /* RCSubscriptionPeriodAPI.m */; }; @@ -53,6 +54,8 @@ /* Begin PBXFileReference section */ 2C396F5D281C64B700669657 /* AdServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdServices.framework; path = System/Library/Frameworks/AdServices.framework; sourceTree = SDKROOT; }; 2DD778F5270E235B0079CBD4 /* ObjCAPITester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ObjCAPITester.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4F3409392A37E5930050EA0E /* RCOtherAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCOtherAPI.h; sourceTree = ""; }; + 4F34093A2A37E5930050EA0E /* RCOtherAPI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCOtherAPI.m; sourceTree = ""; }; 570FAF4E2864ECB000D3C769 /* RCNonSubscriptionTransactionAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCNonSubscriptionTransactionAPI.h; sourceTree = ""; }; 570FAF4F2864ECB000D3C769 /* RCNonSubscriptionTransactionAPI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCNonSubscriptionTransactionAPI.m; sourceTree = ""; }; 5738F426278672070096D623 /* RCStoreProductDiscountAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCStoreProductDiscountAPI.h; sourceTree = ""; }; @@ -160,6 +163,8 @@ A5D614DF26EBE84F007DDB75 /* RCOfferingAPI.m */, A5D614F326EBE84F007DDB75 /* RCOfferingsAPI.h */, A5D614DD26EBE84E007DDB75 /* RCOfferingsAPI.m */, + 4F3409392A37E5930050EA0E /* RCOtherAPI.h */, + 4F34093A2A37E5930050EA0E /* RCOtherAPI.m */, A5D614E126EBE84F007DDB75 /* RCPackageAPI.h */, A5D614DE26EBE84F007DDB75 /* RCPackageAPI.m */, B3A4C835280DE95000D4AE17 /* RCPromotionalOfferAPI.h */, @@ -287,6 +292,7 @@ 5738F42E2786755E0096D623 /* main.m in Sources */, B32554452825E74000DA62EA /* RCConfigurationAPI.m in Sources */, 2DD7790E270E23870079CBD4 /* RCIntroEligibilityAPI.m in Sources */, + 4F34093B2A37E5930050EA0E /* RCOtherAPI.m in Sources */, 5738F42D278674710096D623 /* RCSubscriptionPeriodAPI.m in Sources */, 2DD77916270E23870079CBD4 /* RCTransactionAPI.m in Sources */, 57918A1628F4C58300BF4963 /* RCPurchasesDiagnosticsAPI.m in Sources */, diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCOtherAPI.h b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCOtherAPI.h new file mode 100644 index 0000000000..e8fc9970af --- /dev/null +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCOtherAPI.h @@ -0,0 +1,18 @@ +// +// RCOtherAPI.h +// ObjCAPITester +// +// Created by Nacho Soto on 6/12/23. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RCOtherAPI : NSObject + ++ (void)checkAPI; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCOtherAPI.m b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCOtherAPI.m new file mode 100644 index 0000000000..7dc4202e58 --- /dev/null +++ b/Tests/APITesters/ObjCAPITester/ObjCAPITester/RCOtherAPI.m @@ -0,0 +1,25 @@ +// +// RCOtherAPI.m +// ObjCAPITester +// +// Created by Nacho Soto on 6/12/23. +// + +@import RevenueCat; +@import UIKit; + +#import "RCOtherAPI.h" + +@implementation RCOtherAPI + ++ (void)checkAPI { + #if DEBUG && TARGET_OS_IPHONE && defined(__IPHONE_17_0) + if (@available(iOS 16.0, *)) { + RCDebugViewController *controller __unused = [RCDebugViewController new]; + + [UIViewController.new rc_presentDebugRevenueCatOverlayAnimated:NO]; + } + #endif +} + +@end diff --git a/Tests/APITesters/SwiftAPITester/SwiftAPITester/OtherAPI.swift b/Tests/APITesters/SwiftAPITester/SwiftAPITester/OtherAPI.swift index be6f142cb2..9d355b84e8 100644 --- a/Tests/APITesters/SwiftAPITester/SwiftAPITester/OtherAPI.swift +++ b/Tests/APITesters/SwiftAPITester/SwiftAPITester/OtherAPI.swift @@ -26,6 +26,15 @@ struct AppView: View { } +@available(iOS 16.0, macOS 13.0, tvOS 16.0, *) +func debugViewController() { + let _: UIViewController = DebugViewController() + UIViewController().presentDebugRevenueCatOverlay() + UIViewController().presentDebugRevenueCatOverlay(animated: false) +} + +#endif + #if os(iOS) && swift(>=5.9) @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) @@ -53,5 +62,3 @@ struct PaywallViews: View { } #endif - -#endif diff --git a/Tests/TestingApps/PurchaseTester/PurchaseTester/InitialViewController.swift b/Tests/TestingApps/PurchaseTester/PurchaseTester/InitialViewController.swift index 81dc00295c..613a262d7c 100644 --- a/Tests/TestingApps/PurchaseTester/PurchaseTester/InitialViewController.swift +++ b/Tests/TestingApps/PurchaseTester/PurchaseTester/InitialViewController.swift @@ -25,25 +25,36 @@ class InitialViewController: UIViewController { print(e.localizedDescription) } + let controllerToPresent: UIViewController + // Route the view depending if we have a premium cat user or not if customerInfo?.entitlements["pro_cat"]?.isActive == true { // if we have a pro_cat subscriber, send them to the cat screen let storyboard = UIStoryboard(name: "Main", bundle: nil) - let controller = storyboard.instantiateViewController(withIdentifier: "cats") - controller.modalPresentationStyle = .fullScreen - self.present(controller, animated: true, completion: nil) - + controllerToPresent = storyboard.instantiateViewController(withIdentifier: "cats") + controllerToPresent.modalPresentationStyle = .fullScreen } else { // if we don't have a pro subscriber, send them to the upsell screen let controller = SwiftPaywall( termsOfServiceUrlString: "https://www.revenuecat.com/terms", - privacyPolicyUrlString: "https://www.revenuecat.com/terms") + privacyPolicyUrlString: "https://www.revenuecat.com/terms" + ) controller.titleLabel.text = "Upsell Screen" controller.subtitleLabel.text = "New cats, unlimited cats, personal cat insights and more!" controller.modalPresentationStyle = .fullScreen - self.present(controller, animated: true, completion: nil) + + controllerToPresent = controller + } + + self.present(controllerToPresent, animated: true, completion: nil) + + // Present debug screen on top + if #available(iOS 16.0, *) { + controllerToPresent.presentDebugRevenueCatOverlay(animated: true) + } else { + print("Won't display debug overlay, not supported before iOS 16.0") } }