mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-11 22:55:55 +05:45
176 lines
6.1 KiB
Swift
176 lines
6.1 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
|
|
//
|
|
// PaywallExtensions.swift
|
|
//
|
|
// Created by Nacho Soto on 6/6/23.
|
|
|
|
import StoreKit
|
|
import SwiftUI
|
|
|
|
#if swift(>=5.9)
|
|
|
|
// MARK: - StoreView
|
|
|
|
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
|
|
extension StoreView {
|
|
|
|
/// Creates a view to load a collection of products from the App Store, and merchandise them using an
|
|
/// icon and custom placeholder icon.
|
|
/// When the user purchases products through this paywall, the `RevenueCat` SDK will handle
|
|
/// the result automatically. All you need to do is to dismiss the paywall.
|
|
///
|
|
/// - Warning: In order to use StoreKit paywalls you must configure the `RevenueCat` SDK
|
|
/// in SK2 mode using ``Configuration/Builder/with(storeKitVersion:)``.
|
|
public static func forOffering(
|
|
_ offering: Offering,
|
|
prefersPromotionalIcon: Bool = false,
|
|
@ViewBuilder icon: @escaping (Product) -> Icon,
|
|
@ViewBuilder placeholderIcon: () -> PlaceholderIcon
|
|
) -> some View {
|
|
return self
|
|
.init(
|
|
ids: offering.allProductIdentifiers,
|
|
prefersPromotionalIcon: prefersPromotionalIcon,
|
|
icon: icon,
|
|
placeholderIcon: placeholderIcon
|
|
)
|
|
.handlePurchases(offering)
|
|
}
|
|
|
|
}
|
|
|
|
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
|
|
extension StoreView where Icon == EmptyView, PlaceholderIcon == EmptyView {
|
|
|
|
/// Creates a view to load a collection of products from the App Store, and merchandise them.
|
|
/// When the user purchases products through this paywall, the `RevenueCat` SDK will handle
|
|
/// the result automatically. All you need to do is to dismiss the paywall.
|
|
///
|
|
/// - Warning: In order to use StoreKit paywalls you must configure the `RevenueCat` SDK
|
|
/// in SK2 mode using ``Configuration/Builder/with(storeKitVersion:)``.
|
|
public static func forOffering(
|
|
_ offering: Offering,
|
|
prefersPromotionalIcon: Bool = false
|
|
) -> some View {
|
|
return self
|
|
.init(
|
|
ids: offering.allProductIdentifiers,
|
|
prefersPromotionalIcon: prefersPromotionalIcon
|
|
)
|
|
.handlePurchases(offering)
|
|
}
|
|
}
|
|
|
|
// MARK: - SubscriptionStoreView
|
|
|
|
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
|
|
extension SubscriptionStoreView {
|
|
|
|
/// Creates a ``SubscriptionStoreView`` from an ``Offering``
|
|
/// with custom marketing content.
|
|
/// When the user purchases products through this paywall, the `RevenueCat` SDK will handle
|
|
/// the result automatically. All you need to do is to dismiss the paywall.
|
|
///
|
|
/// - Warning: In order to use StoreKit paywalls you must configure the `RevenueCat` SDK
|
|
/// in SK2 mode using ``Configuration/Builder/with(storeKitVersion:)``.
|
|
public static func forOffering(
|
|
_ offering: Offering,
|
|
@ViewBuilder marketingContent: () -> (Content)
|
|
) -> some View {
|
|
return self
|
|
.init(
|
|
productIDs: offering.subscriptionProductIdentifiers,
|
|
marketingContent: marketingContent
|
|
)
|
|
.handlePurchases(offering)
|
|
}
|
|
|
|
}
|
|
|
|
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
|
|
extension SubscriptionStoreView where Content == AutomaticSubscriptionStoreMarketingContent {
|
|
|
|
/// Creates a ``SubscriptionStoreView`` from an ``Offering``
|
|
/// that doesn't take a custom view to use for marketing content.
|
|
///
|
|
/// - Warning: In order to use StoreKit paywalls you must configure the `RevenueCat` SDK
|
|
/// in SK2 mode using ``Configuration/Builder/with(storeKitVersion:)``.
|
|
public static func forOffering(_ offering: Offering) -> some View {
|
|
return self
|
|
.init(productIDs: offering.subscriptionProductIdentifiers)
|
|
.handlePurchases(offering)
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
|
|
private extension View {
|
|
|
|
func handlePurchases(_ offering: Offering) -> some View {
|
|
return self
|
|
.onInAppPurchaseStart { product in
|
|
guard Purchases.isConfigured else { return }
|
|
|
|
if !Purchases.shared.isStoreKit2EnabledAndAvailable {
|
|
Logger.appleWarning(Strings.configure.sk2_required_for_swiftui_paywalls)
|
|
}
|
|
|
|
// Find offering context from a matching package.
|
|
// Packages could have the same product but each product has the same offering
|
|
// context so that is okay if that happens.
|
|
let offeringContext = offering.availablePackages.first {
|
|
$0.storeProduct.productIdentifier == product.id
|
|
}?.presentedOfferingContext
|
|
|
|
Purchases.shared.cachePresentedOfferingContext(
|
|
offeringContext ?? .init(
|
|
offeringIdentifier: offering.identifier
|
|
),
|
|
productIdentifier: product.id
|
|
)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private extension Offering {
|
|
|
|
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
|
|
var subscriptionProductIdentifiers: [String] {
|
|
return self.products
|
|
.filter { $0.productCategory == .subscription }
|
|
// Filter out products with no subscription periods (non-subscriptions)
|
|
.compactMap { product in
|
|
product.subscriptionPeriod.map { period in
|
|
(
|
|
product: product,
|
|
period: period
|
|
)
|
|
}
|
|
}
|
|
// Sort by subscription period
|
|
.sorted(using: KeyPathComparator(\.period.unit.rawValue, order: .forward))
|
|
.map(\.product.productIdentifier)
|
|
}
|
|
|
|
var allProductIdentifiers: [String] {
|
|
return self.products.map(\.productIdentifier)
|
|
}
|
|
|
|
private var products: some Sequence<StoreProduct> {
|
|
return self.availablePackages.lazy.map(\.storeProduct)
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|