Skip to content

Commit b4e5fcc

Browse files
hyochanclaude
andauthored
fix(android): delegate fetchProducts type 'all' to native query on Android (#239)
## Summary - Fixes `fetchProducts({ type: 'all' })` on Android returning subscriptions as `in-app` with `productStatusAndroid: 'not-found'`. - Root cause: the react-native-iap Android bridge expanded `all` into separate in-app and subs queries and merged them with `putIfAbsent`. Since openiap-google 2.4.x, each per-type query returns a not-found placeholder row for every requested sku of the other type, so the in-app placeholder shadowed the real subscription from the subs query. - Fix: pass the query type through unchanged so the native module resolves `ProductQueryType.All` itself (all three store flavors — play/horizon/amazon — already handle it correctly, matching each sku as in-app first, then subs, then a typed unavailable placeholder). This also stops the `productTypeBySku` cache from being poisoned with `in-app` for subscription skus, which previously broke subsequent `requestPurchase` type hints. - Removes the now-unused `collectAllQueryProducts` helper and its tests (one test baked the buggy first-match-wins semantics in as expected behavior). - Adds a release-note entry to `packages/docs` documenting the fix as the react-native-iap 15.5.4 release (assumed-published mode; the release-tag link goes live once 15.5.4 is deployed). - Other SDKs (expo-iap, flutter_inapp_purchase, godot-iap, maui-iap, kmp-iap) already delegate `all` to the native layer and are unaffected. Closes #238 ## Test plan - [x] `yarn typecheck` passes (react-native-iap) - [x] `yarn lint` passes (react-native-iap) - [x] `bunx prettier --check`, `bun run build`, `bun run audit:docs`, `git diff --check` pass (packages/docs) - [ ] Android CI build passes 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_0154xivm4YeCVS9yxk54bHL6 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved Android `fetchProducts({ type: 'all' })` behavior to fetch products more consistently and align closer to native handling. * Preserves the requested SKU order and includes additional fetched products without dropping unmatched results. * **Documentation** * Added a `react-native-iap 15.5.4` release note describing the Android `fetchProducts({ type: 'all' })` fix and updated the package release links. * **Tests** * Removed Android product-query helper tests that covered the previous multi-part “all” query behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8b2c385 commit b4e5fcc

4 files changed

Lines changed: 128 additions & 217 deletions

File tree

libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt

Lines changed: 24 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -402,43 +402,32 @@ class HybridRnIap : HybridRnIapSpec() {
402402
val skusList = skus.toList()
403403

404404
val products: List<ProductCommon> = try {
405-
when (queryType) {
406-
ProductQueryType.All -> {
407-
collectAllQueryProducts(
408-
skusList = skusList,
409-
fetchKind = { kind ->
410-
RnIapLog.payload(
411-
"fetchProducts.native",
412-
mapOf("skus" to skusList, "type" to kind.rawValue)
413-
)
414-
val fetched = openIap.fetchProducts(ProductRequest(skusList, kind)).productsOrEmpty()
415-
RnIapLog.result(
416-
"fetchProducts.native",
417-
fetched.map { mapOf("id" to it.id, "type" to it.type.rawValue) }
418-
)
419-
fetched
420-
},
421-
onFailure = { kind, error ->
422-
RnIapLog.failure("fetchProducts.native[${kind.rawValue}]", error)
423-
},
424-
)
425-
}
426-
else -> {
427-
RnIapLog.payload(
428-
"fetchProducts.native",
429-
mapOf("skus" to skusList, "type" to queryType.rawValue)
430-
)
431-
val fetched = openIap.fetchProducts(ProductRequest(skusList, queryType)).productsOrEmpty()
432-
RnIapLog.result(
433-
"fetchProducts.native",
434-
fetched.map { mapOf("id" to it.id, "type" to it.type.rawValue) }
435-
)
405+
RnIapLog.payload(
406+
"fetchProducts.native",
407+
mapOf("skus" to skusList, "type" to queryType.rawValue)
408+
)
409+
// Pass the query type through unchanged: the native module resolves
410+
// ProductQueryType.All itself, so each sku keeps its real product type
411+
// instead of being shadowed by a per-type not-found placeholder.
412+
val fetched = openIap.fetchProducts(ProductRequest(skusList, queryType)).productsOrEmpty()
413+
RnIapLog.result(
414+
"fetchProducts.native",
415+
fetched.map { mapOf("id" to it.id, "type" to it.type.rawValue) }
416+
)
436417

437-
// Preserve input order for non-All queries
438-
val byId = fetched.associateBy { it.id }
439-
skusList.mapNotNull { byId[it] }
440-
}
418+
// Match the iOS bridge's result shape: preserve input order,
419+
// drop duplicate skus, then append any fetched products that
420+
// were not requested.
421+
val byId = fetched.associateBy { it.id }
422+
val seenIds = mutableSetOf<String>()
423+
val orderedProducts = mutableListOf<ProductCommon>()
424+
skusList.forEach { sku ->
425+
byId[sku]?.takeIf { seenIds.add(it.id) }?.let(orderedProducts::add)
426+
}
427+
fetched.forEach { product ->
428+
if (seenIds.add(product.id)) orderedProducts.add(product)
441429
}
430+
orderedProducts
442431
} catch (e: OpenIapError) {
443432
throw OpenIapException(toErrorJson(e))
444433
}

libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/ProductQueryHelpers.kt

Lines changed: 0 additions & 42 deletions
This file was deleted.

libraries/react-native-iap/android/src/test/java/com/margelo/nitro/iap/ProductQueryHelpersTest.kt

Lines changed: 0 additions & 140 deletions
This file was deleted.

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

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ interface Note {
2222
element: React.ReactNode;
2323
}
2424

25+
const fetchProductsAllFixReleases = [
26+
['react-native-iap 15.5.4', 'react-native-iap-15.5.4'],
27+
] as const;
28+
2529
const crossSdkAuditReleases = [
2630
['react-native-iap 15.5.3', 'react-native-iap-15.5.3'],
2731
['expo-iap 4.6.0', 'expo-iap-4.6.0'],
@@ -70,6 +74,106 @@ function Releases() {
7074
useScrollToHash();
7175

7276
const allNotes: Note[] = [
77+
// July 20, 2026 - react-native-iap 15.5.4
78+
{
79+
id: 'react-native-iap-fetch-products-all-fix-2026-07-20',
80+
date: new Date('2026-07-20'),
81+
element: (
82+
<div
83+
key="react-native-iap-fetch-products-all-fix-2026-07-20"
84+
style={noteCardStyle}
85+
>
86+
<AnchorLink
87+
id="react-native-iap-fetch-products-all-fix-2026-07-20"
88+
level="h4"
89+
>
90+
July 20, 2026 - react-native-iap 15.5.4
91+
</AnchorLink>
92+
93+
<p
94+
style={{
95+
marginBottom: '1rem',
96+
color: 'var(--text-secondary)',
97+
}}
98+
>
99+
Publishes a focused Android patch for the React Native library. The
100+
OpenIAP Spec and native package versions are unchanged. The
101+
regression was reported in{' '}
102+
<a
103+
href="https://github.com/hyodotdev/openiap/issues/238"
104+
target="_blank"
105+
rel="noopener noreferrer"
106+
className="external-link"
107+
>
108+
issue #238
109+
</a>{' '}
110+
and fixed in{' '}
111+
<a
112+
href="https://github.com/hyodotdev/openiap/pull/239"
113+
target="_blank"
114+
rel="noopener noreferrer"
115+
className="external-link"
116+
>
117+
PR #239
118+
</a>
119+
.
120+
</p>
121+
122+
<h5 style={{ margin: '0 0 0.5rem 0' }}>React Native</h5>
123+
<ul
124+
style={{
125+
marginBottom: '1rem',
126+
paddingLeft: '1.25rem',
127+
fontSize: '0.9rem',
128+
}}
129+
>
130+
<li>
131+
<strong>react-native-iap 15.5.4</strong> - fixes Android{' '}
132+
<code>fetchProducts(&#123; type: &apos;all&apos; &#125;)</code>{' '}
133+
returning subscriptions as <code>in-app</code> with{' '}
134+
<code>productStatusAndroid: &apos;not-found&apos;</code>. The
135+
Android bridge previously expanded <code>all</code> into separate
136+
in-app and subscription queries and merged them first-match-wins,
137+
so the in-app query&apos;s not-found placeholder row shadowed the
138+
real subscription. The bridge now delegates <code>all</code> to
139+
the native query, so each sku keeps its real product type and
140+
status, and the internal product-type cache no longer mislabels
141+
subscription skus (which could break a subsequent{' '}
142+
<code>requestPurchase</code>).
143+
</li>
144+
</ul>
145+
146+
<div
147+
style={{
148+
paddingTop: '1rem',
149+
borderTop: '1px solid var(--border-color)',
150+
}}
151+
>
152+
<h5 style={{ margin: '0 0 0.5rem 0' }}>Package Releases</h5>
153+
<ul
154+
style={{
155+
margin: 0,
156+
paddingLeft: '1.25rem',
157+
fontSize: '0.9rem',
158+
}}
159+
>
160+
{fetchProductsAllFixReleases.map(([label, tag]) => (
161+
<li key={tag}>
162+
<a
163+
href={`https://github.com/hyodotdev/openiap/releases/tag/${tag}`}
164+
target="_blank"
165+
rel="noopener noreferrer"
166+
>
167+
{label}
168+
</a>
169+
</li>
170+
))}
171+
</ul>
172+
</div>
173+
</div>
174+
),
175+
},
176+
73177
// July 20, 2026 - Cross-SDK audit fixes and IAPKit verified-state accuracy
74178
{
75179
id: 'cross-sdk-audit-fixes-2026-07-20',

0 commit comments

Comments
 (0)