Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support fetching eligible win-back offers for a product #4431

Merged
merged 20 commits into from
Nov 6, 2024

Conversation

fire-at-will
Copy link
Contributor

@fire-at-will fire-at-will commented Oct 31, 2024

Description

This PR adds the ability for developers to fetch the win-back offers that a subscriber is eligible for on a given product.

New APIs

All API additions in this PR are marked as internal for now. They will be made public once we introduce the ability to redeem a win-back offer in a future PR.

The PR introduces a new WinBackOffer type to represent a win-back offer:

@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
@objc(RCWinBackOffer)
internal final class WinBackOffer: NSObject, Sendable {

    /// The ``StoreProductDiscount`` in this offer.
    @objc internal let discount: StoreProductDiscount

    init(discount: StoreProductDiscount) {
        self.discount = discount
    }

}

Additionally, the PR introduces two new functions on the Purchases singleton for fetching the win-back offers for a given product that a subscriber is eligible for. These functions are not available when the ENABLE_CUSTOM_ENTITLEMENT_COMPUTATION compiler flag is enabled.

@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
 func eligibleWinBackOffers(
     forProduct product: StoreProduct
 ) async throws -> [WinBackOffer]

@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
 func eligibleWinBackOffers(
     forProduct product: StoreProduct,
     completion: @escaping @Sendable (Result<[WinBackOffer], PublicError>) -> Void
 )

Note

Since we're keeping this functionality as internal for now, this PR is marked with the pr:other tag so that it doesn't get marked as a new feature in the auto-generated changelogs.

@fire-at-will fire-at-will added pr:other feat:Win-Back Offers Supporting iOS 18's Win-Back Offers labels Oct 31, 2024
@fire-at-will fire-at-will self-assigned this Oct 31, 2024
*/
internal func eligibleWinBackOffers(
forProduct product: StoreProduct,
completion: @escaping @Sendable (Result<[WinBackOffer], PublicError>) -> Void
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

completion is marked as @Sendable to avoid warnings below when we dispatch the completion handler on the main actor

@@ -44,7 +44,13 @@ enum AvailabilityChecks {
}

static func iOS17APIAvailableOrSkipTest() throws {
guard #available(iOS 17.0, tvOS 17.0, macOS 14.0, watchOS 10.0, *) else {
guard #available(iOS 17.0, tvOS 17.0, macOS 14.0, watchOS 10.0, visionOS 1.0, *) else {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added visionOS check

@fire-at-will fire-at-will marked this pull request as ready for review October 31, 2024 20:31
@fire-at-will fire-at-will requested review from MarkVillacampa and a team October 31, 2024 20:31
@joshdholtz joshdholtz changed the title CAT-1710: Support fetching eligible win-back offers for a product Support fetching eligible win-back offers for a product Nov 1, 2024
Copy link
Member

@MarkVillacampa MarkVillacampa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really really good!! Great work!

Sources/Error Handling/ErrorCode.swift Show resolved Hide resolved
Sources/Purchasing/Purchases/Purchases.swift Outdated Show resolved Hide resolved
Sources/Purchasing/Purchases/Purchases.swift Outdated Show resolved Hide resolved
Sources/Purchasing/Purchases/PurchasesType.swift Outdated Show resolved Hide resolved
return false
case .verified(let transaction):
// Intentionally exclude transactions acquired through family sharing
return transaction.ownershipType == .purchased
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, you can only be subscribed to a product once, so there must be at most 1 renewalInfo instance that is NOT a family shared one. So maybe we could simplify the logic of the rest of the function by just finding the first status transaction with transaction.ownershipType == .purchased, like Apple does in their sample code:

let purchasedStatus = statuses.first {
    $0.transaction.unsafePayloadValue.ownershipType == .purchased
}

https://developer.apple.com/videos/play/wwdc2024/10110/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is where I thought I might be able to do a bit better than Apple's example in the event where there were multiple renewalInfos.

Is there any chance that there would be more than one renewalInfo for a single product ID if a user subscribes, churns, and then re-subscribed? I'm not seeing many details about this in the docs.

I've implemented this change here and will merge it in to this PR if we think we're safe in the case I mentioned above. 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation does not explicitly state that, but it says:

The array can have more than one subscription status if your subscription supports Family Sharing.

Which I'd argue highly suggest the array can normally only have one status (the purchased one) and potentially more only if it supports family sharing.

Given that, plus Apple's sample code makes me very confident there will always be just one status with ownershipType == .purchased

https://developer.apple.com/documentation/storekit/product/subscriptioninfo/3822296-status

return eligibleWinBackOfferIDsPerRenewalInfo
.flatMap { $0 }
.filter { seen.insert($0).inserted }
}()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By using just one status above, we can get rid of all the deduplication logic here.

fire-at-will and others added 5 commits November 1, 2024 10:53
Co-authored-by: Mark Villacampa <MarkVillacampa@users.noreply.github.com>
Co-authored-by: Mark Villacampa <MarkVillacampa@users.noreply.github.com>
Co-authored-by: Mark Villacampa <MarkVillacampa@users.noreply.github.com>
@fire-at-will fire-at-will merged commit 6de169a into main Nov 6, 2024
7 checks passed
@fire-at-will fire-at-will deleted the cat-1710-support-fetching-eligible-winbacks branch November 6, 2024 21:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat:Win-Back Offers Supporting iOS 18's Win-Back Offers pr:other
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants