Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 10 additions & 13 deletions packages/apple/Sources/Models/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1337,17 +1337,14 @@ public struct SubscriptionProductReplacementParamsAndroid: Codable {

/// Apple App Store verification parameters.
/// Used for server-side receipt validation via App Store Server API.
///
/// ⚠️ SECURITY: Contains sensitive token (jws). Do not log or persist this data.
public struct VerifyPurchaseAppleOptions: Codable {
/// The JWS (JSON Web Signature) representation of the transaction.
/// ⚠️ Sensitive: Do not log this value.
public var jws: String
/// Product SKU to validate
public var sku: String

public init(
jws: String
sku: String
) {
self.jws = jws
self.sku = sku
}
}

Expand All @@ -1366,17 +1363,21 @@ public struct VerifyPurchaseGoogleOptions: Codable {
/// Purchase token from the purchase response.
/// ⚠️ Sensitive: Do not log this value.
public var purchaseToken: String
/// Product SKU to validate
public var sku: String

public init(
accessToken: String,
isSub: Bool? = nil,
packageName: String,
purchaseToken: String
purchaseToken: String,
sku: String
) {
self.accessToken = accessToken
self.isSub = isSub
self.packageName = packageName
self.purchaseToken = purchaseToken
self.sku = sku
}
}

Expand Down Expand Up @@ -1417,19 +1418,15 @@ public struct VerifyPurchaseProps: Codable {
public var google: VerifyPurchaseGoogleOptions?
/// Meta Horizon (Quest) verification parameters.
public var horizon: VerifyPurchaseHorizonOptions?
/// Product SKU to validate
public var sku: String

public init(
apple: VerifyPurchaseAppleOptions? = nil,
google: VerifyPurchaseGoogleOptions? = nil,
horizon: VerifyPurchaseHorizonOptions? = nil,
sku: String
horizon: VerifyPurchaseHorizonOptions? = nil
) {
self.apple = apple
self.google = google
self.horizon = horizon
self.sku = sku
}
}

Expand Down
35 changes: 17 additions & 18 deletions packages/apple/Sources/OpenIapModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -598,25 +598,24 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol {
var jws: String = ""
var isValid = false

// If apple options with JWS are provided, use that directly
// Otherwise, fetch the latest transaction from StoreKit
if let appleOptions = props.apple, !appleOptions.jws.isEmpty {
jws = appleOptions.jws
// When JWS is provided externally, we trust it's valid
// The caller should verify the JWS on their server
isValid = true
} else {
do {
let product = try await storeProduct(for: props.sku)
if let result = await product.latestTransaction {
jws = result.jwsRepresentation
let transaction = try checkVerified(result)
latestPurchase = .purchaseIos(await StoreKitTypesBridge.purchaseIOS(from: transaction, jwsRepresentation: result.jwsRepresentation))
isValid = true
}
} catch {
isValid = false
// Apple options with sku is required
guard let appleOptions = props.apple, !appleOptions.sku.isEmpty else {
throw makePurchaseError(
code: .developerError,
message: "Apple verification requires apple options with sku"
)
}

do {
let product = try await storeProduct(for: appleOptions.sku)
if let result = await product.latestTransaction {
jws = result.jwsRepresentation
let transaction = try checkVerified(result)
latestPurchase = .purchaseIos(await StoreKitTypesBridge.purchaseIOS(from: transaction, jwsRepresentation: result.jwsRepresentation))
isValid = true
}
} catch {
isValid = false
}

return VerifyPurchaseResultIOS(
Expand Down
2 changes: 1 addition & 1 deletion packages/apple/Sources/OpenIapProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public extension OpenIapModuleProtocol {
throw PurchaseError(
code: .featureNotSupported,
message: "Expected iOS validation result",
productId: props.sku
productId: props.apple?.sku
)
}

Expand Down
2 changes: 1 addition & 1 deletion packages/apple/Sources/OpenIapStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ public final class OpenIapStore: ObservableObject {
}

public func verifyPurchase(sku: String) async throws -> VerifyPurchaseResultIOS {
let result = try await module.verifyPurchase(VerifyPurchaseProps(sku: sku))
let result = try await module.verifyPurchase(VerifyPurchaseProps(apple: VerifyPurchaseAppleOptions(sku: sku)))
if case let .verifyPurchaseResultIos(iosResult) = result {
return iosResult
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ private final class FakeOpenIapModule: OpenIapModuleProtocol {
func getReceiptDataIOS() async throws -> String? { "receipt" }
func validateReceiptIOS(_ props: VerifyPurchaseProps) async throws -> VerifyPurchaseResultIOS {
guard case let .verifyPurchaseResultIos(ios) = validateResult else {
throw PurchaseError(code: .featureNotSupported, message: "Android validation not supported", productId: props.sku)
throw PurchaseError(code: .featureNotSupported, message: "Android validation not supported", productId: props.apple?.sku)
}
return ios
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ private final class FakeVerifyPurchaseModule: OpenIapModuleProtocol {
func getReceiptDataIOS() async throws -> String? { "receipt" }
func validateReceiptIOS(_ props: VerifyPurchaseProps) async throws -> VerifyPurchaseResultIOS {
guard case let .verifyPurchaseResultIos(ios) = validateResult else {
throw PurchaseError(code: .featureNotSupported, message: "Expected iOS validation result", productId: props.sku)
throw PurchaseError(code: .featureNotSupported, message: "Expected iOS validation result", productId: props.apple?.sku)
}
return ios
}
Expand Down
2 changes: 1 addition & 1 deletion packages/docs/src/pages/docs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ function Docs() {
className={({ isActive }) => (isActive ? 'active' : '')}
onClick={closeSidebar}
>
Notes
Updates
</NavLink>
</li>
<li>
Expand Down
69 changes: 56 additions & 13 deletions packages/docs/src/pages/docs/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2426,8 +2426,8 @@ Future<void> handleExternalPurchase(String externalUrl) async {
verification.
</p>

<AnchorLink id="purchase-verification-props" level="h3">
PurchaseVerificationProps
<AnchorLink id="verify-purchase-props" level="h3">
VerifyPurchaseProps
</AnchorLink>
<table className="doc-table">
<thead>
Expand All @@ -2439,35 +2439,49 @@ Future<void> handleExternalPurchase(String externalUrl) async {
<tbody>
<tr>
<td>
<code>apple</code>
</td>
<td>
Apple App Store verification options. Contains:{' '}
<code>sku</code>
</td>
<td>Product identifier to verify</td>
</tr>
<tr>
<td>
<code>androidOptions</code>
<code>google</code>
</td>
<td>
Google Play verification options. Contains:{' '}
<code>sku</code>, <code>packageName</code>,{' '}
<code>purchaseToken</code>, <code>accessToken</code>,{' '}
<code>isSub</code>
</td>
</tr>
<tr>
<td>
<code>horizon</code>
</td>
<td>
Android Play Developer API options. Contains:{' '}
<code>packageName</code>, <code>productToken</code>,{' '}
<code>accessToken</code>, <code>isSub</code>
Meta Horizon (Quest) verification options. Contains:{' '}
<code>sku</code>, <code>userId</code>, <code>accessToken</code>
</td>
</tr>
</tbody>
</table>

<AnchorLink id="purchase-verification-result" level="h3">
PurchaseVerificationResult
<AnchorLink id="verify-purchase-result" level="h3">
VerifyPurchaseResult
</AnchorLink>
<p>
Union of <code>PurchaseVerificationResultIOS</code> and{' '}
<code>PurchaseVerificationResultAndroid</code>.
Union of <code>VerifyPurchaseResultIOS</code>,{' '}
<code>VerifyPurchaseResultAndroid</code>, and{' '}
<code>VerifyPurchaseResultHorizon</code>.
</p>
<PlatformTabs>
{{
ios: (
<>
<h4>PurchaseVerificationResultIOS</h4>
<h4>VerifyPurchaseResultIOS</h4>
<table className="doc-table">
<thead>
<tr>
Expand Down Expand Up @@ -2506,7 +2520,7 @@ Future<void> handleExternalPurchase(String externalUrl) async {
),
android: (
<>
<h4>PurchaseVerificationResultAndroid</h4>
<h4>VerifyPurchaseResultAndroid (Google Play)</h4>
<table className="doc-table">
<thead>
<tr>
Expand Down Expand Up @@ -2601,6 +2615,35 @@ Future<void> handleExternalPurchase(String externalUrl) async {
</tr>
</tbody>
</table>

<h4 style={{ marginTop: '2rem' }}>
VerifyPurchaseResultHorizon (Meta Quest)
</h4>
<table className="doc-table">
<thead>
<tr>
<th>Name</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>success</code>
</td>
<td>Whether the entitlement verification succeeded</td>
</tr>
<tr>
<td>
<code>grantTime</code>
</td>
<td>
Unix timestamp when the entitlement was granted (null if
verification failed)
</td>
</tr>
</tbody>
</table>
</>
),
}}
Expand Down
40 changes: 20 additions & 20 deletions packages/docs/src/pages/docs/updates/notes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ function Notes() {
return (
<div className="doc-page">
<SEO
title="Updates"
title="Notes"
description="Important changes and deprecations in IAP libraries and platforms - API changes, breaking changes, validateReceipt to verifyPurchase migration, and guides."
path="/docs/updates/notes"
keywords="IAP updates, validateReceipt, verifyPurchase, receipt validation, purchase verification, migration guide"
/>
<h1>Updates</h1>
<h1>Notes</h1>
<p>Important changes and deprecations in IAP libraries and platforms.</p>

<section>
Expand All @@ -29,29 +29,31 @@ function Notes() {
}}
>
<h4 style={{ marginTop: 0, color: 'var(--text-primary)' }}>
📅 openiap-gql v1.3.3 / openiap-google v1.3.13 / openiap-apple v1.3.1
📅 openiap-gql v1.3.4 / openiap-google v1.3.14 / openiap-apple v1.3.2
- Platform-Specific Verification Options
</h4>
<p>
<strong>verifyPurchase API Refactored:</strong>
<strong>verifyPurchase API Refactored (Breaking Change):</strong>
</p>
<p>
The <code>verifyPurchase</code> API now supports platform-specific
options for Apple, Google, and Meta Horizon stores.
The <code>verifyPurchase</code> API now requires platform-specific
options for Apple, Google, and Meta Horizon stores. The{' '}
<code>sku</code> field has been moved inside each platform-specific
options object.
</p>
<ul>
<li>
<strong>
<code>VerifyPurchaseAppleOptions</code>
</strong>{' '}
- Apple App Store verification with JWS token
- Apple App Store verification with sku
</li>
<li>
<strong>
<code>VerifyPurchaseGoogleOptions</code>
</strong>{' '}
- Google Play verification with packageName, purchaseToken, and
accessToken
- Google Play verification with sku, packageName, purchaseToken,
and accessToken
</li>
<li>
<strong>
Expand All @@ -65,11 +67,11 @@ function Notes() {
<strong>New VerifyPurchaseProps Structure:</strong>
</p>
<CodeBlock language="typescript">
{`// Platform-specific verification (recommended)
{`// Platform-specific verification
verifyPurchase({
sku: 'premium_monthly',
apple: { jws: 'eyJ...' }, // iOS App Store
apple: { sku: 'premium_monthly' }, // iOS App Store
google: { // Google Play
sku: 'premium_monthly',
packageName: 'com.example.app',
purchaseToken: 'token...',
accessToken: 'oauth_token...',
Expand All @@ -80,20 +82,18 @@ verifyPurchase({
userId: '123456789',
accessToken: 'OC|app_id|app_secret'
}
})

// Legacy format still supported (deprecated)
verifyPurchase({
sku: 'premium_monthly',
androidOptions: { ... } // @deprecated - use google instead
})`}
</CodeBlock>
<p>
<strong>Deprecations:</strong>
<strong>Breaking Changes:</strong>
</p>
<ul>
<li>
<code>androidOptions</code> in VerifyPurchaseProps → Use{' '}
<code>sku</code> removed from <code>VerifyPurchaseProps</code>{' '}
root level → Now inside each platform options
</li>
<li>
<code>androidOptions</code> completely removed → Use{' '}
<code>google</code> instead
</li>
</ul>
Expand Down
Loading
Loading