Skip to content

Commit

Permalink
Paywalls tester with sandbox purchases (#4024)
Browse files Browse the repository at this point in the history
Restore's Paywall Tester's ability to make sandbox purchases. You must
include the Paywalls test app's API key in `LocalConfigItems.swift`. If
it doesn't work initially try deleting the app from your simulator (or
device) and try again.




![image](https://github.com/RevenueCat/purchases-ios/assets/109382862/c3e81929-64f0-4ffc-8570-bc94c57951cf)
  • Loading branch information
jamesrb1 authored Jul 5, 2024
1 parent 41d3541 commit 23e96a7
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
4FCA01FB2A3A1CBD00B262C0 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4FCA01FA2A3A1CBD00B262C0 /* StoreKit.framework */; };
4FDF11202A7270F3004F3680 /* SamplePaywallsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FDF111F2A7270F3004F3680 /* SamplePaywallsList.swift */; };
880B2AF22BEC2D62006B9393 /* Preprocessor.sh in Resources */ = {isa = PBXBuildFile; fileRef = 880B2AF12BEC2D62006B9393 /* Preprocessor.sh */; };
88A543E92C387DB00039C6A5 /* APIKeyDashboardList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A543E82C387DB00039C6A5 /* APIKeyDashboardList.swift */; };
88B2F9882BE1943C00B43E0B /* ManagePaywallButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B2F9872BE1943C00B43E0B /* ManagePaywallButton.swift */; };
88B2F98B2BE19B1200B43E0B /* OfferingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B2F98A2BE19B1200B43E0B /* OfferingButton.swift */; };
88B2F98D2BE3F1E900B43E0B /* TemplateInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B2F98C2BE3F1E900B43E0B /* TemplateInfo.swift */; };
Expand Down Expand Up @@ -95,6 +96,7 @@
4FFD2A602AA154B4001F4B0C /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
880B2AF12BEC2D62006B9393 /* Preprocessor.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = Preprocessor.sh; sourceTree = SOURCE_ROOT; };
880B2AF32BEC35AA006B9393 /* Postprocessor.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = Postprocessor.sh; sourceTree = SOURCE_ROOT; };
88A543E82C387DB00039C6A5 /* APIKeyDashboardList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIKeyDashboardList.swift; sourceTree = "<group>"; };
88B2F9872BE1943C00B43E0B /* ManagePaywallButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagePaywallButton.swift; sourceTree = "<group>"; };
88B2F98A2BE19B1200B43E0B /* OfferingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferingButton.swift; sourceTree = "<group>"; };
88B2F98C2BE3F1E900B43E0B /* TemplateInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateInfo.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -227,6 +229,7 @@
88B2F9892BE1944200B43E0B /* OfferingList */ = {
isa = PBXGroup;
children = (
88A543E82C387DB00039C6A5 /* APIKeyDashboardList.swift */,
4FC6F8B12A7403D3002139B2 /* OfferingsList.swift */,
88B2F9872BE1943C00B43E0B /* ManagePaywallButton.swift */,
88B2F98A2BE19B1200B43E0B /* OfferingButton.swift */,
Expand Down Expand Up @@ -424,6 +427,7 @@
88BEB9292BF537F200A3D05F /* LocalConfigItems.swift in Sources */,
88DFC1882BCF3AB700273B6D /* LoginScreen.swift in Sources */,
88DFC1892BCF3AB700273B6D /* LoginWall.swift in Sources */,
88A543E92C387DB00039C6A5 /* APIKeyDashboardList.swift in Sources */,
88BEB9262BF537F200A3D05F /* ConfigItem.swift in Sources */,
88BEB9272BF537F200A3D05F /* Configuration.swift in Sources */,
88DFC1932BCF490400273B6D /* OfferingsResponse.swift in Sources */,
Expand Down Expand Up @@ -626,7 +630,7 @@
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0;
OTHER_SWIFT_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.app;
PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.PaywallsTester;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator watchsimulator watchos";
Expand Down Expand Up @@ -676,7 +680,7 @@
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0;
OTHER_SWIFT_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.app;
PRODUCT_BUNDLE_IDENTIFIER = com.revenuecat.PaywallsTester;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator watchsimulator watchos";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ struct AppContentView: View {
Label("My Apps", systemImage: "network")
}

if Purchases.isConfigured {
APIKeyDashboardList()
.tabItem {
Label("Sandbox Paywalls", systemImage: "testtube.2")
}
}

#if !DEBUG
if !Purchases.isConfigured {
Text("Purchases is not configured")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
//
// APIKeyDashboardList.swift
// SimpleApp
//
// Created by Nacho Soto on 7/27/23.
//

import RevenueCat
#if DEBUG
@testable import RevenueCatUI
#else
import RevenueCatUI
#endif
import SwiftUI

struct APIKeyDashboardList: View {

fileprivate struct Template: Hashable {
var name: String?
}

fileprivate struct Data: Hashable {
var sections: [Template]
var offeringsBySection: [Template: [Offering]]
}

fileprivate struct PresentedPaywall: Hashable {
var offering: Offering
var mode: PaywallViewMode
}

@State
private var offerings: Result<Data, NSError>?

@State
private var presentedPaywall: PresentedPaywall?

var body: some View {
NavigationView {
self.content
.navigationTitle("Live Paywalls")
}
.task {
do {
let offerings = try await Purchases.shared.offerings()
.all
.map(\.value)
.sorted { $0.serverDescription > $1.serverDescription }

let offeringsBySection = Dictionary(
grouping: offerings,
by: { Template(name: $0.paywall?.templateName) }
)

self.offerings = .success(
.init(
sections: Array(offeringsBySection.keys).sorted { $0.description < $1.description },
offeringsBySection: offeringsBySection
)
)
} catch let error as NSError {
self.offerings = .failure(error)
}
}
}

@ViewBuilder
private var content: some View {
switch self.offerings {
case let .success(data):
VStack {
Text(Self.modesInstructions)
.font(.footnote)
self.list(with: data)
}

case let .failure(error):
Text(error.description)

case .none:
SwiftUI.ProgressView()
}
}

@ViewBuilder
private func list(with data: Data) -> some View {
List {
ForEach(data.sections, id: \.self) { template in
Section {
ForEach(data.offeringsBySection[template]!, id: \.id) { offering in
if let paywall = offering.paywall {
#if targetEnvironment(macCatalyst)
NavigationLink(
destination: PaywallPresenter(offering: offering,
mode: .default,
displayCloseButton: false),
tag: PresentedPaywall(offering: offering, mode: .default),
selection: self.$presentedPaywall
) {
OfferButton(offering: offering, paywall: paywall) {}
.contextMenu {
self.contextMenu(for: offering)
}
}
#else
OfferButton(offering: offering, paywall: paywall) {
self.presentedPaywall = .init(offering: offering, mode: .default)
}
#if !os(watchOS)
.contextMenu {
self.contextMenu(for: offering)
}
#endif
#endif
} else {
Text(offering.serverDescription)
}
}
} header: {
Text(verbatim: template.description)
}
}
}
.sheet(item: self.$presentedPaywall) { paywall in
PaywallPresenter(offering: paywall.offering, mode: paywall.mode, introEligility: .eligible)
.onRestoreCompleted { _ in
self.presentedPaywall = nil
}
}
}

#if !os(watchOS)
@ViewBuilder
private func contextMenu(for offering: Offering) -> some View {
ForEach(PaywallViewMode.allCases, id: \.self) { mode in
self.button(for: mode, offering: offering)
}
}
#endif

@ViewBuilder
private func button(for selectedMode: PaywallViewMode, offering: Offering) -> some View {
Button {
self.presentedPaywall = .init(offering: offering, mode: selectedMode)
} label: {
Text(selectedMode.name)
Image(systemName: selectedMode.icon)
}
}

private struct OfferButton: View {
let offering: Offering
let paywall: PaywallData
let action: () -> Void

var body: some View {
Button(action: action) {
Text(self.offering.serverDescription)
}
.buttonStyle(.plain)
.contentShape(Rectangle())
}
}

#if targetEnvironment(macCatalyst)
private static let modesInstructions = "Right click or ⌘ + click to open in different modes."
#else
private static let modesInstructions = "Press and hold to open in different modes."
#endif

}

extension APIKeyDashboardList.Template: CustomStringConvertible {

var description: String {
if let name = self.name {
#if DEBUG
if let template = PaywallTemplate(rawValue: name) {
return template.name
} else {
return "Unrecognized template"
}
#else
return name
#endif
} else {
return "No paywall"
}
}

}

extension APIKeyDashboardList.PresentedPaywall: Identifiable {

var id: String {
return "\(self.offering.id)-\(self.mode.name)"
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,7 @@ struct PaywallPresenter: View {
switch self.mode {
case .fullScreen:

let handler = PurchaseHandler.default(
performPurchase: { package in
var userCancelled = false
var error: Error?

// do stuff

return (userCancelled: userCancelled, error: error)

}, performRestore: {
var success = false
var error: Error?

// do stuff

return (success: success, error: error)
})
let handler = PurchaseHandler.default()

let configuration = PaywallViewConfiguration(
offering: offering,
Expand All @@ -48,8 +32,6 @@ struct PaywallPresenter: View {

PaywallView(configuration: configuration)



#if !os(watchOS)
case .footer:
CustomPaywallContent()
Expand All @@ -64,7 +46,7 @@ struct PaywallPresenter: View {
customerInfo: nil,
condensed: true,
introEligibility: .producing(eligibility: introEligility),
purchaseHandler: .default())
purchaseHandler: .default())
#endif
}
}
Expand Down

0 comments on commit 23e96a7

Please sign in to comment.