Skip to content

Commit a656549

Browse files
authored
Refactor Android IAP integration and examples (#197)
### Summary - Move Android mapping to openiap-google toJSON and remove local records/mappers - Replace PlayUtils with PromiseUtils; centralize promise helpers - Unify Android APIs: getAvailableItems, consumePurchaseAndroid - Add deepLinkToSubscriptionsAndroid + getStorefrontAndroid bridge - Update example screen: active subscription modal, storefront/deeplink tools - Deduplicate active subscriptions by stable key (token/id) ### Notes Aligns platform behavior and simplifies JS bridge. Keeps deprecation shims where helpful; removes legacy aliases where safe. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Android storefront retrieval and native deep-link to manage subscriptions; unified available-purchases API; new purchase-updated/purchase-error events and exported ERROR_CODES; cross-platform getStorefront behavior. - Bug Fixes - Deduplicated active subscriptions to prevent duplicate entries. - Refactor - Android API rename: consumeProductAndroid → consumePurchaseAndroid. - Plugin - Optional local OpenIAP linking with dependency fallback, addDeps toggle, and localPath now supports ios/android. - Documentation - Updated examples and migration notes for the Android rename. - Chores - Added pre-commit test hook and spell-check dictionary entry. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent a01c8d7 commit a656549

File tree

28 files changed

+915
-1091
lines changed

28 files changed

+915
-1091
lines changed

.eslintrc.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ module.exports = {
1212
paths: [
1313
{
1414
name: '.',
15-
message: "Avoid `import from '.'`; use './index' or an explicit path.",
15+
message:
16+
"Avoid `import from '.'`; use './index' or an explicit path.",
1617
},
1718
],
1819
},
@@ -23,8 +24,7 @@ module.exports = {
2324
paths: [
2425
{
2526
name: '.',
26-
message:
27-
"Avoid `require('.')`; use './index' or an explicit path.",
27+
message: "Avoid `require('.')`; use './index' or an explicit path.",
2828
},
2929
],
3030
},

.github/ISSUE_TEMPLATE/bug_report.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ assignees: ''
3232
**Latest Version Check**
3333

3434
- [ ] I confirmed this issue reproduces on the latest version of expo-iap.
35-
<!-- The library continuously fixes past issues; please try the latest release before filing. -->
35+
<!-- The library continuously fixes past issues; please try the latest release before filing. -->
3636

3737
**To Reproduce** Steps to reproduce the behavior:
3838

.husky/pre-commit

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env sh
2+
. "$(dirname "$0")/_/husky.sh"
3+
4+
# Lint TypeScript/JS (and Kotlin if available)
5+
echo "Linting (tsc/eslint/prettier/kt)…"
6+
bun run lint:ci || {
7+
echo "Lint failed. Aborting commit." >&2
8+
exit 1
9+
}
10+
11+
# Run tests with coverage before committing to reduce CI failures
12+
echo "Running test coverage (pre-commit)…"
13+
bun run test:coverage || {
14+
echo "Tests failed. Aborting commit." >&2
15+
exit 1
16+
}

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
"expo",
8989
"gradle",
9090
"hyochan",
91+
"hyodotdev",
9192
"iap",
9293
"inapp",
9394
"ktlint",

android/build.gradle

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ android {
5353

5454
dependencies {
5555
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7"
56-
implementation "com.android.billingclient:billing-ktx:8.0.0"
57-
implementation "com.google.android.gms:play-services-base:18.1.0"
56+
// Use OpenIAP Google module only; avoid direct BillingClient dependency
57+
if (findProject(":openiap-google") != null) {
58+
implementation project(":openiap-google")
59+
} else {
60+
// Fallback to published artifact when local project isn't linked
61+
implementation "io.github.hyochan.openiap:openiap-google:1.0.1"
62+
}
5863
}

android/src/main/java/expo/modules/iap/ExpoIapModule.kt

Lines changed: 170 additions & 652 deletions
Large diffs are not rendered by default.

android/src/main/java/expo/modules/iap/PlayUtils.kt

Lines changed: 0 additions & 178 deletions
This file was deleted.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package expo.modules.iap
2+
3+
import android.util.Log
4+
import dev.hyo.openiap.OpenIapError
5+
import expo.modules.kotlin.Promise
6+
7+
object PromiseUtils {
8+
private val promises = java.util.concurrent.ConcurrentHashMap<String, java.util.concurrent.CopyOnWriteArrayList<Promise>>()
9+
10+
const val TAG = "PromiseUtils"
11+
12+
// React Native specific promise key used by JS bridge
13+
const val PROMISE_BUY_ITEM = "PROMISE_BUY_ITEM"
14+
15+
fun addPromiseForKey(
16+
key: String,
17+
promise: Promise,
18+
) {
19+
promises.computeIfAbsent(key) { java.util.concurrent.CopyOnWriteArrayList() }.add(promise)
20+
}
21+
22+
fun resolvePromisesForKey(
23+
key: String,
24+
value: Any?,
25+
) {
26+
promises[key]?.forEach { promise ->
27+
promise.safeResolve(value)
28+
}
29+
promises.remove(key)
30+
}
31+
32+
fun rejectAllPendingPromises() {
33+
// Snapshot to avoid concurrent modification
34+
promises.values.flatMap { it.toList() }.forEach { promise ->
35+
promise.safeReject(OpenIapError.E_CONNECTION_CLOSED, "Connection has been closed", null)
36+
}
37+
promises.clear()
38+
}
39+
40+
fun rejectPromisesForKey(
41+
key: String,
42+
code: String,
43+
message: String?,
44+
err: Exception?,
45+
) {
46+
promises[key]?.forEach { promise ->
47+
promise.safeReject(code, message, err)
48+
}
49+
promises.remove(key)
50+
}
51+
}
52+
53+
const val TAG = "IapPromises"
54+
55+
fun Promise.safeResolve(value: Any?) {
56+
try {
57+
this.resolve(value)
58+
} catch (e: RuntimeException) {
59+
Log.d(TAG, "Already consumed ${e.message}")
60+
}
61+
}
62+
63+
fun Promise.safeReject(message: String) = this.safeReject("E_UNKNOWN", message, null)
64+
65+
fun Promise.safeReject(
66+
code: String,
67+
message: String?,
68+
) = this.safeReject(code, message, null)
69+
70+
fun Promise.safeReject(
71+
code: String,
72+
throwable: Throwable?,
73+
) = this.safeReject(code, null, throwable)
74+
75+
fun Promise.safeReject(
76+
code: String,
77+
message: String?,
78+
throwable: Throwable?,
79+
) {
80+
try {
81+
this.reject(code, message, throwable)
82+
} catch (e: RuntimeException) {
83+
Log.d(TAG, "Already consumed ${e.message}")
84+
}
85+
}

0 commit comments

Comments
 (0)