Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduced debugRevenueCatOverlay(): new SwiftUI debug overlay #2567

24 changes: 24 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@
4F54DF432A1D8D0700FD72BF /* MockTransactionPoster.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F54DF412A1D8D0700FD72BF /* MockTransactionPoster.swift */; };
4F69EB092A14406E00ED6D4B /* Matchers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F69EB082A14406E00ED6D4B /* Matchers.swift */; };
4F69EB0A2A14406E00ED6D4B /* Matchers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F69EB082A14406E00ED6D4B /* Matchers.swift */; };
4F6BED592A26A14400CD9322 /* DebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6BED582A26A14400CD9322 /* DebugView.swift */; };
4F6BEDD92A26B55C00CD9322 /* DebugViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6BEDD82A26B55C00CD9322 /* DebugViewModel.swift */; };
4F6BEDE02A26B65900CD9322 /* DebugViewSheetPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6BEDDF2A26B65900CD9322 /* DebugViewSheetPresentation.swift */; };
4F6BEDE22A26B69500CD9322 /* DebugContentViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6BEDE12A26B69500CD9322 /* DebugContentViews.swift */; };
4F6BEE1F2A27B02400CD9322 /* CustomEntitlementsComputationIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6BEE022A27ADF900CD9322 /* CustomEntitlementsComputationIntegrationTests.swift */; };
4F6BEE272A27B02400CD9322 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = 4F6BEE092A27B02400CD9322 /* Nimble */; };
4F6BEE282A27B02400CD9322 /* RevenueCat_CustomEntitlementComputation in Frameworks */ = {isa = PBXBuildFile; productRef = 4F6BEE0D2A27B02400CD9322 /* RevenueCat_CustomEntitlementComputation */; };
Expand Down Expand Up @@ -914,6 +918,10 @@
4F54DF3E2A1D8C7500FD72BF /* MockStoreKit2TransactionFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreKit2TransactionFetcher.swift; sourceTree = "<group>"; };
4F54DF412A1D8D0700FD72BF /* MockTransactionPoster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTransactionPoster.swift; sourceTree = "<group>"; };
4F69EB082A14406E00ED6D4B /* Matchers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Matchers.swift; sourceTree = "<group>"; };
4F6BED582A26A14400CD9322 /* DebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugView.swift; sourceTree = "<group>"; };
4F6BEDD82A26B55C00CD9322 /* DebugViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugViewModel.swift; sourceTree = "<group>"; };
4F6BEDDF2A26B65900CD9322 /* DebugViewSheetPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugViewSheetPresentation.swift; sourceTree = "<group>"; };
4F6BEDE12A26B69500CD9322 /* DebugContentViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugContentViews.swift; sourceTree = "<group>"; };
4F6BEE022A27ADF900CD9322 /* CustomEntitlementsComputationIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEntitlementsComputationIntegrationTests.swift; sourceTree = "<group>"; };
4F6BEE312A27B02400CD9322 /* BackendCustomEntitlementsIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BackendCustomEntitlementsIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
4F7DBFBC2A1E986C00A2F511 /* StoreKit2TransactionFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2TransactionFetcher.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1994,6 +2002,7 @@
35E840C1270FB45600899AE2 /* Support */ = {
isa = PBXGroup;
children = (
4F6BED572A26A13800CD9322 /* DebugUI */,
A5F0104D2717B3150090732D /* BeginRefundRequestHelper.swift */,
35E840C5270FB47C00899AE2 /* ManageSubscriptionsHelper.swift */,
578C5F2B28DB82DD00A56F02 /* PurchasesDiagnostics.swift */,
Expand Down Expand Up @@ -2091,6 +2100,17 @@
path = BasicTypes;
sourceTree = "<group>";
};
4F6BED572A26A13800CD9322 /* DebugUI */ = {
isa = PBXGroup;
children = (
4F6BED582A26A14400CD9322 /* DebugView.swift */,
4F6BEDD82A26B55C00CD9322 /* DebugViewModel.swift */,
4F6BEDDF2A26B65900CD9322 /* DebugViewSheetPresentation.swift */,
4F6BEDE12A26B69500CD9322 /* DebugContentViews.swift */,
);
path = DebugUI;
sourceTree = "<group>";
};
4FD291BC2A1E9A180098D1B9 /* StoreKit2 */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -3058,6 +3078,7 @@
2DD58DD827F240EB000FDFE3 /* EmptyFile.swift in Sources */,
2CD72942268A823900BFC976 /* Data+Extensions.swift in Sources */,
5766AA3E283C750300FA6091 /* Operators+Extensions.swift in Sources */,
4F6BEDE22A26B69500CD9322 /* DebugContentViews.swift in Sources */,
B3B5FBBC269D121B00104A0C /* Offerings.swift in Sources */,
9A65E03B25918B0900DE00B0 /* CustomerInfoStrings.swift in Sources */,
57CFB98427FE2258002A6730 /* StoreKit2Setting.swift in Sources */,
Expand Down Expand Up @@ -3253,6 +3274,7 @@
B35042C626CDD3B100905B95 /* PurchasesDelegate.swift in Sources */,
0313FD41268A506400168386 /* DateProvider.swift in Sources */,
57FDAABA284937A0009A48F1 /* SandboxEnvironmentDetector.swift in Sources */,
4F6BED592A26A14400CD9322 /* DebugView.swift in Sources */,
5733B18E27FF586A00EC2045 /* BackendError.swift in Sources */,
B39E811A268E849900D31189 /* AttributionNetwork.swift in Sources */,
57488C7629CB90F90000EE7E /* CustomerInfo+OfflineEntitlements.swift in Sources */,
Expand All @@ -3269,6 +3291,8 @@
B34605C1279A6E380031CA74 /* NetworkOperation.swift in Sources */,
57488C7829CB90F90000EE7E /* PurchasedSK2Product.swift in Sources */,
5766AA56283D4C5400FA6091 /* IgnoreHashable.swift in Sources */,
4F6BEDE02A26B65900CD9322 /* DebugViewSheetPresentation.swift in Sources */,
4F6BEDD92A26B55C00CD9322 /* DebugViewModel.swift in Sources */,
5766C622282DAA700067D886 /* GetIntroEligibilityResponse.swift in Sources */,
35E840CC270FB70D00899AE2 /* ManageSubscriptionsHelper.swift in Sources */,
6E38843A0CAFD551013D0A3F /* StoreProduct.swift in Sources */,
Expand Down
7 changes: 6 additions & 1 deletion Sources/Misc/SystemInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,13 @@ class SystemInfo {

#if os(iOS) || os(tvOS)
var sharedUIApplication: UIApplication? {
UIApplication.value(forKey: "sharedApplication") as? UIApplication
return Self.sharedUIApplication
}

static var sharedUIApplication: UIApplication? {
return UIApplication.value(forKey: "sharedApplication") as? UIApplication
}

#endif

static func isAppleSubscription(managementURL: URL) -> Bool {
Expand Down
12 changes: 12 additions & 0 deletions Sources/Purchasing/Purchases/Purchases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1413,6 +1413,10 @@ internal extension Purchases {
return self.systemInfo.isSandbox
}

var observerMode: Bool {
return self.systemInfo.observerMode
}

var configuredUserDefaults: UserDefaults {
return self.userDefaults
}
Expand All @@ -1421,6 +1425,14 @@ internal extension Purchases {
return self.backend.offlineCustomerInfoEnabled
}

var storeKit2Setting: StoreKit2Setting {
return self.systemInfo.storeKit2Setting
}

var responseVerificationMode: Signing.ResponseVerificationMode {
return self.systemInfo.responseVerificationMode
}

var publicKey: Signing.PublicKey? {
return self.systemInfo.responseVerificationMode.publicKey
}
Expand Down
255 changes: 255 additions & 0 deletions Sources/Support/DebugUI/DebugContentViews.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
//
// 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
//
// DebugViewContent.swift
//
// Created by Nacho Soto on 5/30/23.

#if DEBUG && os(iOS) && swift(>=5.8)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we support macos as well? It probably shouldn't be a bottom sheet though... We can think about it and add in a future PR though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we'd have to make it slightly different cause that API isn't available there 👍🏻


import SwiftUI

@available(iOS 16.0, *)
struct DebugSwiftUIRootView: View {

@StateObject
private var model = DebugViewModel()

var body: some View {
NavigationStack(path: self.$model.navigationPath) {
DebugSummaryView(model: self.model)
.navigationDestination(for: DebugViewPath.self) { path in
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New SwiftUI navigation API 🤓

switch path {
case let .offering(offering):
DebugOfferingView(offering: offering)

case let .package(package):
DebugPackageView(package: package)
}
}
.background(
Rectangle()
.foregroundStyle(Material.thinMaterial)
.edgesIgnoringSafeArea(.all)
)
}
.task {
await self.model.load()
}
}

}

private enum DebugViewPath: Hashable {

case offering(Offering)
case package(Package)

}

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
private struct DebugSummaryView: View {

@ObservedObject
var model: DebugViewModel

var body: some View {
List {
self.diagnosticsSection

self.configurationSection

#if !ENABLE_CUSTOM_ENTITLEMENT_COMPUTATION
self.customerInfoSection
#endif

self.offeringsSection
}
.listStyle(.insetGrouped)
.scrollContentBackground(.hidden)
.navigationTitle("RevenueCat Debug View")
}

private var diagnosticsSection: some View {
Section("Diagnostics") {
LabeledContent("Status") {
HStack {
Text(self.model.diagnosticsStatus)
self.model.diagnosticsIcon
}
}
}
}

private var configurationSection: some View {
Section("Configuration") {
switch self.model.configuration {
case .loading:
Text("Loading...")

case let .loaded(config):
LabeledContent("SDK version", value: SystemInfo.frameworkVersion)
LabeledContent("Observer mode", value: config.observerMode.description)
LabeledContent("Sandbox", value: config.sandbox.description)
LabeledContent("StoreKit 2", value: config.storeKit2Enabled ? "on" : "off")
LabeledContent("Offline Customer Info",
value: config.offlineCustomerInfoSupport ? "enabled" : "disabled")
LabeledContent("Entitlement Verification Mode", value: config.verificationMode.display)
}
}
}

#if !ENABLE_CUSTOM_ENTITLEMENT_COMPUTATION
private var customerInfoSection: some View {
Section("Customer Info") {
switch self.model.customerInfo {
case .loading:
Text("Loading...")

case let .loaded(info):
LabeledContent("User ID", value: info.originalAppUserId)
LabeledContent("Active Entitlements", value: info.entitlements.active.count.description)

if let latestExpiration = info.latestExpirationDate {
LabeledContent("Latest Expiration Date",
value: latestExpiration.formatted(date: .abbreviated,
time: .omitted))
}

case let .failed(error):
Text("Error loading customer info: \(error.localizedDescription)")
}
}
}
#endif

@ViewBuilder
private var offeringsSection: some View {
Section("Offerings") {
switch self.model.offerings {
case .loading:
Text("Loading...")

case let .loaded(offerings):
ForEach(Array(offerings.all.values)) { offering in
NavigationLink(value: DebugViewPath.offering(offering)) {
VStack {
LabeledContent(
offering.identifier,
value: "\(offering.availablePackages.count) package(s)"
)
}
}
}

case let .failed(error):
Text("Error loading offerings: \(error.localizedDescription)")
}
}
}

}

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
private struct DebugOfferingView: View {

var offering: Offering

var body: some View {
List {
Section("Data") {
LabeledContent("Identifier", value: self.offering.id)
LabeledContent("Description", value: self.offering.serverDescription)
}

Section("Packages") {
ForEach(self.offering.availablePackages) { package in
NavigationLink(value: DebugViewPath.package(package)) {
Text(package.identifier)
}
}
}
}
.navigationTitle("Offering")
}

}

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
private struct DebugPackageView: View {

var package: Package

@State private var error: NSError? {
didSet {
if self.error != nil {
self.displayError = true
}
}
}

@State private var displayError: Bool = false
@State private var purchasing: Bool = false

var body: some View {
List {
Section("Data") {
LabeledContent("Identifier", value: self.package.identifier)
LabeledContent("Price", value: self.package.localizedPriceString)
LabeledContent("Product", value: self.package.storeProduct.productIdentifier)
LabeledContent("Type", value: self.package.packageType.description ?? "")
}

Section("Purchasing") {
Button {
_ = Task<Void, Never> {
do {
self.purchasing = true
try await self.purchase()
} catch {
self.error = error as NSError
}

self.purchasing = false
}
} label: {
Text("Purchase")
}
.disabled(self.purchasing)
}
}
.navigationTitle("Package")
.alert(
"Error",
isPresented: self.$displayError,
presenting: self.error
) { error in
Text(error.localizedDescription)
}
}

private func purchase() async throws {
_ = try await Purchases.shared.purchase(package: self.package)
}

}

private extension Signing.ResponseVerificationMode {

var display: String {
switch self {
case .disabled: return "disabled"
case .informational: return "informational"
case .enforced: return "enforced"
}
}

}

#endif
Loading