From 4413503d68d7455c7349f1d80615ce6cf2935902 Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Wed, 15 Jan 2025 08:16:21 -0600 Subject: [PATCH] [Paywalls V2] Added V1 fallback paywall into Paywall V2 error logic (#4666) * Start of some fallback fixes * Better fallback paywall stuff * Lint --- RevenueCatUI/PaywallView.swift | 21 ++++- .../Templates/V2/PaywallsV2View.swift | 81 +++++++++++++------ .../FamilySharingTogglePreview.swift | 4 +- .../MultiTierPreview.swift | 3 +- .../Template1Preview.swift | 3 +- .../PaywallComponentPropertyTypes.swift | 10 +-- 6 files changed, 88 insertions(+), 34 deletions(-) diff --git a/RevenueCatUI/PaywallView.swift b/RevenueCatUI/PaywallView.swift index 20fe14bbb2..ed477f2caf 100644 --- a/RevenueCatUI/PaywallView.swift +++ b/RevenueCatUI/PaywallView.swift @@ -23,6 +23,7 @@ import SwiftUI @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable, message: "RevenueCatUI does not support macOS yet") @available(tvOS, unavailable, message: "RevenueCatUI does not support tvOS yet") +// swiftlint:disable:next type_body_length public struct PaywallView: View { private let contentToDisplay: PaywallViewConfiguration.Content @@ -254,6 +255,23 @@ public struct PaywallView: View { #if PAYWALL_COMPONENTS if let paywallComponents = offering.paywallComponents { + // For fallback view + let paywall: PaywallData = .createDefault(with: offering.availablePackages, + locale: self.locale) + let paywallView = LoadedOfferingPaywallView( + offering: offering, + activelySubscribedProductIdentifiers: activelySubscribedProductIdentifiers, + paywall: paywall, + template: PaywallData.defaultTemplate, + mode: self.mode, + fonts: fonts, + displayCloseButton: self.displayCloseButton, + introEligibility: checker, + purchaseHandler: purchaseHandler, + locale: self.locale, + showZeroDecimalPlacePrices: showZeroDecimalPlacePrices + ) + PaywallsV2View( paywallComponents: paywallComponents, offering: offering, @@ -265,7 +283,8 @@ public struct PaywallView: View { return } onRequestedDismissal() - } + }, + fallbackView: paywallView ) .environmentObject(self.introEligibility) .environmentObject(self.purchaseHandler) diff --git a/RevenueCatUI/Templates/V2/PaywallsV2View.swift b/RevenueCatUI/Templates/V2/PaywallsV2View.swift index e0bd98e425..07501b6c64 100644 --- a/RevenueCatUI/Templates/V2/PaywallsV2View.swift +++ b/RevenueCatUI/Templates/V2/PaywallsV2View.swift @@ -32,7 +32,7 @@ private struct PaywallState { } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -struct PaywallsV2View: View { +struct PaywallsV2View: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass @@ -53,16 +53,19 @@ struct PaywallsV2View: View { private let uiConfigProvider: UIConfigProvider private let offering: Offering private let onDismiss: () -> Void + private let fallbackView: Content public init( paywallComponents: Offering.PaywallComponents, offering: Offering, introEligibilityChecker: TrialOrIntroEligibilityChecker, showZeroDecimalPlacePrices: Bool, - onDismiss: @escaping () -> Void + onDismiss: @escaping () -> Void, + fallbackView: Content ) { let uiConfigProvider = UIConfigProvider(uiConfig: paywallComponents.uiConfig) + self.fallbackView = fallbackView self.paywallComponentsData = paywallComponents.data self.uiConfigProvider = uiConfigProvider self.offering = offering @@ -91,36 +94,64 @@ struct PaywallsV2View: View { } public var body: some View { - switch self.paywallStateManager.state { - case .success(let paywallState): - LoadedPaywallsV2View( - paywallState: paywallState, - uiConfigProvider: self.uiConfigProvider, - onDismiss: self.onDismiss + if let errorInfo = self.paywallComponentsData.errorInfo, !errorInfo.isEmpty { + // Show fallback paywall and debug error message that + // occurred while decoding the paywall + self.fallbackViewWithErrorMessage( + "Error decoding paywall response on: \(errorInfo.keys.joined(separator: ", "))" ) - .environment(\.screenCondition, ScreenCondition.from(self.horizontalSizeClass)) - .environmentObject(self.introOfferEligibilityContext) - .disabled(self.purchaseHandler.actionInProgress) - .onAppear { - self.purchaseHandler.trackPaywallImpression( - self.createEventData() + } else { + switch self.paywallStateManager.state { + case .success(let paywallState): + LoadedPaywallsV2View( + paywallState: paywallState, + uiConfigProvider: self.uiConfigProvider, + onDismiss: self.onDismiss ) - } - .onDisappear { self.purchaseHandler.trackPaywallClose() } - .onChangeOf(self.purchaseHandler.purchased) { purchased in - if purchased { - self.onDismiss() + .environment(\.screenCondition, ScreenCondition.from(self.horizontalSizeClass)) + .environmentObject(self.introOfferEligibilityContext) + .disabled(self.purchaseHandler.actionInProgress) + .onAppear { + self.purchaseHandler.trackPaywallImpression( + self.createEventData() + ) } + .onDisappear { self.purchaseHandler.trackPaywallClose() } + .onChangeOf(self.purchaseHandler.purchased) { purchased in + if purchased { + self.onDismiss() + } + } + .task { + await self.introOfferEligibilityContext.computeEligibility(for: paywallState.packages) + } + case .failure(let error): + // Show fallback paywall and debug error message that + // occurred while validating data and view models + self.fallbackViewWithErrorMessage( + "Error validating paywall: \(error.localizedDescription)" + ) } - .task { - await self.introOfferEligibilityContext.computeEligibility(for: paywallState.packages) - } - case .failure: - // WIP: Need to use fallback paywall - Text("Error creating paywall") } } + @ViewBuilder + func fallbackViewWithErrorMessage(_ errorMessage: String) -> some View { + let fullMessage = """ + \(errorMessage) + Validate your paywall is correct in the RevenueCat dashboard, + update your SDK, or contact RevenueCat support. + View console logs for full detail. + The displayed paywall contains default configuration. + This error will be hidden in production. + """ + + DebugErrorView( + fullMessage, + replacement: self.fallbackView + ) + } + private func createEventData() -> PaywallEvent.Data { return .init( offering: self.offering, diff --git a/RevenueCatUI/Templates/V2/Previews/TemplateComponentsViewPreviews/FamilySharingTogglePreview.swift b/RevenueCatUI/Templates/V2/Previews/TemplateComponentsViewPreviews/FamilySharingTogglePreview.swift index fad0fa366f..930c61f695 100644 --- a/RevenueCatUI/Templates/V2/Previews/TemplateComponentsViewPreviews/FamilySharingTogglePreview.swift +++ b/RevenueCatUI/Templates/V2/Previews/TemplateComponentsViewPreviews/FamilySharingTogglePreview.swift @@ -10,6 +10,7 @@ // TestPaywallPreviews.swift // // Created by Josh Holtz on 9/26/24. +// swiftlint:disable file_length import Foundation import RevenueCat @@ -387,7 +388,8 @@ struct FamilySharingTogglePreview_Previews: PreviewProvider { ]), introEligibilityChecker: .default(), showZeroDecimalPlacePrices: true, - onDismiss: { } + onDismiss: { }, + fallbackView: EmptyView() ) .previewRequiredEnvironmentProperties() .previewLayout(.fixed(width: 400, height: 800)) diff --git a/RevenueCatUI/Templates/V2/Previews/TemplateComponentsViewPreviews/MultiTierPreview.swift b/RevenueCatUI/Templates/V2/Previews/TemplateComponentsViewPreviews/MultiTierPreview.swift index 6ebb30fe29..7bf036441b 100644 --- a/RevenueCatUI/Templates/V2/Previews/TemplateComponentsViewPreviews/MultiTierPreview.swift +++ b/RevenueCatUI/Templates/V2/Previews/TemplateComponentsViewPreviews/MultiTierPreview.swift @@ -397,7 +397,8 @@ struct MultiTierPreview_Previews: PreviewProvider { ]), introEligibilityChecker: .default(), showZeroDecimalPlacePrices: true, - onDismiss: { } + onDismiss: { }, + fallbackView: EmptyView() ) .previewRequiredEnvironmentProperties() .previewLayout(.fixed(width: 400, height: 800)) diff --git a/RevenueCatUI/Templates/V2/Previews/TemplateComponentsViewPreviews/Template1Preview.swift b/RevenueCatUI/Templates/V2/Previews/TemplateComponentsViewPreviews/Template1Preview.swift index 55954347f0..8f9e2ca33e 100644 --- a/RevenueCatUI/Templates/V2/Previews/TemplateComponentsViewPreviews/Template1Preview.swift +++ b/RevenueCatUI/Templates/V2/Previews/TemplateComponentsViewPreviews/Template1Preview.swift @@ -205,7 +205,8 @@ struct Template1Preview_Previews: PreviewProvider { availablePackages: [package]), introEligibilityChecker: .default(), showZeroDecimalPlacePrices: true, - onDismiss: { } + onDismiss: { }, + fallbackView: EmptyView() ) .previewRequiredEnvironmentProperties() .previewLayout(.fixed(width: 400, height: 800)) diff --git a/Sources/Paywalls/Components/Common/PaywallComponentPropertyTypes.swift b/Sources/Paywalls/Components/Common/PaywallComponentPropertyTypes.swift index 422f40d9c0..e8a6391030 100644 --- a/Sources/Paywalls/Components/Common/PaywallComponentPropertyTypes.swift +++ b/Sources/Paywalls/Components/Common/PaywallComponentPropertyTypes.swift @@ -152,7 +152,7 @@ public extension PaywallComponent { switch self { case .rectangle(let corners): try container.encode(ShapeType.rectangle.rawValue, forKey: .type) - try container.encode(corners, forKey: .corners) + try container.encodeIfPresent(corners, forKey: .corners) case .pill: try container.encode(ShapeType.pill.rawValue, forKey: .type) } @@ -200,7 +200,7 @@ public extension PaywallComponent { switch self { case .rectangle(let corners): try container.encode(ShapeType.rectangle.rawValue, forKey: .type) - try container.encode(corners, forKey: .corners) + try container.encodeIfPresent(corners, forKey: .corners) case .circle: try container.encode(ShapeType.circle.rawValue, forKey: .type) } @@ -249,14 +249,14 @@ public extension PaywallComponent { switch self { case .rectangle(let corners): - try container.encode(MaskShapeType.rectangle.rawValue, forKey: .type) + try container.encodeIfPresent(MaskShapeType.rectangle.rawValue, forKey: .type) try container.encode(corners, forKey: .corners) case .pill: try container.encode(MaskShapeType.pill.rawValue, forKey: .type) case .concave: - try container.encode(MaskShapeType.pill.rawValue, forKey: .type) + try container.encode(MaskShapeType.concave.rawValue, forKey: .type) case .convex: - try container.encode(MaskShapeType.pill.rawValue, forKey: .type) + try container.encode(MaskShapeType.convex.rawValue, forKey: .type) } }