Skip to content

Commit b98b6d3

Browse files
hyochanclaude
andauthored
feat: sync with openiap v1.3.15 - simplify Android input field names (#3129)
## Summary - **Breaking Change**: Simplify Android input type field names (remove redundant `Android` suffix) - **New Feature**: Add `offerToken` support for one-time purchase discounts (Android 7.0+) - Sync with OpenIAP v1.3.15 (gql: 1.3.15, google: 1.3.26, apple: 1.3.13) ## Breaking Changes Fields inside `RequestPurchaseAndroidProps` and `RequestSubscriptionAndroidProps` no longer need `Android` suffix: | Old Name (v14.7.3) | New Name (v14.7.4) | |-------------------|-------------------| | `obfuscatedAccountIdAndroid` | `obfuscatedAccountId` | | `obfuscatedProfileIdAndroid` | `obfuscatedProfileId` | | `purchaseTokenAndroid` | `purchaseToken` | | `replacementModeAndroid` | `replacementMode` | **Note**: Response types (like `PurchaseAndroid.purchaseTokenAndroid`) keep their suffix. ## Migration ```typescript // Before await requestPurchase({ request: { google: { skus: ['subscription_id'], purchaseTokenAndroid: currentPurchaseToken, replacementModeAndroid: 1, }, }, type: 'subs', }); // After await requestPurchase({ request: { google: { skus: ['subscription_id'], purchaseToken: currentPurchaseToken, replacementMode: 1, }, }, type: 'subs', }); ``` ## Test plan - [x] TypeScript typecheck passes - [x] All 187 tests pass - [x] Nitro specs regenerated successfully - [x] Example apps updated - [x] Documentation updated with cross-platform examples 🤖 Generated with [Claude Code](https://claude.ai/code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for one-time purchase discount offers (Android 7.0+). * **Documentation** * Updated API docs, examples, and release notes with simplified Android input naming and a migration guide. * **Chores** * Simplified Android request field names (removed platform-specific suffixes) and aligned request payloads. * **Bug Fixes** * Improved parsing and error handling in purchase verification flows. * **Tests** * Added comprehensive tests for offer/token mappings and Android input typings. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 42d6f2b commit b98b6d3

File tree

15 files changed

+763
-75
lines changed

15 files changed

+763
-75
lines changed

android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ class HybridRnIap : HybridRnIapSpec() {
415415

416416
val requestProps = when (queryType) {
417417
ProductQueryType.Subs -> {
418-
val replacementMode = (androidRequest.replacementModeAndroid as? Number)?.toInt()
418+
val replacementMode = (androidRequest.replacementMode as? Number)?.toInt()
419419

420420
// Parse subscriptionProductReplacementParams (8.1.0+)
421421
val subscriptionProductReplacementParams = androidRequest.subscriptionProductReplacementParams?.let { params ->
@@ -427,10 +427,10 @@ class HybridRnIap : HybridRnIapSpec() {
427427

428428
val androidProps = RequestSubscriptionAndroidProps(
429429
isOfferPersonalized = androidRequest.isOfferPersonalized,
430-
obfuscatedAccountIdAndroid = androidRequest.obfuscatedAccountIdAndroid,
431-
obfuscatedProfileIdAndroid = androidRequest.obfuscatedProfileIdAndroid,
432-
purchaseTokenAndroid = androidRequest.purchaseTokenAndroid,
433-
replacementModeAndroid = replacementMode,
430+
obfuscatedAccountId = androidRequest.obfuscatedAccountId,
431+
obfuscatedProfileId = androidRequest.obfuscatedProfileId,
432+
purchaseToken = androidRequest.purchaseToken,
433+
replacementMode = replacementMode,
434434
skus = androidRequest.skus.toList(),
435435
subscriptionOffers = normalizedOffers,
436436
subscriptionProductReplacementParams = subscriptionProductReplacementParams
@@ -445,8 +445,9 @@ class HybridRnIap : HybridRnIapSpec() {
445445
ProductQueryType.InApp, ProductQueryType.All -> {
446446
val androidProps = RequestPurchaseAndroidProps(
447447
isOfferPersonalized = androidRequest.isOfferPersonalized,
448-
obfuscatedAccountIdAndroid = androidRequest.obfuscatedAccountIdAndroid,
449-
obfuscatedProfileIdAndroid = androidRequest.obfuscatedProfileIdAndroid,
448+
obfuscatedAccountId = androidRequest.obfuscatedAccountId,
449+
obfuscatedProfileId = androidRequest.obfuscatedProfileId,
450+
offerToken = androidRequest.offerToken,
450451
skus = androidRequest.skus.toList()
451452
)
452453
RequestPurchaseProps(
@@ -1396,6 +1397,7 @@ class HybridRnIap : HybridRnIapSpec() {
13961397
}
13971398

13981399
val props = dev.hyo.openiap.VerifyPurchaseWithProviderProps.fromJson(propsMap)
1400+
?: throw Exception("Failed to parse VerifyPurchaseWithProviderProps")
13991401
val result = openIap.verifyPurchaseWithProvider(props)
14001402

14011403
RnIapLog.result("verifyPurchaseWithProvider", mapOf("provider" to result.provider, "hasIapkit" to (result.iapkit != null)))

docs/CONVENTIONS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -501,9 +501,9 @@ await requestPurchase({
501501
quantity: 1,
502502
appAccountToken: 'user-123',
503503
},
504-
android: {
504+
google: {
505505
skus: [productId],
506-
obfuscatedAccountIdAndroid: 'user-123',
506+
obfuscatedAccountId: 'user-123',
507507
},
508508
},
509509
type: 'in-app',
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
---
2+
slug: 14.7.4-android-input-naming
3+
title: 14.7.4 - Android Input Type Field Naming Simplification
4+
authors: [hyochan]
5+
tags: [release, openiap, android, billing, breaking-change]
6+
date: 2026-01-20
7+
---
8+
9+
# 14.7.4 Release Notes
10+
11+
This release simplifies field naming in Android input types (`RequestPurchaseAndroidProps` and `RequestSubscriptionAndroidProps`). Since these types are already Android-specific, their fields no longer need the `Android` suffix.
12+
13+
## Breaking Changes
14+
15+
### Simplified Field Names in Android Input Types
16+
17+
Fields inside platform-specific input types no longer require the platform suffix. The parent type name already indicates the platform context.
18+
19+
**Why this change?**
20+
21+
When you write `google: { offerToken: "..." }`, the `google` key already tells you this is Android-specific. Adding `Android` suffix to fields inside is redundant:
22+
23+
```typescript
24+
// Redundant - we know it's Android from the parent
25+
google: { offerTokenAndroid: "..." }
26+
27+
// Cleaner - parent context is sufficient
28+
google: { offerToken: "..." }
29+
```
30+
31+
### Migration Guide
32+
33+
| Old Name (v14.7.3) | New Name (v14.7.4) |
34+
|-------------------|-------------------|
35+
| `obfuscatedAccountIdAndroid` | `obfuscatedAccountId` |
36+
| `obfuscatedProfileIdAndroid` | `obfuscatedProfileId` |
37+
| `purchaseTokenAndroid` | `purchaseToken` |
38+
| `replacementModeAndroid` | `replacementMode` |
39+
40+
**Before (v14.7.3):**
41+
42+
```typescript
43+
await requestPurchase({
44+
request: {
45+
google: {
46+
skus: ['subscription_id'],
47+
subscriptionOffers: [{sku: 'subscription_id', offerToken: 'token'}],
48+
purchaseTokenAndroid: currentPurchaseToken,
49+
replacementModeAndroid: 1,
50+
obfuscatedAccountIdAndroid: 'user_123',
51+
},
52+
},
53+
type: 'subs',
54+
});
55+
```
56+
57+
**After (v14.7.4):**
58+
59+
```typescript
60+
await requestPurchase({
61+
request: {
62+
google: {
63+
skus: ['subscription_id'],
64+
subscriptionOffers: [{sku: 'subscription_id', offerToken: 'token'}],
65+
purchaseToken: currentPurchaseToken,
66+
replacementMode: 1,
67+
obfuscatedAccountId: 'user_123',
68+
},
69+
},
70+
type: 'subs',
71+
});
72+
```
73+
74+
### Important: Response Types Keep Suffixes
75+
76+
The suffix removal only applies to **input types** (request parameters). Response types still use suffixes because they're cross-platform:
77+
78+
```typescript
79+
// Response fields KEEP the Android suffix
80+
const purchase = purchases[0] as PurchaseAndroid;
81+
82+
// These response fields still have Android suffix
83+
console.log(purchase.purchaseTokenAndroid); // Keep suffix
84+
console.log(purchase.obfuscatedAccountIdAndroid); // Keep suffix
85+
86+
// But input fields don't need it anymore
87+
await requestPurchase({
88+
request: {
89+
google: {
90+
skus: [product.id],
91+
obfuscatedAccountId: 'user_123', // Input: no suffix
92+
},
93+
},
94+
type: 'in-app',
95+
});
96+
```
97+
98+
## New Features
99+
100+
### One-Time Purchase Discount Offers (Android 7.0+)
101+
102+
This release adds support for discount offers on one-time (in-app) purchases:
103+
104+
```typescript
105+
import {fetchProducts, requestPurchase} from 'react-native-iap';
106+
import type {ProductAndroid} from 'react-native-iap';
107+
108+
// 1. Fetch products with discount offers
109+
const products = await fetchProducts({
110+
skus: ['premium_upgrade'],
111+
type: 'in-app',
112+
});
113+
114+
const product = products[0] as ProductAndroid;
115+
116+
// 2. Get the discount offer
117+
const discountOffer = product.discountOffers?.[0];
118+
119+
// 3. Purchase with the discount
120+
if (discountOffer?.offerTokenAndroid) {
121+
await requestPurchase({
122+
request: {
123+
google: {
124+
skus: [product.id],
125+
offerToken: discountOffer.offerTokenAndroid, // Use simplified input field
126+
},
127+
},
128+
type: 'in-app',
129+
});
130+
}
131+
```
132+
133+
## Naming Convention Summary
134+
135+
| Field Location | Suffix Required? | Example |
136+
|----------------|------------------|---------|
137+
| Inside `RequestPurchaseAndroidProps` | **NO** | `offerToken` |
138+
| Inside `RequestSubscriptionAndroidProps` | **NO** | `purchaseToken` |
139+
| Cross-platform response type | **YES** | `PurchaseAndroid.purchaseTokenAndroid` |
140+
141+
## OpenIAP Versions
142+
143+
| Package | Version |
144+
|---------|---------|
145+
| openiap-gql | 1.3.15 |
146+
| openiap-google | 1.3.26 |
147+
| openiap-apple | 1.3.13 |
148+
149+
For detailed changes, see the [OpenIAP Release Notes](https://www.openiap.dev/docs/updates/notes).

docs/docs/api/methods/unified-apis.md

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ const buySubscription = (subscriptionId: string, subscription?: any) => {
172172

173173
### Detailed Platform Examples
174174

175-
#### iOS Only
175+
#### Cross-Platform (Recommended)
176176

177177
```tsx
178178
await requestPurchase({
@@ -182,12 +182,16 @@ await requestPurchase({
182182
quantity: 1,
183183
appAccountToken: 'user-account-token',
184184
},
185+
google: {
186+
skus: [productId],
187+
obfuscatedAccountId: 'user-account-id',
188+
},
185189
},
186190
type: 'in-app',
187191
});
188192
```
189193

190-
#### iOS with Advanced Commerce Data (iOS 15+)
194+
#### With Advanced Commerce Data (iOS 15+)
191195

192196
Use `advancedCommerceData` to pass attribution data (campaign tokens, affiliate IDs) during purchase:
193197

@@ -198,19 +202,26 @@ await requestPurchase({
198202
sku: productId,
199203
advancedCommerceData: 'campaign_summer_2025',
200204
},
205+
google: {
206+
skus: [productId],
207+
},
201208
},
202209
type: 'in-app',
203210
});
204211
```
205212

206-
#### Android Only
213+
#### With One-Time Purchase Discount (Android 7.0+)
207214

208215
```tsx
209216
await requestPurchase({
210217
request: {
211-
skus: [productId],
212-
obfuscatedAccountIdAndroid: 'user-account-id',
213-
obfuscatedProfileIdAndroid: 'user-profile-id',
218+
apple: {
219+
sku: productId,
220+
},
221+
google: {
222+
skus: [productId],
223+
offerToken: discountOffer.offerTokenAndroid, // From product.discountOffers
224+
},
214225
},
215226
type: 'in-app',
216227
});
@@ -226,9 +237,10 @@ await requestPurchase({
226237
- `quantity?` (number, iOS only): Purchase quantity
227238
- `appAccountToken?` (string, iOS only): User identifier for purchase verification
228239
- `advancedCommerceData?` (string, iOS only): Campaign token or attribution data for StoreKit 2's `Product.PurchaseOption.custom` API (iOS 15+)
229-
- `obfuscatedAccountIdAndroid?` (string, Android only): Obfuscated account ID
230-
- `obfuscatedProfileIdAndroid?` (string, Android only): Obfuscated profile ID
240+
- `obfuscatedAccountId?` (string, Android only): Obfuscated account ID
241+
- `obfuscatedProfileId?` (string, Android only): Obfuscated profile ID
231242
- `isOfferPersonalized?` (boolean, Android only): Whether offer is personalized
243+
- `offerToken?` (string, Android 7.0+ only): Offer token for one-time purchase discounts
232244
- `type?` ('in-app' | 'subs'): Purchase type, defaults to 'in-app'
233245

234246
**Returns:** `Promise<Purchase | Purchase[] | void>`

docs/docs/examples/subscription-flow.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ await requestPurchase({
8686
google: {
8787
skus: ['premium_yearly'],
8888
subscriptionOffers: [...],
89-
purchaseTokenAndroid: currentPurchase.purchaseToken, // Required
90-
replacementModeAndroid: 1, // WITH_TIME_PRORATION
89+
purchaseToken: currentPurchase.purchaseToken, // Required
90+
replacementMode: 1, // WITH_TIME_PRORATION
9191
},
9292
},
9393
type: 'subs',
@@ -231,7 +231,7 @@ The [example app](https://github.com/hyochan/react-native-iap/blob/main/example/
231231
## Key Points
232232

233233
- **iOS**: Use subscription groups for automatic plan management
234-
- **Android**: Must include `subscriptionOffers` and `purchaseTokenAndroid` for upgrades
234+
- **Android**: Must include `subscriptionOffers` and `purchaseToken` for upgrades
235235
- **Validation**: Always validate receipts on your server before granting access
236236
- **Hook callbacks**: Use `onPurchaseSuccess` instead of promise chaining
237237

docs/static/llms-full.txt

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,8 @@ await requestPurchase({
280280
},
281281
google: {
282282
skus: ['product_id'],
283-
obfuscatedAccountIdAndroid: 'user-id', // Optional
283+
obfuscatedAccountId: 'user-id', // Optional
284+
offerToken: 'discount_token', // Optional (7.0+) - for one-time purchase discounts
284285
},
285286
},
286287
type: 'in-app',
@@ -647,17 +648,17 @@ interface RequestPurchaseIosProps {
647648

648649
interface RequestPurchaseAndroidProps {
649650
skus: string[];
650-
obfuscatedAccountIdAndroid?: string;
651-
obfuscatedProfileIdAndroid?: string;
651+
obfuscatedAccountId?: string;
652+
obfuscatedProfileId?: string;
652653
isOfferPersonalized?: boolean;
653-
subscriptionOffers?: Array<{sku: string; offerToken: string}>;
654-
purchaseTokenAndroid?: string;
655-
replacementModeAndroid?: number;
654+
offerToken?: string; // For one-time purchase discounts (7.0+)
656655
developerBillingOption?: DeveloperBillingOptionParamsAndroid;
657656
}
658657

659658
interface RequestSubscriptionAndroidProps extends RequestPurchaseAndroidProps {
660659
subscriptionOffers?: AndroidSubscriptionOfferInput[];
660+
purchaseToken?: string;
661+
replacementMode?: number;
661662
subscriptionProductReplacementParams?: SubscriptionProductReplacementParamsAndroid;
662663
}
663664
```
@@ -1089,7 +1090,7 @@ const upgradeSubscription = async (
10891090
google: {
10901091
skus: [newSubscriptionId],
10911092
subscriptionOffers: offers,
1092-
purchaseTokenAndroid: currentPurchaseToken,
1093+
purchaseToken: currentPurchaseToken,
10931094
subscriptionProductReplacementParams: {
10941095
oldProductId: 'old_subscription_id',
10951096
replacementMode: 'with-time-proration',

docs/static/llms.txt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,41 @@ await requestPurchase({
291291
});
292292
```
293293

294+
### Subscription Upgrade/Downgrade (Android)
295+
296+
```tsx
297+
await requestPurchase({
298+
request: {
299+
apple: {sku: 'premium_yearly'},
300+
google: {
301+
skus: ['premium_yearly'],
302+
subscriptionOffers: offers,
303+
purchaseToken: currentPurchaseToken, // Required for upgrades
304+
replacementMode: 1, // WITH_TIME_PRORATION
305+
},
306+
},
307+
type: 'subs',
308+
});
309+
```
310+
311+
### One-Time Purchase Discount (Android 7.0+, v14.7.4+)
312+
313+
```tsx
314+
const product = products.find(p => p.id === 'premium_unlock');
315+
const discountOffer = product?.discountOffers?.[0];
316+
317+
await requestPurchase({
318+
request: {
319+
apple: {sku: 'premium_unlock'},
320+
google: {
321+
skus: ['premium_unlock'],
322+
offerToken: discountOffer?.offerTokenAndroid, // Apply discount
323+
},
324+
},
325+
type: 'in-app',
326+
});
327+
```
328+
294329
### iOS Subscription Offers (v14.7.3+)
295330

296331
```tsx

0 commit comments

Comments
 (0)