This repository has been archived by the owner on Feb 24, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Privacy Pro Free Trials - Models and API (#1120)
Task/Issue URL: https://app.asana.com/0/1208114992212396/1208796999534221/f iOS PR: duckduckgo/iOS#3691 macOS PR: duckduckgo/macos-browser#3641 What kind of version bump will this require?: Minor **Optional**: Tech Design URL: https://app.asana.com/0/481882893211075/1208773437150501/f **Description**: This PR includes the following: * Adds a new model type for Subscription Offers * Adds a new API to `StorePurchaseManager` to retrieve Free Trial Subscriptions * Updates existing `subscriptionOptions` API to NOT return Free Trial Subscriptions * Adds abstractions on StoreKit types to enable testing * Adds tests (**Note: Make sure to expand the `StorePurchaseManagerTests` file when reviewing**)
- Loading branch information
1 parent
b7f7ba9
commit e8654e1
Showing
10 changed files
with
885 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
45 changes: 45 additions & 0 deletions
45
Sources/Subscription/Managers/StorePurchaseManager/ProductFetching.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
// | ||
// ProductFetching.swift | ||
// | ||
// Copyright © 2024 DuckDuckGo. All rights reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
|
||
import Foundation | ||
import StoreKit | ||
|
||
/// A protocol for types that can fetch subscription products. | ||
@available(macOS 12.0, iOS 15.0, *) | ||
public protocol ProductFetching { | ||
/// Fetches products for the specified identifiers. | ||
/// - Parameter identifiers: An array of product identifiers to fetch. | ||
/// - Returns: An array of subscription products. | ||
/// - Throws: An error if the fetch operation fails. | ||
func products(for identifiers: [String]) async throws -> [any SubscriptionProduct] | ||
} | ||
|
||
/// A default implementation of ProductFetching that uses StoreKit's standard product fetching. | ||
@available(macOS 12.0, iOS 15.0, *) | ||
public final class DefaultProductFetcher: ProductFetching { | ||
/// Initializes a new DefaultProductFetcher instance. | ||
public init() {} | ||
|
||
/// Fetches products using StoreKit's Product.products API. | ||
/// - Parameter identifiers: An array of product identifiers to fetch. | ||
/// - Returns: An array of subscription products. | ||
/// - Throws: An error if the fetch operation fails. | ||
public func products(for identifiers: [String]) async throws -> [any SubscriptionProduct] { | ||
return try await Product.products(for: identifiers) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
Sources/Subscription/Managers/StorePurchaseManager/SubscriptionProduct.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
// | ||
// SubscriptionProduct.swift | ||
// | ||
// Copyright © 2024 DuckDuckGo. All rights reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
|
||
import Foundation | ||
import StoreKit | ||
|
||
/// A protocol that defines the core functionality and properties of a subscription product. | ||
/// Conforming types must provide information about pricing, description, and subscription terms. | ||
@available(macOS 12.0, iOS 15.0, *) | ||
public protocol SubscriptionProduct: Equatable { | ||
/// The unique identifier of the product. | ||
var id: String { get } | ||
|
||
/// The user-facing name of the product. | ||
var displayName: String { get } | ||
|
||
/// The formatted price that should be displayed to users. | ||
var displayPrice: String { get } | ||
|
||
/// A detailed description of the product. | ||
var description: String { get } | ||
|
||
/// Indicates whether this is a monthly subscription. | ||
var isMonthly: Bool { get } | ||
|
||
/// Indicates whether this is a yearly subscription. | ||
var isYearly: Bool { get } | ||
|
||
/// The introductory offer associated with this subscription, if any. | ||
var introductoryOffer: SubscriptionProductIntroductoryOffer? { get } | ||
|
||
/// Indicates whether this subscription has a Free Trial offer available. | ||
var hasFreeTrialOffer: Bool { get } | ||
|
||
/// Asynchronously determines whether the user is eligible for an introductory offer. | ||
var isEligibleForIntroOffer: Bool { get async } | ||
|
||
/// Initiates a purchase of the subscription with the specified options. | ||
/// - Parameter options: A set of options to configure the purchase. | ||
/// - Returns: The result of the purchase attempt. | ||
/// - Throws: An error if the purchase fails. | ||
func purchase(options: Set<Product.PurchaseOption>) async throws -> Product.PurchaseResult | ||
} | ||
|
||
/// Extends StoreKit's Product to conform to SubscriptionProduct. | ||
@available(macOS 12.0, iOS 15.0, *) | ||
extension Product: SubscriptionProduct { | ||
/// Determines if this is a monthly subscription by checking if the subscription period | ||
/// is exactly one month. | ||
public var isMonthly: Bool { | ||
guard let subscription else { return false } | ||
return subscription.subscriptionPeriod.unit == .month && | ||
subscription.subscriptionPeriod.value == 1 | ||
} | ||
|
||
/// Determines if this is a yearly subscription by checking if the subscription period | ||
/// is exactly one year. | ||
public var isYearly: Bool { | ||
guard let subscription else { return false } | ||
return subscription.subscriptionPeriod.unit == .year && | ||
subscription.subscriptionPeriod.value == 1 | ||
} | ||
|
||
/// Returns the introductory offer for this subscription if available. | ||
public var introductoryOffer: (any SubscriptionProductIntroductoryOffer)? { | ||
subscription?.introductoryOffer | ||
} | ||
|
||
/// Indicates whether this subscription has a Free Trial offer. | ||
public var hasFreeTrialOffer: Bool { | ||
return subscription?.introductoryOffer?.isFreeTrial ?? false | ||
} | ||
|
||
/// Asynchronously checks if the user is eligible for an introductory offer. | ||
public var isEligibleForIntroOffer: Bool { | ||
get async { | ||
guard let subscription else { return false } | ||
return await subscription.isEligibleForIntroOffer | ||
} | ||
} | ||
|
||
/// Implements Equatable by comparing product IDs. | ||
public static func == (lhs: Product, rhs: Product) -> Bool { | ||
return lhs.id == rhs.id | ||
} | ||
} |
Oops, something went wrong.