Skip to content

Commit

Permalink
Merge pull request #285 from superwall/v4-develop
Browse files Browse the repository at this point in the history
4.0.0-beta.4
  • Loading branch information
yusuftor authored Jan 23, 2025
2 parents 1db3260 + eb33eb6 commit 265a982
Show file tree
Hide file tree
Showing 9 changed files with 260 additions and 335 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

The changelog for `SuperwallKit`. Also see the [releases](https://github.com/superwall/Superwall-iOS/releases) on GitHub.

## 4.0.0-beta.4

### Fixes

- Fixes a crash that was caused by a concurrency issue.

## 4.0.0-beta.3

### Breaking Changes
Expand Down
2 changes: 1 addition & 1 deletion Examples/Advanced/Advanced/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ struct HomeView: View {
ZStack {
switch page {
case .nonGated:
Text("Non gated feature launched")
Text("Non-gated feature launched")
case .pro:
Text("Pro feature launched")
case .diamond:
Expand Down
5 changes: 0 additions & 5 deletions Sources/SuperwallKit/Graveyard/SuperwallGraveyard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,6 @@ extension Superwall {
}

@available(*, unavailable, renamed: "SuperwallPlacementInfo")
@objc(SWKSuperwallEventInfo)
@objcMembers
public final class SuperwallEventInfo: NSObject {}

extension SuperwallDelegate {
Expand All @@ -152,12 +150,9 @@ extension SuperwallDelegate {
}

@available(*, unavailable, renamed: "ProductStore")
@objc(SWKStore)
public enum Store: Int {
case appStore
}

@available(*, unavailable, renamed: "Product")
@objc(SWKProductInfo)
@objcMembers
public final class ProductInfo: NSObject, Codable, Sendable {}
2 changes: 1 addition & 1 deletion Sources/SuperwallKit/Misc/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ let sdkVersion = """
*/

let sdkVersion = """
4.0.0-beta.3
4.0.0-beta.4
"""
293 changes: 245 additions & 48 deletions Sources/SuperwallKit/Models/Product/Product.swift
Original file line number Diff line number Diff line change
@@ -1,76 +1,273 @@
//
// ProductType.swift
// Superwall
// File.swift
//
// Created by Yusuf Tör on 01/03/2022.
//
// Created by Yusuf Tör on 29/03/2024.
//

import Foundation

/// The type of product.
@objc(SWKProductType)
public enum ProductType: Int, Codable, Sendable {
/// The primary product of the paywall.
case primary

/// The secondary product of the paywall.
case secondary

/// The tertiary product of the paywall.
case tertiary
/// An enum whose types specify the store which the product belongs to.
@objc(SWKProductStore)
public enum ProductStore: Int, Codable, Sendable {
/// An Apple App Store product.
case appStore

enum InternalProductType: String, Codable {
case primary
case secondary
case tertiary
enum CodingKeys: String, CodingKey {
case appStore = "APP_STORE"
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()

switch self {
case .primary:
try container.encode(InternalProductType.primary.rawValue)
case .secondary:
try container.encode(InternalProductType.secondary.rawValue)
case .tertiary:
try container.encode(InternalProductType.tertiary.rawValue)
case .appStore:
try container.encode(CodingKeys.appStore.rawValue)
}
}

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let rawValue = try container.decode(String.self)
guard let internalProductType = InternalProductType(rawValue: rawValue) else {
let type = CodingKeys(rawValue: rawValue)
switch type {
case .appStore:
self = .appStore
case .none:
throw DecodingError.valueNotFound(
String.self,
.init(
codingPath: [],
debugDescription: "Unsupported product store type."
)
)
}
}
}

/// An Apple App Store product.
@objc(SWKAppStoreProduct)
@objcMembers
public final class AppStoreProduct: NSObject, Codable, Sendable {
/// The bundleId that the product is associated with
let bundleId: String?

/// The store the product belongs to.
let store: ProductStore

/// The product identifier.
public let id: String

enum CodingKeys: String, CodingKey {
case bundleId
case id = "productIdentifier"
case store
}

init(
store: ProductStore = .appStore,
id: String
) {
self.bundleId = Bundle.main.bundleIdentifier
self.store = store
self.id = id
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(store, forKey: .store)
try container.encodeIfPresent(bundleId, forKey: .bundleId)
}

public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.store = try container.decode(ProductStore.self, forKey: .store)

// If the bundle ID is present, and it's not equal to the bundle
// ID of the app, it gets ignored.
let bundleId = try container.decodeIfPresent(String.self, forKey: .bundleId)
if let bundleId = bundleId,
bundleId != Bundle.main.bundleIdentifier {
throw DecodingError.typeMismatch(
InternalProductType.self,
.init(
codingPath: [],
debugDescription: "Didn't find a primary, secondary, or tertiary product type."
)
String.self,
.init(
codingPath: [],
debugDescription: "The bundle id of the product didn't match the bundle id of the app."
)
)
}
switch internalProductType {
case .primary:
self = .primary
case .secondary:
self = .secondary
case .tertiary:
self = .tertiary
self.bundleId = bundleId
super.init()
}

public override func isEqual(_ object: Any?) -> Bool {
guard let other = object as? AppStoreProduct else {
return false
}
return bundleId == other.bundleId
&& store == other.store
&& id == other.id
}

public override var hash: Int {
var hasher = Hasher()
hasher.combine(bundleId)
hasher.combine(store)
hasher.combine(id)
return hasher.finalize()
}
}

// MARK: - CustomStringConvertible
extension ProductType: CustomStringConvertible {
public var description: String {
switch self {
case .primary:
return InternalProductType.primary.rawValue
case .secondary:
return InternalProductType.secondary.rawValue
case .tertiary:
return InternalProductType.tertiary.rawValue
/// An objc-only type that specifies a store and a product.
@objc(SWKStoreProductAdapter)
@objcMembers
public final class StoreProductAdapterObjc: NSObject, Codable, Sendable {
/// The store associated with the product.
public let store: ProductStore

/// The App Store product. This is non-nil if `store` is
/// `appStore`.
public let appStoreProduct: AppStoreProduct?

init(
store: ProductStore,
appStoreProduct: AppStoreProduct?
) {
self.store = store
self.appStoreProduct = appStoreProduct
}
}

/// The product in the paywall.
@objc(SWKProduct)
@objcMembers
public final class Product: NSObject, Codable, Sendable {
/// The type of store and its associated product.
public enum StoreProductType: Codable, Sendable, Hashable {
case appStore(AppStoreProduct)
}

private enum CodingKeys: String, CodingKey {
case name = "referenceName"
case storeProduct
case entitlements
}

/// The name of the product in the editor.
///
/// This is optional because products can also be decoded from outside
/// of a paywall.
public let name: String?

/// The type of product
public let type: StoreProductType

/// Convenience variable that accesses the product's identifier.
public var id: String {
switch type {
case .appStore(let product):
return product.id
}
}

/// The entitlement associated with the product.
public let entitlements: Set<Entitlement>

/// The objc-only type of product.
@objc(adapter)
public let objcAdapter: StoreProductAdapterObjc

init(
name: String?,
type: StoreProductType,
entitlements: Set<Entitlement>
) {
self.name = name
self.type = type
self.entitlements = entitlements

switch type {
case .appStore(let product):
objcAdapter = .init(
store: .appStore,
appStoreProduct: product
)
}
}

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

try container.encodeIfPresent(name, forKey: .name)

try container.encode(entitlements, forKey: .entitlements)

switch type {
case .appStore(let product):
try container.encode(product, forKey: .storeProduct)
}
}

public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decodeIfPresent(String.self, forKey: .name)

// These will throw an error if the StoreProduct is not an AppStoreProduct or if the
// entitlement type is not `SERVICE_LEVEL`, which must be caught in a `Throwable` and
// ignored in the paywall object.
entitlements = try container.decode(Set<Entitlement>.self, forKey: .entitlements)
let storeProduct = try container.decode(AppStoreProduct.self, forKey: .storeProduct)
type = .appStore(storeProduct)
objcAdapter = .init(store: .appStore, appStoreProduct: storeProduct)
}

public override func isEqual(_ object: Any?) -> Bool {
guard let other = object as? Product else {
return false
}
return name == other.name
&& type == other.type
&& entitlements == other.entitlements
}

public override var hash: Int {
var hasher = Hasher()
hasher.combine(name)
hasher.combine(type)
hasher.combine(entitlements)
return hasher.finalize()
}
}

struct TemplatingProductItem: Encodable {
let name: String
let productId: String

private enum CodingKeys: String, CodingKey {
case product
case productId
}

static func create(from productItems: [Product]) -> [TemplatingProductItem] {
return productItems.compactMap {
guard let name = $0.name else {
return nil
}
return TemplatingProductItem(
name: name,
productId: $0.id
)
}
}

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

// Encode name as "product" for templating
try container.encode(name, forKey: .product)

// Encode product ID as "productId" for templating
try container.encode(productId, forKey: .productId)
}
}
Loading

0 comments on commit 265a982

Please sign in to comment.