Skip to content

Commit b5c1dce

Browse files
authored
refactor: update verifyPurchase to support platform-specific options (#53)
- Add VerifyPurchaseAppleOptions (jws) for iOS App Store verification - Add VerifyPurchaseGoogleOptions (packageName, purchaseToken, accessToken, isSub) for Play Store - Add VerifyPurchaseHorizonOptions (sku, userId, accessToken) for Meta Quest S2S API - Update VerifyPurchaseProps to accept apple, google, horizon fields - Deprecate androidOptions in favor of google field - Implement verifyPurchaseWithHorizon() for Meta S2S verification - Add platform-specific documentation comments Note: Pending version updates for release: - openiap-gql: 1.3.3 - openiap-google: 1.3.13 - openiap-apple: 1.3.1 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Multi-platform purchase/subscription verification: Apple App Store, Google Play, and Meta Horizon support. * Platform-specific verification inputs and IAPKit verification flows for Apple, Google, and Horizon. * Option to supply an external Apple JWS to short‑circuit local receipt fetch/validation. * **Documentation** * Updated API docs with platform-specific verification examples, schema changes, and deprecation notes for legacy Android options. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 3049ca7 commit b5c1dce

14 files changed

Lines changed: 1281 additions & 143 deletions

File tree

packages/apple/Sources/Models/Types.swift

Lines changed: 100 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,15 @@ public struct VerifyPurchaseResultAndroid: Codable {
844844
public var testTransaction: Bool
845845
}
846846

847+
/// Result from Meta Horizon verify_entitlement API.
848+
/// Returns verification status and grant time for the entitlement.
849+
public struct VerifyPurchaseResultHorizon: Codable {
850+
/// Unix timestamp (seconds) when the entitlement was granted.
851+
public var grantTime: Double?
852+
/// Whether the entitlement verification succeeded.
853+
public var success: Bool
854+
}
855+
847856
public struct VerifyPurchaseResultIOS: Codable {
848857
/// Whether the receipt is valid
849858
public var isValid: Bool
@@ -1142,6 +1151,12 @@ public struct RequestPurchaseProps: Codable {
11421151
}
11431152
}
11441153

1154+
/// Platform-specific purchase request parameters.
1155+
///
1156+
/// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store.
1157+
/// - apple: Always targets App Store
1158+
/// - google: Targets Play Store by default, or Horizon when built with horizon flavor
1159+
/// (determined at build time, not runtime)
11451160
public struct RequestPurchasePropsByPlatforms: Codable {
11461161
/// @deprecated Use google instead
11471162
public var android: RequestPurchaseAndroidProps?
@@ -1228,6 +1243,12 @@ public struct RequestSubscriptionIosProps: Codable {
12281243
}
12291244
}
12301245

1246+
/// Platform-specific subscription request parameters.
1247+
///
1248+
/// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store.
1249+
/// - apple: Always targets App Store
1250+
/// - google: Targets Play Store by default, or Horizon when built with horizon flavor
1251+
/// (determined at build time, not runtime)
12311252
public struct RequestSubscriptionPropsByPlatforms: Codable {
12321253
/// @deprecated Use google instead
12331254
public var android: RequestSubscriptionAndroidProps?
@@ -1273,12 +1294,16 @@ public struct RequestVerifyPurchaseWithIapkitGoogleProps: Codable {
12731294
}
12741295
}
12751296

1297+
/// Platform-specific verification parameters for IAPKit.
1298+
///
1299+
/// - apple: Verifies via App Store (JWS token)
1300+
/// - google: Verifies via Play Store (purchase token)
12761301
public struct RequestVerifyPurchaseWithIapkitProps: Codable {
12771302
/// API key used for the Authorization header (Bearer {apiKey}).
12781303
public var apiKey: String?
1279-
/// Apple verification parameters.
1304+
/// Apple App Store verification parameters.
12801305
public var apple: RequestVerifyPurchaseWithIapkitAppleProps?
1281-
/// Google verification parameters.
1306+
/// Google Play Store verification parameters.
12821307
public var google: RequestVerifyPurchaseWithIapkitGoogleProps?
12831308

12841309
public init(
@@ -1310,36 +1335,100 @@ public struct SubscriptionProductReplacementParamsAndroid: Codable {
13101335
}
13111336
}
13121337

1313-
public struct VerifyPurchaseAndroidOptions: Codable {
1338+
/// Apple App Store verification parameters.
1339+
/// Used for server-side receipt validation via App Store Server API.
1340+
///
1341+
/// ⚠️ SECURITY: Contains sensitive token (jws). Do not log or persist this data.
1342+
public struct VerifyPurchaseAppleOptions: Codable {
1343+
/// The JWS (JSON Web Signature) representation of the transaction.
1344+
/// ⚠️ Sensitive: Do not log this value.
1345+
public var jws: String
1346+
1347+
public init(
1348+
jws: String
1349+
) {
1350+
self.jws = jws
1351+
}
1352+
}
1353+
1354+
/// Google Play Store verification parameters.
1355+
/// Used for server-side receipt validation via Google Play Developer API.
1356+
///
1357+
/// ⚠️ SECURITY: Contains sensitive tokens (accessToken, purchaseToken). Do not log or persist this data.
1358+
public struct VerifyPurchaseGoogleOptions: Codable {
1359+
/// Google OAuth2 access token for API authentication.
1360+
/// ⚠️ Sensitive: Do not log this value.
13141361
public var accessToken: String
1362+
/// Whether this is a subscription purchase (affects API endpoint used)
13151363
public var isSub: Bool?
1364+
/// Android package name (e.g., com.example.app)
13161365
public var packageName: String
1317-
public var productToken: String
1366+
/// Purchase token from the purchase response.
1367+
/// ⚠️ Sensitive: Do not log this value.
1368+
public var purchaseToken: String
13181369

13191370
public init(
13201371
accessToken: String,
13211372
isSub: Bool? = nil,
13221373
packageName: String,
1323-
productToken: String
1374+
purchaseToken: String
13241375
) {
13251376
self.accessToken = accessToken
13261377
self.isSub = isSub
13271378
self.packageName = packageName
1328-
self.productToken = productToken
1379+
self.purchaseToken = purchaseToken
13291380
}
13301381
}
13311382

1383+
/// Meta Horizon (Quest) verification parameters.
1384+
/// Used for server-side entitlement verification via Meta's S2S API.
1385+
/// POST https://graph.oculus.com/$APP_ID/verify_entitlement
1386+
///
1387+
/// ⚠️ SECURITY: Contains sensitive token (accessToken). Do not log or persist this data.
1388+
public struct VerifyPurchaseHorizonOptions: Codable {
1389+
/// Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token).
1390+
/// ⚠️ Sensitive: Do not log this value.
1391+
public var accessToken: String
1392+
/// The SKU for the add-on item, defined in Meta Developer Dashboard
1393+
public var sku: String
1394+
/// The user ID of the user whose purchase you want to verify
1395+
public var userId: String
1396+
1397+
public init(
1398+
accessToken: String,
1399+
sku: String,
1400+
userId: String
1401+
) {
1402+
self.accessToken = accessToken
1403+
self.sku = sku
1404+
self.userId = userId
1405+
}
1406+
}
1407+
1408+
/// Platform-specific purchase verification parameters.
1409+
///
1410+
/// - apple: Verifies via App Store Server API
1411+
/// - google: Verifies via Google Play Developer API
1412+
/// - horizon: Verifies via Meta's S2S API (verify_entitlement endpoint)
13321413
public struct VerifyPurchaseProps: Codable {
1333-
/// Android-specific validation options
1334-
public var androidOptions: VerifyPurchaseAndroidOptions?
1414+
/// Apple App Store verification parameters.
1415+
public var apple: VerifyPurchaseAppleOptions?
1416+
/// Google Play Store verification parameters.
1417+
public var google: VerifyPurchaseGoogleOptions?
1418+
/// Meta Horizon (Quest) verification parameters.
1419+
public var horizon: VerifyPurchaseHorizonOptions?
13351420
/// Product SKU to validate
13361421
public var sku: String
13371422

13381423
public init(
1339-
androidOptions: VerifyPurchaseAndroidOptions? = nil,
1424+
apple: VerifyPurchaseAppleOptions? = nil,
1425+
google: VerifyPurchaseGoogleOptions? = nil,
1426+
horizon: VerifyPurchaseHorizonOptions? = nil,
13401427
sku: String
13411428
) {
1342-
self.androidOptions = androidOptions
1429+
self.apple = apple
1430+
self.google = google
1431+
self.horizon = horizon
13431432
self.sku = sku
13441433
}
13451434
}
@@ -1667,6 +1756,7 @@ public enum Purchase: Codable, PurchaseCommon {
16671756
public enum VerifyPurchaseResult: Codable {
16681757
case verifyPurchaseResultAndroid(VerifyPurchaseResultAndroid)
16691758
case verifyPurchaseResultIos(VerifyPurchaseResultIOS)
1759+
case verifyPurchaseResultHorizon(VerifyPurchaseResultHorizon)
16701760
}
16711761

16721762
// MARK: - Root Operations

packages/apple/Sources/OpenIapModule.swift

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -598,16 +598,25 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol {
598598
var jws: String = ""
599599
var isValid = false
600600

601-
do {
602-
let product = try await storeProduct(for: props.sku)
603-
if let result = await product.latestTransaction {
604-
jws = result.jwsRepresentation
605-
let transaction = try checkVerified(result)
606-
latestPurchase = .purchaseIos(await StoreKitTypesBridge.purchaseIOS(from: transaction, jwsRepresentation: result.jwsRepresentation))
607-
isValid = true
601+
// If apple options with JWS are provided, use that directly
602+
// Otherwise, fetch the latest transaction from StoreKit
603+
if let appleOptions = props.apple, !appleOptions.jws.isEmpty {
604+
jws = appleOptions.jws
605+
// When JWS is provided externally, we trust it's valid
606+
// The caller should verify the JWS on their server
607+
isValid = true
608+
} else {
609+
do {
610+
let product = try await storeProduct(for: props.sku)
611+
if let result = await product.latestTransaction {
612+
jws = result.jwsRepresentation
613+
let transaction = try checkVerified(result)
614+
latestPurchase = .purchaseIos(await StoreKitTypesBridge.purchaseIOS(from: transaction, jwsRepresentation: result.jwsRepresentation))
615+
isValid = true
616+
}
617+
} catch {
618+
isValid = false
608619
}
609-
} catch {
610-
isValid = false
611620
}
612621

613622
return VerifyPurchaseResultIOS(

packages/docs/src/pages/docs/updates/notes.tsx

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,91 @@ function Notes() {
1919
<section>
2020
<h2>📝 API & Terminology Changes</h2>
2121

22+
<div
23+
style={{
24+
background: 'var(--bg-secondary)',
25+
border: '1px solid var(--border-color)',
26+
borderRadius: '0.5rem',
27+
padding: '1rem',
28+
marginBottom: '1.5rem',
29+
}}
30+
>
31+
<h4 style={{ marginTop: 0, color: 'var(--text-primary)' }}>
32+
📅 openiap-gql v1.3.3 / openiap-google v1.3.13 / openiap-apple v1.3.1
33+
- Platform-Specific Verification Options
34+
</h4>
35+
<p>
36+
<strong>verifyPurchase API Refactored:</strong>
37+
</p>
38+
<p>
39+
The <code>verifyPurchase</code> API now supports platform-specific
40+
options for Apple, Google, and Meta Horizon stores.
41+
</p>
42+
<ul>
43+
<li>
44+
<strong>
45+
<code>VerifyPurchaseAppleOptions</code>
46+
</strong>{' '}
47+
- Apple App Store verification with JWS token
48+
</li>
49+
<li>
50+
<strong>
51+
<code>VerifyPurchaseGoogleOptions</code>
52+
</strong>{' '}
53+
- Google Play verification with packageName, purchaseToken, and
54+
accessToken
55+
</li>
56+
<li>
57+
<strong>
58+
<code>VerifyPurchaseHorizonOptions</code>
59+
</strong>{' '}
60+
- Meta Horizon (Quest) verification via S2S API with sku, userId,
61+
and accessToken
62+
</li>
63+
</ul>
64+
<p>
65+
<strong>New VerifyPurchaseProps Structure:</strong>
66+
</p>
67+
<CodeBlock language="typescript">
68+
{`// Platform-specific verification (recommended)
69+
verifyPurchase({
70+
sku: 'premium_monthly',
71+
apple: { jws: 'eyJ...' }, // iOS App Store
72+
google: { // Google Play
73+
packageName: 'com.example.app',
74+
purchaseToken: 'token...',
75+
accessToken: 'oauth_token...',
76+
isSub: true
77+
},
78+
horizon: { // Meta Quest
79+
sku: '50_gems',
80+
userId: '123456789',
81+
accessToken: 'OC|app_id|app_secret'
82+
}
83+
})
84+
85+
// Legacy format still supported (deprecated)
86+
verifyPurchase({
87+
sku: 'premium_monthly',
88+
androidOptions: { ... } // @deprecated - use google instead
89+
})`}
90+
</CodeBlock>
91+
<p>
92+
<strong>Deprecations:</strong>
93+
</p>
94+
<ul>
95+
<li>
96+
<code>androidOptions</code> in VerifyPurchaseProps → Use{' '}
97+
<code>google</code> instead
98+
</li>
99+
</ul>
100+
<p>
101+
See:{' '}
102+
<a href="/docs/apis#verify-purchase">verifyPurchase API</a>,{' '}
103+
<a href="/docs/types#verify-purchase-props">VerifyPurchaseProps</a>
104+
</p>
105+
</div>
106+
22107
<div
23108
style={{
24109
background: 'var(--bg-secondary)',

packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import dev.hyo.openiap.utils.HorizonBillingConverters.toPurchase
3434
import dev.hyo.openiap.utils.HorizonBillingConverters.toSubscriptionProduct
3535
import dev.hyo.openiap.utils.toProduct
3636
import dev.hyo.openiap.utils.verifyPurchaseWithGooglePlay
37+
import dev.hyo.openiap.utils.verifyPurchaseWithHorizon
3738
import dev.hyo.openiap.MutationVerifyPurchaseHandler
3839
import dev.hyo.openiap.MutationValidateReceiptHandler
3940
import dev.hyo.openiap.MutationVerifyPurchaseWithProviderHandler
@@ -654,7 +655,17 @@ class OpenIapModule(
654655
}
655656

656657
override val verifyPurchase: MutationVerifyPurchaseHandler = { props ->
657-
verifyPurchaseWithGooglePlay(props, TAG)
658+
// Use Horizon API if horizon options provided, otherwise fallback to Google Play
659+
if (props.horizon != null) {
660+
val horizonAppId = appId ?: throw OpenIapError.DeveloperError
661+
val horizonResult = verifyPurchaseWithHorizon(props, horizonAppId, TAG)
662+
if (!horizonResult.success) {
663+
throw OpenIapError.InvalidPurchaseVerification
664+
}
665+
horizonResult
666+
} else {
667+
verifyPurchaseWithGooglePlay(props, TAG)
668+
}
658669
}
659670

660671
override val verifyPurchaseWithProvider: MutationVerifyPurchaseWithProviderHandler = { props ->

0 commit comments

Comments
 (0)