mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-10 22:45:55 +05:45
### Changes: - Exposed `internal`-only `Purchases.cachePresentedOfferingIdentifier` - Changed `SwiftUI` extensions to `static` methods: this is required to be able to call an instance method on them - Added integration test to verify this offering identifier is used Technically this is an API change, but these APIs are implicitly beta because they're only available in Xcode 15 beta.
416 lines
13 KiB
Swift
416 lines
13 KiB
Swift
//
|
|
// 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 && swift(>=5.8) && (os(iOS) || os(macOS))
|
|
|
|
import StoreKit
|
|
import SwiftUI
|
|
|
|
#if os(macOS)
|
|
import AppKit
|
|
#endif
|
|
|
|
// swiftlint:disable file_length
|
|
|
|
@available(iOS 16.0, macOS 13.0, *)
|
|
struct DebugSwiftUIRootView: View {
|
|
|
|
@StateObject
|
|
private var model = DebugViewModel()
|
|
|
|
@Environment(\.presentationMode)
|
|
private var presentationMode
|
|
|
|
var body: some View {
|
|
NavigationStack(path: self.$model.navigationPath) {
|
|
DebugSummaryView(model: self.model)
|
|
.navigationDestination(for: DebugViewPath.self) { path in
|
|
switch path {
|
|
case let .offering(offering):
|
|
DebugOfferingView(offering: offering)
|
|
|
|
case let .package(package):
|
|
DebugPackageView(package: package)
|
|
}
|
|
}
|
|
.background(
|
|
Rectangle()
|
|
.foregroundStyle(Material.thinMaterial)
|
|
.edgesIgnoringSafeArea(.all)
|
|
)
|
|
#if os(macOS) || targetEnvironment(macCatalyst)
|
|
.toolbar {
|
|
ToolbarItem(placement: .automatic) {
|
|
Button {
|
|
self.presentationMode.wrappedValue.dismiss()
|
|
} label: {
|
|
Label("Close", systemImage: "xmark")
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
.task {
|
|
await self.model.load()
|
|
}
|
|
}
|
|
|
|
static let cornerRadius: CGFloat = 24
|
|
|
|
}
|
|
|
|
private enum DebugViewPath: Hashable {
|
|
|
|
case offering(Offering)
|
|
case package(Package)
|
|
|
|
}
|
|
|
|
@available(iOS 16.0, macOS 13.0, *)
|
|
internal 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
|
|
}
|
|
#if os(iOS)
|
|
.listStyle(.insetGrouped)
|
|
#endif
|
|
.scrollContentBackground(.hidden)
|
|
.navigationTitle("RevenueCat Debug")
|
|
}
|
|
|
|
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):
|
|
Group {
|
|
LabeledContent("SDK version", value: config.sdkVersion)
|
|
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)
|
|
LabeledContent("Receipt URL", value: config.receiptURL?.absoluteString ?? "")
|
|
#if os(macOS)
|
|
.contextMenu {
|
|
Button {
|
|
if let url = config.receiptURL {
|
|
NSWorkspace.shared.selectFile(
|
|
nil,
|
|
inFileViewerRootedAtPath: url.deletingLastPathComponent().path
|
|
)
|
|
}
|
|
} label: {
|
|
Text("Show in Finder")
|
|
}
|
|
}
|
|
#endif
|
|
|
|
ShareLink(item: config, preview: .init("Configuration")) {
|
|
Label("Share", systemImage: "square.and.arrow.up")
|
|
}
|
|
}
|
|
.textSelection(.enabled)
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
}
|
|
|
|
#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: self.model.currentAppUserID ?? "")
|
|
LabeledContent("Original 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, *)
|
|
private struct DebugOfferingView: View {
|
|
|
|
@State private var showingSubscriptionSheet = false
|
|
@State private var showingStoreSheet = false
|
|
|
|
var offering: Offering
|
|
|
|
var body: some View {
|
|
if #available(iOS 17.0, macOS 14.0, tvOS 17.0, *) {
|
|
content
|
|
#if swift(>=5.9)
|
|
.onInAppPurchaseCompletion { _, _ in
|
|
self.showingSubscriptionSheet = false
|
|
self.showingStoreSheet = false
|
|
}
|
|
#endif
|
|
} else {
|
|
content
|
|
}
|
|
}
|
|
|
|
private var content: 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
if #available(iOS 17.0, macOS 14.0, tvOS 17.0, *) {
|
|
Section("Paywalls") {
|
|
Button {
|
|
self.showingSubscriptionSheet = true
|
|
} label: {
|
|
Text("Display SubscriptionStoreView")
|
|
}
|
|
|
|
Button {
|
|
self.showingStoreSheet = true
|
|
} label: {
|
|
Text("Display StoreView")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Offering")
|
|
#if swift(>=5.9)
|
|
.sheet(isPresented: self.$showingSubscriptionSheet) {
|
|
if #available(iOS 17.0, macOS 14.0, tvOS 17.0, *) {
|
|
self.subscriptionStoreView
|
|
}
|
|
}
|
|
.sheet(isPresented: self.$showingStoreSheet) {
|
|
if #available(iOS 17.0, macOS 14.0, tvOS 17.0, *) {
|
|
StoreView.forOffering(self.offering)
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if swift(>=5.9)
|
|
@available(iOS 17.0, macOS 14.0, tvOS 17.0, *)
|
|
private var subscriptionStoreView: some View {
|
|
SubscriptionStoreView.forOffering(self.offering) {
|
|
VStack {
|
|
VStack {
|
|
Text("🐈")
|
|
Text("RevenueCat Demo Paywall")
|
|
}
|
|
.font(.title)
|
|
|
|
Text(self.offering.getMetadataValue(for: "title", default: "Premium Access"))
|
|
.font(.title2)
|
|
.foregroundStyle(.primary)
|
|
|
|
Text(self.offering.getMetadataValue(for: "subtitle",
|
|
default: "Unlimited access to premium content."))
|
|
.foregroundStyle(.secondary)
|
|
.font(.subheadline)
|
|
}
|
|
.containerBackground(for: .subscriptionStoreFullHeight) {
|
|
Rectangle()
|
|
.edgesIgnoringSafeArea(.all)
|
|
.foregroundStyle(Color.blue.gradient.quaternary)
|
|
}
|
|
}
|
|
.backgroundStyle(.clear)
|
|
.subscriptionStoreButtonLabel(.multiline)
|
|
.subscriptionStorePickerItemBackground(.thickMaterial)
|
|
}
|
|
#endif
|
|
|
|
}
|
|
|
|
@available(iOS 16.0, macOS 13.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 with RevenueCat")
|
|
}
|
|
|
|
#if swift(>=5.9)
|
|
if #available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) {
|
|
ProductView(id: self.package.storeProduct.productIdentifier)
|
|
.productViewStyle(ProductStyle())
|
|
}
|
|
#endif
|
|
}
|
|
.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)
|
|
}
|
|
|
|
}
|
|
|
|
@available(iOS 16.0, macOS 13.0, *)
|
|
extension DebugViewModel.Configuration: Transferable {
|
|
|
|
static var transferRepresentation: some TransferRepresentation {
|
|
return CodableRepresentation(
|
|
for: DebugViewModel.Configuration.self,
|
|
contentType: .plainText,
|
|
encoder: JSONEncoder.prettyPrinted,
|
|
decoder: JSONDecoder.default
|
|
)
|
|
}
|
|
|
|
}
|
|
|
|
#if swift(>=5.9)
|
|
@available(iOS 17.0, macOS 14.0, *)
|
|
private struct ProductStyle: ProductViewStyle {
|
|
|
|
func makeBody(configuration: ProductViewStyleConfiguration) -> some View {
|
|
switch configuration.state {
|
|
case .loading:
|
|
ProgressView()
|
|
.progressViewStyle(.circular)
|
|
|
|
case .success:
|
|
Button {
|
|
configuration.purchase()
|
|
} label: {
|
|
Text("Purchase with StoreKit")
|
|
}
|
|
|
|
default:
|
|
ProductView(configuration)
|
|
}
|
|
}
|
|
|
|
}
|
|
#endif
|
|
|
|
#endif
|