Skip to content

Commit

Permalink
Store decoding errors in individual paywall
Browse files Browse the repository at this point in the history
  • Loading branch information
joshdholtz committed Nov 13, 2024
1 parent b70cd2e commit 9d4f465
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 104 deletions.
4 changes: 4 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
2C91068C2CE22D4F00189565 /* JustifyContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C91068B2CE22D4F00189565 /* JustifyContent.swift */; };
2C91068E2CE2481A00189565 /* SizeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C91068D2CE2481800189565 /* SizeModifier.swift */; };
2C9107E02CE2E33400189565 /* StoredEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6FEE42AA940B700780B45 /* StoredEvent.swift */; };
2C9107F02CE2ED8700189565 /* PaywallComponentsData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9107EF2CE2ED8300189565 /* PaywallComponentsData.swift */; };
2CAB87F72CAAB13200247013 /* Shape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAB87F62CAAB13200247013 /* Shape.swift */; };
2CB8CF9327BF538F00C34DE3 /* PlatformInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB8CF9227BF538F00C34DE3 /* PlatformInfo.swift */; };
2CC791552CC0452100FBE120 /* PurchaseButtonComponentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC791522CC0452100FBE120 /* PurchaseButtonComponentViewModel.swift */; };
Expand Down Expand Up @@ -1227,6 +1228,7 @@
2C9106892CE22D3500189565 /* FlexVStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexVStack.swift; sourceTree = "<group>"; };
2C91068B2CE22D4F00189565 /* JustifyContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustifyContent.swift; sourceTree = "<group>"; };
2C91068D2CE2481800189565 /* SizeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SizeModifier.swift; sourceTree = "<group>"; };
2C9107EF2CE2ED8300189565 /* PaywallComponentsData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallComponentsData.swift; sourceTree = "<group>"; };
2CAB87F62CAAB13200247013 /* Shape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shape.swift; sourceTree = "<group>"; };
2CB8CF9227BF538F00C34DE3 /* PlatformInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformInfo.swift; sourceTree = "<group>"; };
2CC7914B2CC0452100FBE120 /* PackageComponentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageComponentView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4050,6 +4052,7 @@
5774F9B52805E6CC00997128 /* CustomerInfoResponse.swift */,
5766C621282DAA700067D886 /* GetIntroEligibilityResponse.swift */,
57D5412D27F6311C004CC35C /* OfferingsResponse.swift */,
2C9107EF2CE2ED8300189565 /* PaywallComponentsData.swift */,
574A2F4A282D7AEA00150D40 /* PostOfferResponse.swift */,
57488A7E29CA145B0000EE7E /* ProductEntitlementMappingResponse.swift */,
);
Expand Down Expand Up @@ -5824,6 +5827,7 @@
57488C7629CB90F90000EE7E /* CustomerInfo+OfflineEntitlements.swift in Sources */,
37E35C8515C5E2D01B0AF5C1 /* Strings.swift in Sources */,
4F5C05BD2A43A21A00651C7D /* Locale+Extensions.swift in Sources */,
2C9107F02CE2ED8700189565 /* PaywallComponentsData.swift in Sources */,
2D9F4A5526C30CA800B07B43 /* PurchasesOrchestrator.swift in Sources */,
57C2931528BFEF4F0054EDFC /* PurchasesError.swift in Sources */,
57FD7B1528DA4037009CA4E4 /* PurchasesType.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ struct TemplateComponentsView: View {
self.paywallComponentsData = paywallComponentsData
self.onDismiss = onDismiss

guard (self.paywallComponentsData.errorInfo ?? [:]).isEmpty else {
self.componentViewModel = Self.fallbackPaywallViewModels()
self._paywallState = .init(wrappedValue: PaywallState(selectedPackage: nil))

return
}

// Step 0: Decide which ComponentsConfig to use (base is default)
let componentsConfig = paywallComponentsData.componentsConfig.base

Expand Down
103 changes: 0 additions & 103 deletions Sources/Networking/Responses/OfferingsResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,109 +14,6 @@

import Foundation

#if PAYWALL_COMPONENTS

public struct PaywallComponentsData: Codable, Equatable, Sendable {

public struct ComponentsConfig: Codable, Equatable, Sendable {

public var base: PaywallComponentsConfig

public init(base: PaywallComponentsConfig) {
self.base = base
}

}

public struct PaywallComponentsConfig: Codable, Equatable, Sendable {

public var stack: PaywallComponent.StackComponent
public let stickyFooter: PaywallComponent.StickyFooterComponent?

public init(
stack: PaywallComponent.StackComponent,
stickyFooter: PaywallComponent.StickyFooterComponent?
) {
self.stack = stack
self.stickyFooter = stickyFooter
}

}

public enum LocalizationData: Codable, Equatable, Sendable {
case string(String), image(PaywallComponent.ThemeImageUrls)

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let stringValue = try? container.decode(String.self) {
self = .string(stringValue)
} else if let imageValue = try? container.decode(PaywallComponent.ThemeImageUrls.self) {
self = .image(imageValue)
} else {
throw DecodingError.typeMismatch(
LocalizationData.self,
DecodingError.Context(codingPath: decoder.codingPath,
debugDescription: "Wrong type for LocalizationData")
)
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let stringValue):
try container.encode(stringValue)
case .image(let imageValue):
try container.encode(imageValue)
}
}
}

public var templateName: String

/// The base remote URL where assets for this paywall are stored.
public var assetBaseURL: URL

/// The revision identifier for this paywall.
public var revision: Int {
get { return self._revision }
set { self._revision = newValue }
}

public var componentsConfig: ComponentsConfig
public var componentsLocalizations: [PaywallComponent.LocaleID: PaywallComponent.LocalizationDictionary]
public var defaultLocale: String

@DefaultDecodable.Zero
internal private(set) var _revision: Int = 0

private enum CodingKeys: String, CodingKey {
case templateName
case componentsConfig
case componentsLocalizations
case defaultLocale
case assetBaseURL = "assetBaseUrl"
case _revision = "revision"
}

public init(templateName: String,
assetBaseURL: URL,
componentsConfig: ComponentsConfig,
componentsLocalizations: [PaywallComponent.LocaleID: PaywallComponent.LocalizationDictionary],
revision: Int,
defaultLocaleIdentifier: String) {
self.templateName = templateName
self.assetBaseURL = assetBaseURL
self.componentsConfig = componentsConfig
self.componentsLocalizations = componentsLocalizations
self._revision = revision
self.defaultLocale = defaultLocaleIdentifier
}

}

#endif

struct OfferingsResponse {

struct Offering {
Expand Down
207 changes: 207 additions & 0 deletions Sources/Networking/Responses/PaywallComponentsData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
//
// 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
//
// PaywallComponentsData.swift
//
// Created by Josh Holtz on 11/11/24.

import Foundation

#if PAYWALL_COMPONENTS

public struct PaywallComponentsData: Codable, Equatable, Sendable {

public struct ComponentsConfig: Codable, Equatable, Sendable {

public var base: PaywallComponentsConfig

public init(base: PaywallComponentsConfig) {
self.base = base
}

}

public struct PaywallComponentsConfig: Codable, Equatable, Sendable {

public var stack: PaywallComponent.StackComponent
public let stickyFooter: PaywallComponent.StickyFooterComponent?

public init(
stack: PaywallComponent.StackComponent,
stickyFooter: PaywallComponent.StickyFooterComponent?
) {
self.stack = stack
self.stickyFooter = stickyFooter
}

}

public enum LocalizationData: Codable, Equatable, Sendable {
case string(String), image(PaywallComponent.ThemeImageUrls)

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let stringValue = try? container.decode(String.self) {
self = .string(stringValue)
} else if let imageValue = try? container.decode(PaywallComponent.ThemeImageUrls.self) {
self = .image(imageValue)
} else {
throw DecodingError.typeMismatch(
LocalizationData.self,
DecodingError.Context(codingPath: decoder.codingPath,
debugDescription: "Wrong type for LocalizationData")
)
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let stringValue):
try container.encode(stringValue)
case .image(let imageValue):
try container.encode(imageValue)
}
}
}

public var templateName: String

/// The base remote URL where assets for this paywall are stored.
public var assetBaseURL: URL

/// The revision identifier for this paywall.
public var revision: Int {
get { return self._revision }
set { self._revision = newValue }
}

public var componentsConfig: ComponentsConfig
public var componentsLocalizations: [PaywallComponent.LocaleID: PaywallComponent.LocalizationDictionary]
public var defaultLocale: String

@DefaultDecodable.Zero
internal private(set) var _revision: Int = 0

public var errorInfo: [String: EquatableError]? = nil

private enum CodingKeys: String, CodingKey {
case templateName
case componentsConfig
case componentsLocalizations
case defaultLocale
case assetBaseURL = "assetBaseUrl"
case _revision = "revision"
}

public init(templateName: String,
assetBaseURL: URL,
componentsConfig: ComponentsConfig,
componentsLocalizations: [PaywallComponent.LocaleID: PaywallComponent.LocalizationDictionary],
revision: Int,
defaultLocaleIdentifier: String) {
self.templateName = templateName
self.assetBaseURL = assetBaseURL
self.componentsConfig = componentsConfig
self.componentsLocalizations = componentsLocalizations
self._revision = revision
self.defaultLocale = defaultLocaleIdentifier
}

}

extension PaywallComponentsData {

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
var errors: [String: EquatableError] = [:]

do {
templateName = try container.decode(String.self, forKey: .templateName)
} catch {
errors["templateName"] = .init(error)
templateName = ""
}

do {
assetBaseURL = try container.decode(URL.self, forKey: .assetBaseURL)
} catch {
errors["assetBaseURL"] = .init(error)
assetBaseURL = URL(string: "https://example.com")!
}

do {
componentsConfig = try container.decode(ComponentsConfig.self, forKey: .componentsConfig)
} catch {
errors["componentsConfig"] = .init(error)
componentsConfig = ComponentsConfig(base: PaywallComponentsConfig(
stack: .init(components: []),
stickyFooter: nil
))
}

do {
componentsLocalizations = try container.decode(
[PaywallComponent.LocaleID: PaywallComponent.LocalizationDictionary].self,
forKey: .componentsLocalizations
)
} catch {
errors["componentsLocalizations"] = .init(error)
componentsLocalizations = [:]
}

do {
defaultLocale = try container.decode(String.self, forKey: .defaultLocale)
} catch {
errors["defaultLocale"] = .init(error)
defaultLocale = "en"
}

do {
_revision = try container.decode(Int.self, forKey: ._revision)
} catch {
errors["_revision"] = .init(error)
_revision = 0
}

if !errors.isEmpty {
errorInfo = errors
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(templateName, forKey: .templateName)
try container.encode(assetBaseURL, forKey: .assetBaseURL)
try container.encode(componentsConfig, forKey: .componentsConfig)
try container.encode(componentsLocalizations, forKey: .componentsLocalizations)
try container.encode(defaultLocale, forKey: .defaultLocale)
try container.encode(_revision, forKey: ._revision)
}

}

extension PaywallComponentsData {

public struct EquatableError: Equatable, Sendable {
let description: String

init(_ error: Error) {
self.description = String(describing: error)
}

public static func == (lhs: EquatableError, rhs: EquatableError) -> Bool {
return lhs.description == rhs.description
}
}

}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,13 @@ struct APIKeyDashboardList: View {

var body: some View {
Button(action: action) {
Text(self.offering.serverDescription)
HStack {
Text(self.offering.serverDescription)
Spacer()
Image(systemName: "exclamationmark.circle.fill")
.foregroundStyle(Color.red)
.hidden(if: self.offering.paywallComponentsData?.errorInfo?.isEmpty ?? true)
}
}
.buttonStyle(.plain)
.contentShape(Rectangle())
Expand Down

0 comments on commit 9d4f465

Please sign in to comment.