Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
b372744
feat: add win back offer
kaiquegazola Jan 21, 2025
bedc5f8
feat: add app account token parameter
kaiquegazola Jan 21, 2025
63d2270
feat: add promotional offer param
kaiquegazola Jan 21, 2025
1fa0441
test: add win back offer, promotional and app account token tests
kaiquegazola Jan 22, 2025
1b298c6
chore: add promotional offers in example app
kaiquegazola Jan 22, 2025
7faddc0
test: add Sk2PurchaseParam tests
kaiquegazola Jan 22, 2025
b67d029
chore: bump version
kaiquegazola Jan 22, 2025
caf6cc9
Merge branch 'main' into feature/in-app-purchase-storekit2-win-back-o…
kaiquegazola Jan 22, 2025
ea32bd1
fix: format files
kaiquegazola Jan 22, 2025
45f6fb2
Merge remote-tracking branch 'origin/feature/in-app-purchase-storekit…
kaiquegazola Jan 22, 2025
3822279
Merge remote-tracking branch 'origin/main' into feature/in-app-purcha…
kaiquegazola Jan 30, 2025
acaa4fb
feat: add checkWinBackOfferEligibility
kaiquegazola Jan 31, 2025
39f94da
fix: add compiler check to avoid older xcode issues
kaiquegazola Jan 31, 2025
c696b03
fix: fix SK2SubscriptionOfferSignatureMessage usage
kaiquegazola Jan 31, 2025
8c9bd1e
chore: bump version
kaiquegazola Jan 31, 2025
01cf3c0
fix: fix unsupported compiler test
kaiquegazola Jan 31, 2025
36314b8
Merge branch 'main' into feature/in-app-purchase-storekit2-win-back-o…
kaiquegazola Jan 31, 2025
73257b2
fix: fix promotional offer list test in unsupported compilers
kaiquegazola Jan 31, 2025
5362391
refactor: remove comment
kaiquegazola Jan 31, 2025
a64d3bc
Merge branch 'main' into feature/in-app-purchase-storekit2-win-back-o…
kaiquegazola Feb 26, 2025
e327191
refactor: remove #if compiler(>=6.0) directive in IAP SK2
kaiquegazola May 4, 2025
ef114df
Merge remote-tracking branch 'origin/feature/in-app-purchase-storekit…
kaiquegazola May 4, 2025
26784e4
Merge remote-tracking branch 'origin/main' into feature/in-app-purcha…
kaiquegazola May 4, 2025
179a4df
chore: regenerate pigeon files
kaiquegazola May 4, 2025
a10dbb9
fix: fix IAP SK example win back offer
kaiquegazola May 4, 2025
d8c090a
chore: dump version
kaiquegazola May 4, 2025
5d213ab
refactor: remove unused unverified case
kaiquegazola May 4, 2025
853cc99
Merge remote-tracking branch 'origin/main' into feature/in-app-purcha…
kaiquegazola May 14, 2025
26596f2
fix: fix merge conflicts
kaiquegazola May 14, 2025
6377464
fix: dart doc period sentence
kaiquegazola May 23, 2025
8d68c8c
refactor: change checkWinBackOfferEligibility to isWinBackOfferEligible
kaiquegazola May 24, 2025
f47b7d2
chore: update CHANGELOG to reflect new isWinBackOfferEligible function
kaiquegazola May 24, 2025
6abc429
refactor: change checkWinBackOfferEligibility to isWinBackOfferEligib…
kaiquegazola May 24, 2025
cd59de0
refactor: simplify eligibility check with contains and verified check
kaiquegazola May 24, 2025
baec1a4
refactor: move SK2SubscriptionOfferSignature to sk2_promotional_offer…
kaiquegazola May 24, 2025
87a424c
refactor: mark SK2PromotionalOffer as final
kaiquegazola May 24, 2025
09b651a
refactor: add Sk2PurchaseParam.fromOffer factory for cleaner offer ha…
kaiquegazola May 24, 2025
0792274
docs: improve isWinBackOfferEligible error handling documentation
kaiquegazola May 24, 2025
7f9d9c2
refactor: make _convertPromotionalOffer static
kaiquegazola May 24, 2025
8e96dbd
docs: improve buyNonConsumable documentation with clearer usage examples
kaiquegazola May 24, 2025
2b4099e
Merge remote-tracking branch 'origin/main' into feature/in-app-purcha…
kaiquegazola May 24, 2025
a6697e1
docs: fix missing periods
kaiquegazola May 24, 2025
0f7530d
refactor: make options final exhaustive
kaiquegazola May 31, 2025
af4dfff
Merge branch 'main' into feature/in-app-purchase-storekit2-win-back-o…
kaiquegazola May 31, 2025
69025cb
Merge branch 'main' into feature/in-app-purchase-storekit2-win-back-o…
LouiseHsu Jun 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## 0.4.1

* Adds **Win Back Offers** support for StoreKit2:
- Includes new `checkWinBackOfferEligibility` method for eligibility verification
* Adds **Promotional Offers** support in StoreKit2 purchases
* Fixes introductory pricing handling in promotional offers list in StoreKit2
* Ensures proper `appAccountToken` handling for StoreKit2 purchases

## 0.4.0

* **BREAKING CHANGE:** StoreKit 2 is now the default for all devices that support it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,36 @@ extension InAppPurchasePlugin: InAppPurchase2API {
return completion(.failure(error))
}

let result = try await product.purchase(options: [])
var purchaseOptions: Set<Product.PurchaseOption> = []

if let appAccountToken = options?.appAccountToken,
let accountTokenUUID = UUID(uuidString: appAccountToken)
{
purchaseOptions.insert(.appAccountToken(accountTokenUUID))
}

if #available(iOS 17.4, macOS 14.4, *) {
if let promotionalOffer = options?.promotionalOffer {
purchaseOptions.insert(
.promotionalOffer(
offerID: promotionalOffer.promotionalOfferId,
signature: promotionalOffer.promotionalOfferSignature.convertToSignature
)
)
}
}

if #available(iOS 18.0, macOS 15.0, *) {
if let winBackOfferId = options?.winBackOfferId,
let winBackOffer = product.subscription?.winBackOffers.first(where: {
$0.id == winBackOfferId
})
{
purchaseOptions.insert(.winBackOffer(winBackOffer))
}
}

let result = try await product.purchase(options: purchaseOptions)

switch result {
case .success(let verification):
Expand Down Expand Up @@ -88,6 +117,78 @@ extension InAppPurchasePlugin: InAppPurchase2API {
}
}

/// Checks if the user is eligible for a specific win back offer.
///
/// - Parameters:
/// - productId: The product ID associated with the offer.
/// - offerId: The ID of the win back offer.
/// - completion: Returns `Bool` for eligibility or `Error` on failure.
///
/// - Availability: iOS 18.0+, macOS 15.0+, Swift 6.0+ (Xcode 16+).
func checkWinBackOfferEligibility(
productId: String,
offerId: String,
completion: @escaping (Result<Bool, Error>) -> Void
) {
if #available(iOS 18.0, macOS 15.0, *) {
Task {
do {
guard let product = try await Product.products(for: [productId]).first else {
completion(
.failure(
PigeonError(
code: "storekit2_failed_to_fetch_product",
message: "Storekit has failed to fetch this product.",
details: "Product ID: \(productId)")))
return
}

guard let subscription = product.subscription else {
completion(
.failure(
PigeonError(
code: "storekit2_not_subscription",
message: "Product is not a subscription",
details: "Product ID: \(productId)")))
return
}

let statuses = try await subscription.status

var isEligible = false
for status in statuses {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: maybe:

let isEligible = try await subscription.status
  .contains { $0.eligibleWinBackOfferIDs.contains(offerID) }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I updated it to include a .verified check to avoid using unverified data:

let isEligible = try await subscription.status.contains { status in
    if case .verified(let renewalInfo) = status.renewalInfo {
        return renewalInfo.eligibleWinBackOfferIDs.contains(offerId)
    }
    return false
}

switch status.renewalInfo {
case .verified(let renewalInfo):
if renewalInfo.eligibleWinBackOfferIDs.contains(offerId) {
isEligible = true
break
}
default:
continue
}
}

completion(.success(isEligible))

} catch {
completion(
.failure(
PigeonError(
code: "storekit2_eligibility_check_failed",
message: "Failed to check offer eligibility: \(error.localizedDescription)",
details: "Product ID: \(productId), Error: \(error)")))
}
}
} else {
completion(
.failure(
PigeonError(
code: "storekit2_unsupported_platform_version",
message: "Win back offers require iOS 18+ or macOS 15.0+",
details: nil)))
}
}

/// Wrapper method around StoreKit2's transactions() method
/// https://developer.apple.com/documentation/storekit/product/3851116-products
func transactions(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,23 @@ extension Product.ProductType {
@available(iOS 15.0, macOS 12.0, *)
extension Product.SubscriptionInfo {
var convertToPigeon: SK2SubscriptionInfoMessage {
var allOffers: [SK2SubscriptionOfferMessage] = []

if #available(iOS 18.0, macOS 15.0, *) {
allOffers.append(contentsOf: winBackOffers.map { $0.convertToPigeon })
}

allOffers.append(contentsOf: promotionalOffers.map { $0.convertToPigeon })

if let introductory = introductoryOffer {
allOffers.append(introductory.convertToPigeon)
}

return SK2SubscriptionInfoMessage(
promotionalOffers: promotionalOffers.map({ $0.convertToPigeon }),
promotionalOffers: allOffers,
subscriptionGroupID: subscriptionGroupID,
subscriptionPeriod: subscriptionPeriod.convertToPigeon)
subscriptionPeriod: subscriptionPeriod.convertToPigeon
)
}
}

Expand Down Expand Up @@ -90,6 +103,33 @@ extension SK2SubscriptionOfferMessage: Equatable {
}
}

extension SK2SubscriptionOfferSignatureMessage {
@available(iOS 17.4, macOS 14.4, *)
var convertToSignature: Product.SubscriptionOffer.Signature {
return Product.SubscriptionOffer.Signature(
keyID: keyID,
nonce: nonceAsUUID,
timestamp: Int(timestamp),
signature: signatureAsData
)
}

var nonceAsUUID: UUID {
guard let uuid = UUID(uuidString: nonce) else {
fatalError("Invalid UUID format for nonce: \(nonce)")
Copy link
Contributor

Choose a reason for hiding this comment

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

So if the developer passes in a nonce that is not a valid UUID it would crash the app? That seems to be a bit heavy-handed. Is there an error reporting mechanism for programmer errors like this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, it felt a bit heavy-handed, but I followed the existing pattern in this file. Happy to change it if there’s a preferred way — any suggestion on the standard we should follow here?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah I didn't know it was an existing pattern. In that case if there's a better way to handle these errors more gracefully we can refactor those later.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you show me the way, I can do a PR later.

}
return uuid
}

var signatureAsData: Data {
guard let data = Data(base64Encoded: signature) else {
fatalError("Invalid Base64 format for signature: \(signature)")
}
return data
}

}

@available(iOS 15.0, macOS 12.0, *)
extension Product.SubscriptionOffer.OfferType {
var convertToPigeon: SK2SubscriptionOfferTypeMessage {
Expand All @@ -99,7 +139,12 @@ extension Product.SubscriptionOffer.OfferType {
case .promotional:
return SK2SubscriptionOfferTypeMessage.promotional
default:
fatalError("An unknown OfferType was passed in")
if #available(iOS 18.0, macOS 15.0, *) {
if self == .winBack {
return SK2SubscriptionOfferTypeMessage.winBack
}
}
fatalError("An unknown or unsupported OfferType was passed in")
}
}
}
Expand Down
Loading