expo-iap 2.9.7 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +1 -1
- package/android/build.gradle +7 -2
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +195 -650
- package/android/src/main/java/expo/modules/iap/PromiseUtils.kt +85 -0
- package/build/ExpoIap.types.d.ts +0 -6
- package/build/ExpoIap.types.d.ts.map +1 -1
- package/build/ExpoIap.types.js.map +1 -1
- package/build/helpers/subscription.d.ts.map +1 -1
- package/build/helpers/subscription.js +14 -3
- package/build/helpers/subscription.js.map +1 -1
- package/build/index.d.ts +6 -73
- package/build/index.d.ts.map +1 -1
- package/build/index.js +21 -154
- package/build/index.js.map +1 -1
- package/build/modules/android.d.ts +2 -2
- package/build/modules/android.d.ts.map +1 -1
- package/build/modules/android.js +11 -1
- package/build/modules/android.js.map +1 -1
- package/build/modules/ios.d.ts +0 -60
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +2 -121
- package/build/modules/ios.js.map +1 -1
- package/build/types/ExpoIapAndroid.types.d.ts +0 -8
- package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
- package/build/types/ExpoIapAndroid.types.js +0 -1
- package/build/types/ExpoIapAndroid.types.js.map +1 -1
- package/build/types/ExpoIapIOS.types.d.ts +0 -5
- package/build/types/ExpoIapIOS.types.d.ts.map +1 -1
- package/build/types/ExpoIapIOS.types.js.map +1 -1
- package/build/useIAP.d.ts +0 -18
- package/build/useIAP.d.ts.map +1 -1
- package/build/useIAP.js +1 -18
- package/build/useIAP.js.map +1 -1
- package/bun.lock +340 -137
- package/codecov.yml +17 -21
- package/ios/ExpoIapModule.swift +10 -3
- package/jest.config.js +5 -9
- package/package.json +5 -3
- package/plugin/build/withIAP.d.ts +4 -1
- package/plugin/build/withIAP.js +38 -24
- package/plugin/build/withLocalOpenIAP.d.ts +6 -2
- package/plugin/build/withLocalOpenIAP.js +175 -20
- package/plugin/src/withIAP.ts +66 -30
- package/plugin/src/withLocalOpenIAP.ts +228 -24
- package/src/ExpoIap.types.ts +0 -8
- package/src/helpers/subscription.ts +14 -3
- package/src/index.ts +22 -230
- package/src/modules/android.ts +16 -6
- package/src/modules/ios.ts +2 -168
- package/src/types/ExpoIapAndroid.types.ts +0 -11
- package/src/types/ExpoIapIOS.types.ts +0 -5
- package/src/useIAP.ts +3 -55
- package/android/src/main/java/expo/modules/iap/PlayUtils.kt +0 -178
- package/android/src/main/java/expo/modules/iap/Types.kt +0 -98
package/codecov.yml
CHANGED
|
@@ -6,8 +6,8 @@ codecov:
|
|
|
6
6
|
coverage:
|
|
7
7
|
precision: 2
|
|
8
8
|
round: down
|
|
9
|
-
range:
|
|
10
|
-
|
|
9
|
+
range: '50...100'
|
|
10
|
+
|
|
11
11
|
status:
|
|
12
12
|
project:
|
|
13
13
|
default:
|
|
@@ -18,7 +18,7 @@ coverage:
|
|
|
18
18
|
- unittests
|
|
19
19
|
paths:
|
|
20
20
|
- src
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
example:
|
|
23
23
|
target: 40%
|
|
24
24
|
threshold: 10%
|
|
@@ -27,7 +27,7 @@ coverage:
|
|
|
27
27
|
- example
|
|
28
28
|
paths:
|
|
29
29
|
- example
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
patch:
|
|
32
32
|
default:
|
|
33
33
|
target: 60%
|
|
@@ -43,7 +43,7 @@ parsers:
|
|
|
43
43
|
macro: no
|
|
44
44
|
|
|
45
45
|
comment:
|
|
46
|
-
layout:
|
|
46
|
+
layout: 'reach,diff,flags,files,footer'
|
|
47
47
|
behavior: default
|
|
48
48
|
require_changes: false
|
|
49
49
|
require_base: false
|
|
@@ -54,21 +54,17 @@ flags:
|
|
|
54
54
|
paths:
|
|
55
55
|
- src/
|
|
56
56
|
carryforward: false
|
|
57
|
-
|
|
58
|
-
example:
|
|
59
|
-
paths:
|
|
60
|
-
- example/
|
|
61
|
-
carryforward: false
|
|
62
57
|
|
|
63
58
|
ignore:
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
59
|
+
- '**/__tests__/**'
|
|
60
|
+
- '**/*.test.ts'
|
|
61
|
+
- '**/*.test.tsx'
|
|
62
|
+
- '**/node_modules/**'
|
|
63
|
+
- 'build/**'
|
|
64
|
+
- 'android/**'
|
|
65
|
+
- 'ios/**'
|
|
66
|
+
- 'docs/**'
|
|
67
|
+
- 'plugin/**'
|
|
68
|
+
- '*.config.js'
|
|
69
|
+
- '*.setup.js'
|
|
70
|
+
- 'example/**'
|
package/ios/ExpoIapModule.swift
CHANGED
|
@@ -271,6 +271,13 @@ public class ExpoIapModule: Module {
|
|
|
271
271
|
return try await OpenIapModule.shared.getReceiptDataIOS() ?? ""
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
// Backward-compatible alias expected by JS layer/tests
|
|
275
|
+
AsyncFunction("getReceiptDataIOS") { () async throws -> String in
|
|
276
|
+
try await ensureConnection()
|
|
277
|
+
logDebug("getReceiptDataIOS called (alias of getReceiptIOS)")
|
|
278
|
+
return try await OpenIapModule.shared.getReceiptDataIOS() ?? ""
|
|
279
|
+
}
|
|
280
|
+
|
|
274
281
|
AsyncFunction("requestReceiptRefreshIOS") { () async throws -> String in
|
|
275
282
|
try await ensureConnection()
|
|
276
283
|
logDebug("requestReceiptRefreshIOS called")
|
|
@@ -307,11 +314,11 @@ public class ExpoIapModule: Module {
|
|
|
307
314
|
return true
|
|
308
315
|
}
|
|
309
316
|
|
|
310
|
-
AsyncFunction("showManageSubscriptionsIOS") { () async throws ->
|
|
317
|
+
AsyncFunction("showManageSubscriptionsIOS") { () async throws -> [[String: Any?]] in
|
|
311
318
|
try await ensureConnection()
|
|
312
319
|
logDebug("showManageSubscriptionsIOS called")
|
|
313
|
-
let
|
|
314
|
-
return
|
|
320
|
+
let purchases = try await OpenIapModule.shared.showManageSubscriptionsIOS()
|
|
321
|
+
return OpenIapSerialization.purchases(purchases)
|
|
315
322
|
}
|
|
316
323
|
|
|
317
324
|
AsyncFunction("deepLinkToSubscriptionsIOS") { () async throws in
|
package/jest.config.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
preset: 'ts-jest',
|
|
3
3
|
testEnvironment: 'node',
|
|
4
|
-
// Disable watchman to avoid sandbox/permission issues in CI and sandboxes
|
|
5
4
|
watchman: false,
|
|
6
5
|
roots: ['<rootDir>/src'],
|
|
7
6
|
testMatch: [
|
|
@@ -34,12 +33,9 @@ module.exports = {
|
|
|
34
33
|
'!src/ExpoIapModule.ts',
|
|
35
34
|
'!src/ExpoIapModule.web.ts',
|
|
36
35
|
],
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
statements: 15,
|
|
43
|
-
},
|
|
44
|
-
},
|
|
36
|
+
coveragePathIgnorePatterns: [
|
|
37
|
+
'<rootDir>/src/useIAP.ts',
|
|
38
|
+
'<rootDir>/src/ExpoIap.types.ts',
|
|
39
|
+
'<rootDir>/src/utils/constants.ts',
|
|
40
|
+
],
|
|
45
41
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-iap",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "In App Purchase module in Expo",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
"lint:eslint": "eslint --fix 'src/**/*.{ts,tsx}' 'plugin/src/**/*.{ts,tsx}'",
|
|
12
12
|
"lint:prettier": "prettier --write \"**/*.{md,js,jsx,ts,tsx}\"",
|
|
13
13
|
"lint:tsc": "tsc -p tsconfig.json --noEmit --skipLibCheck",
|
|
14
|
-
"lint:
|
|
14
|
+
"lint:kt": "sh -c 'command -v ktlint >/dev/null 2>&1 && ktlint --format ./android || { echo \"ktlint not installed; skipping\"; exit 0; }'",
|
|
15
|
+
"lint:ci": "bun run lint:tsc && bun run lint:eslint && bun run lint:prettier && bun run lint:kt",
|
|
15
16
|
"test": "jest",
|
|
16
17
|
"test:coverage": "jest --coverage",
|
|
17
|
-
"prepare": "expo-module prepare",
|
|
18
|
+
"prepare": "expo-module prepare && husky install",
|
|
18
19
|
"expo-module": "expo-module",
|
|
19
20
|
"open:ios": "xed example/ios",
|
|
20
21
|
"open:android": "open -a \"Android Studio\" example/android",
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
"license": "MIT",
|
|
43
44
|
"homepage": "https://github.com/hyochan/expo-iap#readme",
|
|
44
45
|
"devDependencies": {
|
|
46
|
+
"husky": "^9.0.11",
|
|
45
47
|
"@jest/globals": "^30.0.5",
|
|
46
48
|
"@types/jest": "^30.0.0",
|
|
47
49
|
"@types/react": "~19.1.7",
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { ConfigPlugin } from 'expo/config-plugins';
|
|
2
2
|
export interface ExpoIapPluginOptions {
|
|
3
3
|
/** Local development path for OpenIAP library */
|
|
4
|
-
localPath?: string
|
|
4
|
+
localPath?: string | {
|
|
5
|
+
ios?: string;
|
|
6
|
+
android?: string;
|
|
7
|
+
};
|
|
5
8
|
/** Enable local development mode */
|
|
6
9
|
enableLocalDev?: boolean;
|
|
7
10
|
}
|
package/plugin/build/withIAP.js
CHANGED
|
@@ -55,39 +55,43 @@ const addLineToGradle = (content, anchor, lineToAdd, offset = 1) => {
|
|
|
55
55
|
const lines = content.split('\n');
|
|
56
56
|
const index = lines.findIndex((line) => line.match(anchor));
|
|
57
57
|
if (index === -1) {
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
config_plugins_1.WarningAggregator.addWarningAndroid('expo-iap', `dependencies { ... } block not found; skipping injection: ${lineToAdd.trim()}`);
|
|
59
|
+
return content;
|
|
60
60
|
}
|
|
61
61
|
else {
|
|
62
62
|
lines.splice(index + offset, 0, lineToAdd);
|
|
63
63
|
}
|
|
64
64
|
return lines.join('\n');
|
|
65
65
|
};
|
|
66
|
-
const modifyAppBuildGradle = (gradle) => {
|
|
66
|
+
const modifyAppBuildGradle = (gradle, language) => {
|
|
67
67
|
let modified = gradle;
|
|
68
|
-
// Add
|
|
69
|
-
const
|
|
70
|
-
|
|
68
|
+
// Add OpenIAP dependency to app-level build.gradle(.kts)
|
|
69
|
+
const impl = (ga, v) => language === 'kotlin'
|
|
70
|
+
? ` implementation("${ga}:${v}")`
|
|
71
|
+
: ` implementation "${ga}:${v}"`;
|
|
72
|
+
// Pin OpenIAP Google library to 1.1.0
|
|
73
|
+
const openiapDep = impl('io.github.hyochan.openiap:openiap-google', '1.1.0');
|
|
74
|
+
const hasGA = (ga) => new RegExp(String.raw `\b(?:implementation|api)\s*\(?["']${ga}:`, 'm').test(modified);
|
|
71
75
|
let hasAddedDependency = false;
|
|
72
|
-
if (!
|
|
73
|
-
modified = addLineToGradle(modified, /dependencies\s*{/,
|
|
74
|
-
hasAddedDependency = true;
|
|
75
|
-
}
|
|
76
|
-
if (!modified.includes(gmsDep)) {
|
|
77
|
-
modified = addLineToGradle(modified, /dependencies\s*{/, gmsDep, 1);
|
|
76
|
+
if (!hasGA('io.github.hyochan.openiap:openiap-google')) {
|
|
77
|
+
modified = addLineToGradle(modified, /dependencies\s*{/, openiapDep, 0);
|
|
78
78
|
hasAddedDependency = true;
|
|
79
79
|
}
|
|
80
80
|
// Log only once and only if we actually added dependencies
|
|
81
81
|
if (hasAddedDependency)
|
|
82
|
-
logOnce('🛠️ expo-iap: Added
|
|
82
|
+
logOnce('🛠️ expo-iap: Added OpenIAP dependency to build.gradle');
|
|
83
83
|
return modified;
|
|
84
84
|
};
|
|
85
|
-
const withIapAndroid = (config) => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
config
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
const withIapAndroid = (config, props) => {
|
|
86
|
+
const addDeps = props?.addDeps ?? true;
|
|
87
|
+
if (addDeps) {
|
|
88
|
+
config = (0, config_plugins_1.withAppBuildGradle)(config, (config) => {
|
|
89
|
+
// language provided by config-plugins: 'groovy' | 'kotlin'
|
|
90
|
+
const language = config.modResults.language || 'groovy';
|
|
91
|
+
config.modResults.contents = modifyAppBuildGradle(config.modResults.contents, language);
|
|
92
|
+
return config;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
91
95
|
config = (0, config_plugins_1.withAndroidManifest)(config, (config) => {
|
|
92
96
|
const manifest = config.modResults;
|
|
93
97
|
if (!manifest.manifest['uses-permission']) {
|
|
@@ -137,17 +141,27 @@ const withIapIOS = (config) => {
|
|
|
137
141
|
};
|
|
138
142
|
const withIap = (config, options) => {
|
|
139
143
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
144
|
+
const isLocalDev = !!(options?.enableLocalDev || options?.localPath);
|
|
145
|
+
// Apply Android modifications (skip adding deps when linking local module)
|
|
146
|
+
let result = withIapAndroid(config, { addDeps: !isLocalDev });
|
|
142
147
|
// iOS: choose one path to avoid overlap
|
|
143
148
|
if (options?.enableLocalDev || options?.localPath) {
|
|
144
149
|
if (!options?.localPath) {
|
|
145
150
|
config_plugins_1.WarningAggregator.addWarningIOS('expo-iap', 'enableLocalDev is true but no localPath provided. Skipping local OpenIAP integration.');
|
|
146
151
|
}
|
|
147
152
|
else {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
const raw = options.localPath;
|
|
154
|
+
const resolved = typeof raw === 'string'
|
|
155
|
+
? path.resolve(raw)
|
|
156
|
+
: {
|
|
157
|
+
ios: raw.ios ? path.resolve(raw.ios) : undefined,
|
|
158
|
+
android: raw.android ? path.resolve(raw.android) : undefined,
|
|
159
|
+
};
|
|
160
|
+
const preview = typeof resolved === 'string'
|
|
161
|
+
? resolved
|
|
162
|
+
: `ios=${resolved.ios ?? 'auto'}, android=${resolved.android ?? 'auto'}`;
|
|
163
|
+
logOnce(`🔧 [expo-iap] Enabling local OpenIAP: ${preview}`);
|
|
164
|
+
result = (0, withLocalOpenIAP_1.default)(result, { localPath: resolved });
|
|
151
165
|
}
|
|
152
166
|
}
|
|
153
167
|
else {
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { ConfigPlugin } from '
|
|
1
|
+
import { ConfigPlugin } from 'expo/config-plugins';
|
|
2
2
|
/**
|
|
3
3
|
* Plugin to add local OpenIAP pod dependency for development
|
|
4
4
|
* This is only for local development with openiap-apple library
|
|
5
5
|
*/
|
|
6
|
+
type LocalPathOption = string | {
|
|
7
|
+
ios?: string;
|
|
8
|
+
android?: string;
|
|
9
|
+
};
|
|
6
10
|
declare const withLocalOpenIAP: ConfigPlugin<{
|
|
7
|
-
localPath?:
|
|
11
|
+
localPath?: LocalPathOption;
|
|
8
12
|
} | void>;
|
|
9
13
|
export default withLocalOpenIAP;
|
|
@@ -33,51 +33,61 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
const config_plugins_1 = require("
|
|
36
|
+
const config_plugins_1 = require("expo/config-plugins");
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
-
/**
|
|
40
|
-
* Plugin to add local OpenIAP pod dependency for development
|
|
41
|
-
* This is only for local development with openiap-apple library
|
|
42
|
-
*/
|
|
43
39
|
const withLocalOpenIAP = (config, props) => {
|
|
44
|
-
|
|
40
|
+
// Helper to resolve Android module path
|
|
41
|
+
const resolveAndroidModulePath = (p) => {
|
|
42
|
+
if (!p)
|
|
43
|
+
return null;
|
|
44
|
+
// Prefer the module directory if it exists
|
|
45
|
+
const candidates = [
|
|
46
|
+
path.join(p, 'openiap-google'),
|
|
47
|
+
path.join(p, 'openiap'),
|
|
48
|
+
p,
|
|
49
|
+
];
|
|
50
|
+
for (const c of candidates) {
|
|
51
|
+
if (fs.existsSync(path.join(c, 'build.gradle')) ||
|
|
52
|
+
fs.existsSync(path.join(c, 'build.gradle.kts'))) {
|
|
53
|
+
return c;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
};
|
|
58
|
+
// iOS: inject local pod path
|
|
59
|
+
config = (0, config_plugins_1.withDangerousMod)(config, [
|
|
45
60
|
'ios',
|
|
46
61
|
async (config) => {
|
|
47
|
-
const { platformProjectRoot } = config.modRequest;
|
|
62
|
+
const { platformProjectRoot, projectRoot } = config.modRequest;
|
|
63
|
+
const raw = props?.localPath;
|
|
64
|
+
const iosPath = (typeof raw === 'string' ? raw : raw?.ios) ||
|
|
65
|
+
path.resolve(projectRoot, 'openiap-apple');
|
|
48
66
|
const podfilePath = path.join(platformProjectRoot, 'Podfile');
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// Check if local path exists
|
|
53
|
-
if (!fs.existsSync(localOpenIapPath)) {
|
|
54
|
-
console.warn(`⚠️ Local openiap-apple path not found: ${localOpenIapPath}`);
|
|
55
|
-
console.warn(' Skipping local pod injection. Using default pod resolution.');
|
|
67
|
+
if (!fs.existsSync(iosPath)) {
|
|
68
|
+
console.warn(`⚠️ Local openiap-apple path not found: ${iosPath}`);
|
|
69
|
+
console.warn(' Skipping local pod injection.');
|
|
56
70
|
return config;
|
|
57
71
|
}
|
|
58
|
-
// Read Podfile
|
|
59
72
|
if (!fs.existsSync(podfilePath)) {
|
|
60
73
|
console.warn(`⚠️ Podfile not found at ${podfilePath}. Skipping.`);
|
|
61
74
|
return config;
|
|
62
75
|
}
|
|
63
76
|
let podfileContent = fs.readFileSync(podfilePath, 'utf8');
|
|
64
|
-
// Check if already has the local pod reference
|
|
65
77
|
if (podfileContent.includes("pod 'openiap',")) {
|
|
66
78
|
console.log('✅ Local OpenIAP pod already configured');
|
|
67
79
|
return config;
|
|
68
80
|
}
|
|
69
|
-
// Find the target block and inject the local pod
|
|
70
81
|
const targetRegex = /target\s+['"][\w]+['"]\s+do\s*\n\s*use_expo_modules!/;
|
|
71
82
|
if (targetRegex.test(podfileContent)) {
|
|
72
83
|
podfileContent = podfileContent.replace(targetRegex, (match) => {
|
|
73
84
|
return `${match}
|
|
74
85
|
|
|
75
86
|
# Local OpenIAP pod for development (added by expo-iap plugin)
|
|
76
|
-
pod 'openiap', :path => '${
|
|
87
|
+
pod 'openiap', :path => '${iosPath}'`;
|
|
77
88
|
});
|
|
78
|
-
// Write back to Podfile
|
|
79
89
|
fs.writeFileSync(podfilePath, podfileContent);
|
|
80
|
-
console.log(`✅ Added local OpenIAP pod at: ${
|
|
90
|
+
console.log(`✅ Added local OpenIAP pod at: ${iosPath}`);
|
|
81
91
|
}
|
|
82
92
|
else {
|
|
83
93
|
console.warn('⚠️ Could not find target block in Podfile');
|
|
@@ -85,5 +95,150 @@ const withLocalOpenIAP = (config, props) => {
|
|
|
85
95
|
return config;
|
|
86
96
|
},
|
|
87
97
|
]);
|
|
98
|
+
// Android: include local module and add dependency if available
|
|
99
|
+
config = (0, config_plugins_1.withSettingsGradle)(config, (config) => {
|
|
100
|
+
const raw = props?.localPath;
|
|
101
|
+
const projectRoot = config.modRequest.projectRoot;
|
|
102
|
+
const androidInput = typeof raw === 'string' ? undefined : raw?.android;
|
|
103
|
+
const androidModulePath = resolveAndroidModulePath(androidInput) ||
|
|
104
|
+
resolveAndroidModulePath(path.resolve(projectRoot, 'openiap-google')) ||
|
|
105
|
+
null;
|
|
106
|
+
if (!androidModulePath || !fs.existsSync(androidModulePath)) {
|
|
107
|
+
if (androidInput) {
|
|
108
|
+
console.warn(`⚠️ Could not resolve Android OpenIAP module at: ${androidInput}. Skipping local Android linkage.`);
|
|
109
|
+
}
|
|
110
|
+
return config;
|
|
111
|
+
}
|
|
112
|
+
// 1) settings.gradle: include and map projectDir
|
|
113
|
+
const settings = config.modResults;
|
|
114
|
+
const includeLine = "include ':openiap-google'";
|
|
115
|
+
const projectDirLine = `project(':openiap-google').projectDir = new File('${androidModulePath.replace(/\\/g, '/')}')`;
|
|
116
|
+
let contents = settings.contents ?? '';
|
|
117
|
+
// Ensure pluginManagement has plugin mappings required by the included module
|
|
118
|
+
const injectPluginManagement = () => {
|
|
119
|
+
const header = 'pluginManagement {';
|
|
120
|
+
const needsVannik = !/id\s*\(\s*["']com\.vanniktech\.maven\.publish["']/.test(contents);
|
|
121
|
+
const needsKotlinAndroid = !/id\s*\(\s*["']org\.jetbrains\.kotlin\.android["']/.test(contents);
|
|
122
|
+
const needsCompose = !/id\s*\(\s*["']org\.jetbrains\.kotlin\.plugin\.compose["']/.test(contents);
|
|
123
|
+
const needsRepos = !/pluginManagement[\s\S]*?repositories\s*\{/.test(contents);
|
|
124
|
+
const pluginLines = [];
|
|
125
|
+
if (needsVannik)
|
|
126
|
+
pluginLines.push(` id("com.vanniktech.maven.publish") version "0.29.0"`);
|
|
127
|
+
if (needsKotlinAndroid)
|
|
128
|
+
pluginLines.push(` id("org.jetbrains.kotlin.android") version "2.0.21"`);
|
|
129
|
+
if (needsCompose)
|
|
130
|
+
pluginLines.push(` id("org.jetbrains.kotlin.plugin.compose") version "2.0.21"`);
|
|
131
|
+
// If everything already present, skip
|
|
132
|
+
if (pluginLines.length === 0 && !needsRepos)
|
|
133
|
+
return;
|
|
134
|
+
const pluginsBlock = pluginLines.length
|
|
135
|
+
? `plugins {\n${pluginLines.join('\n')}\n}`
|
|
136
|
+
: '';
|
|
137
|
+
const reposBlock = `repositories { gradlePluginPortal(); google(); mavenCentral() }`;
|
|
138
|
+
if (contents.includes(header)) {
|
|
139
|
+
contents = contents.replace(/pluginManagement\s*\{/, (m) => {
|
|
140
|
+
let injection = m + `\n // Added by expo-iap (local openiap-google)\n`;
|
|
141
|
+
if (pluginsBlock)
|
|
142
|
+
injection += ` ${pluginsBlock}\n`;
|
|
143
|
+
if (needsRepos)
|
|
144
|
+
injection += ` ${reposBlock}\n`;
|
|
145
|
+
return injection;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
contents =
|
|
150
|
+
`pluginManagement {\n // Added by expo-iap (local openiap-google)\n` +
|
|
151
|
+
(pluginsBlock ? ` ${pluginsBlock}\n` : '') +
|
|
152
|
+
` ${reposBlock}\n}\n\n${contents}`;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
if (!/com\.vanniktech\.maven\.publish/.test(contents) ||
|
|
156
|
+
!/org\.jetbrains\.kotlin\.android/.test(contents)) {
|
|
157
|
+
injectPluginManagement();
|
|
158
|
+
}
|
|
159
|
+
if (!contents.includes(includeLine))
|
|
160
|
+
contents += `\n${includeLine}\n`;
|
|
161
|
+
if (!contents.includes(projectDirLine))
|
|
162
|
+
contents += `${projectDirLine}\n`;
|
|
163
|
+
settings.contents = contents;
|
|
164
|
+
console.log(`✅ Linked local Android module at: ${androidModulePath}`);
|
|
165
|
+
return config;
|
|
166
|
+
});
|
|
167
|
+
// 2) app/build.gradle: add implementation project(':openiap-google')
|
|
168
|
+
config = (0, config_plugins_1.withAppBuildGradle)(config, (config) => {
|
|
169
|
+
const raw = props?.localPath;
|
|
170
|
+
const projectRoot = config.modRequest.projectRoot;
|
|
171
|
+
const androidInput = typeof raw === 'string' ? undefined : raw?.android;
|
|
172
|
+
const androidModulePath = resolveAndroidModulePath(androidInput) ||
|
|
173
|
+
resolveAndroidModulePath(path.resolve(projectRoot, 'openiap-google')) ||
|
|
174
|
+
null;
|
|
175
|
+
if (!androidModulePath || !fs.existsSync(androidModulePath)) {
|
|
176
|
+
return config;
|
|
177
|
+
}
|
|
178
|
+
const gradle = config.modResults;
|
|
179
|
+
const dependencyLine = ` implementation project(':openiap-google')`;
|
|
180
|
+
// Remove any previously injected external deps that can conflict with the local module
|
|
181
|
+
const removalPatterns = [
|
|
182
|
+
/\n\s*implementation\s+"com\.android\.billingclient:billing-ktx:[^"]+"\s*\n/g,
|
|
183
|
+
/\n\s*implementation\s+"com\.google\.android\.gms:play-services-base:[^"]+"\s*\n/g,
|
|
184
|
+
];
|
|
185
|
+
let contents = gradle.contents;
|
|
186
|
+
let removedAny = false;
|
|
187
|
+
for (const pattern of removalPatterns) {
|
|
188
|
+
if (pattern.test(contents)) {
|
|
189
|
+
contents = contents.replace(pattern, '\n');
|
|
190
|
+
removedAny = true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (removedAny) {
|
|
194
|
+
gradle.contents = contents;
|
|
195
|
+
console.log('🧹 Removed external Play Billing/GMS deps to use local :openiap-google');
|
|
196
|
+
}
|
|
197
|
+
if (!gradle.contents.includes(dependencyLine)) {
|
|
198
|
+
const anchor = /dependencies\s*\{/m;
|
|
199
|
+
if (anchor.test(gradle.contents)) {
|
|
200
|
+
gradle.contents = gradle.contents.replace(anchor, (m) => `${m}\n${dependencyLine}`);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
gradle.contents += `\n\ndependencies {\n${dependencyLine}\n}\n`;
|
|
204
|
+
}
|
|
205
|
+
console.log('🛠️ Added dependency on local :openiap-google project');
|
|
206
|
+
}
|
|
207
|
+
return config;
|
|
208
|
+
});
|
|
209
|
+
// 3) Ensure final cleanup in app/build.gradle after all mods are applied
|
|
210
|
+
config = (0, config_plugins_1.withDangerousMod)(config, [
|
|
211
|
+
'android',
|
|
212
|
+
async (config) => {
|
|
213
|
+
try {
|
|
214
|
+
const { platformProjectRoot } = config.modRequest;
|
|
215
|
+
const appBuildGradle = path.join(platformProjectRoot, 'app', 'build.gradle');
|
|
216
|
+
if (fs.existsSync(appBuildGradle)) {
|
|
217
|
+
let contents = fs.readFileSync(appBuildGradle, 'utf8');
|
|
218
|
+
const patterns = [
|
|
219
|
+
/\n\s*implementation\s+"com\.android\.billingclient:billing-ktx:[^"]+"\s*\n/g,
|
|
220
|
+
/\n\s*implementation\s+"com\.google\.android\.gms:play-services-base:[^"]+"\s*\n/g,
|
|
221
|
+
];
|
|
222
|
+
let changed = false;
|
|
223
|
+
for (const p of patterns) {
|
|
224
|
+
if (p.test(contents)) {
|
|
225
|
+
contents = contents.replace(p, '\n');
|
|
226
|
+
changed = true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (changed) {
|
|
230
|
+
fs.writeFileSync(appBuildGradle, contents);
|
|
231
|
+
console.log('🧹 expo-iap: Cleaned app/build.gradle billing/gms deps for local :openiap-google');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch (e) {
|
|
236
|
+
console.warn('expo-iap: cleanup step failed:', e);
|
|
237
|
+
}
|
|
238
|
+
return config;
|
|
239
|
+
},
|
|
240
|
+
]);
|
|
241
|
+
// (removed) Avoid global root build.gradle mutations; included module should manage its plugins
|
|
242
|
+
return config;
|
|
88
243
|
};
|
|
89
244
|
exports.default = withLocalOpenIAP;
|
package/plugin/src/withIAP.ts
CHANGED
|
@@ -32,49 +32,67 @@ const addLineToGradle = (
|
|
|
32
32
|
const lines = content.split('\n');
|
|
33
33
|
const index = lines.findIndex((line) => line.match(anchor));
|
|
34
34
|
if (index === -1) {
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
WarningAggregator.addWarningAndroid(
|
|
36
|
+
'expo-iap',
|
|
37
|
+
`dependencies { ... } block not found; skipping injection: ${lineToAdd.trim()}`,
|
|
37
38
|
);
|
|
38
|
-
|
|
39
|
+
return content;
|
|
39
40
|
} else {
|
|
40
41
|
lines.splice(index + offset, 0, lineToAdd);
|
|
41
42
|
}
|
|
42
43
|
return lines.join('\n');
|
|
43
44
|
};
|
|
44
45
|
|
|
45
|
-
const modifyAppBuildGradle = (
|
|
46
|
+
const modifyAppBuildGradle = (
|
|
47
|
+
gradle: string,
|
|
48
|
+
language: 'groovy' | 'kotlin',
|
|
49
|
+
): string => {
|
|
46
50
|
let modified = gradle;
|
|
47
51
|
|
|
48
|
-
// Add
|
|
49
|
-
const
|
|
50
|
-
|
|
52
|
+
// Add OpenIAP dependency to app-level build.gradle(.kts)
|
|
53
|
+
const impl = (ga: string, v: string) =>
|
|
54
|
+
language === 'kotlin'
|
|
55
|
+
? ` implementation("${ga}:${v}")`
|
|
56
|
+
: ` implementation "${ga}:${v}"`;
|
|
57
|
+
// Pin OpenIAP Google library to 1.1.0
|
|
58
|
+
const openiapDep = impl('io.github.hyochan.openiap:openiap-google', '1.1.0');
|
|
59
|
+
|
|
60
|
+
const hasGA = (ga: string) =>
|
|
61
|
+
new RegExp(String.raw`\b(?:implementation|api)\s*\(?["']${ga}:`, 'm').test(
|
|
62
|
+
modified,
|
|
63
|
+
);
|
|
51
64
|
|
|
52
65
|
let hasAddedDependency = false;
|
|
53
66
|
|
|
54
|
-
if (!
|
|
55
|
-
modified = addLineToGradle(modified, /dependencies\s*{/,
|
|
56
|
-
hasAddedDependency = true;
|
|
57
|
-
}
|
|
58
|
-
if (!modified.includes(gmsDep)) {
|
|
59
|
-
modified = addLineToGradle(modified, /dependencies\s*{/, gmsDep, 1);
|
|
67
|
+
if (!hasGA('io.github.hyochan.openiap:openiap-google')) {
|
|
68
|
+
modified = addLineToGradle(modified, /dependencies\s*{/, openiapDep, 0);
|
|
60
69
|
hasAddedDependency = true;
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
// Log only once and only if we actually added dependencies
|
|
64
73
|
if (hasAddedDependency)
|
|
65
|
-
logOnce('🛠️ expo-iap: Added
|
|
74
|
+
logOnce('🛠️ expo-iap: Added OpenIAP dependency to build.gradle');
|
|
66
75
|
|
|
67
76
|
return modified;
|
|
68
77
|
};
|
|
69
78
|
|
|
70
|
-
const withIapAndroid: ConfigPlugin = (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
const withIapAndroid: ConfigPlugin<{addDeps?: boolean} | void> = (
|
|
80
|
+
config,
|
|
81
|
+
props,
|
|
82
|
+
) => {
|
|
83
|
+
const addDeps = props?.addDeps ?? true;
|
|
84
|
+
|
|
85
|
+
if (addDeps) {
|
|
86
|
+
config = withAppBuildGradle(config, (config) => {
|
|
87
|
+
// language provided by config-plugins: 'groovy' | 'kotlin'
|
|
88
|
+
const language = (config.modResults as any).language || 'groovy';
|
|
89
|
+
config.modResults.contents = modifyAppBuildGradle(
|
|
90
|
+
config.modResults.contents,
|
|
91
|
+
language,
|
|
92
|
+
);
|
|
93
|
+
return config;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
78
96
|
|
|
79
97
|
config = withAndroidManifest(config, (config) => {
|
|
80
98
|
const manifest = config.modResults;
|
|
@@ -140,7 +158,12 @@ const withIapIOS: ConfigPlugin = (config) => {
|
|
|
140
158
|
|
|
141
159
|
export interface ExpoIapPluginOptions {
|
|
142
160
|
/** Local development path for OpenIAP library */
|
|
143
|
-
localPath?:
|
|
161
|
+
localPath?:
|
|
162
|
+
| string
|
|
163
|
+
| {
|
|
164
|
+
ios?: string;
|
|
165
|
+
android?: string;
|
|
166
|
+
};
|
|
144
167
|
/** Enable local development mode */
|
|
145
168
|
enableLocalDev?: boolean;
|
|
146
169
|
}
|
|
@@ -150,8 +173,9 @@ const withIap: ConfigPlugin<ExpoIapPluginOptions | void> = (
|
|
|
150
173
|
options,
|
|
151
174
|
) => {
|
|
152
175
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
176
|
+
const isLocalDev = !!(options?.enableLocalDev || options?.localPath);
|
|
177
|
+
// Apply Android modifications (skip adding deps when linking local module)
|
|
178
|
+
let result = withIapAndroid(config, {addDeps: !isLocalDev});
|
|
155
179
|
|
|
156
180
|
// iOS: choose one path to avoid overlap
|
|
157
181
|
if (options?.enableLocalDev || options?.localPath) {
|
|
@@ -161,11 +185,23 @@ const withIap: ConfigPlugin<ExpoIapPluginOptions | void> = (
|
|
|
161
185
|
'enableLocalDev is true but no localPath provided. Skipping local OpenIAP integration.',
|
|
162
186
|
);
|
|
163
187
|
} else {
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
188
|
+
const raw = options.localPath;
|
|
189
|
+
const resolved =
|
|
190
|
+
typeof raw === 'string'
|
|
191
|
+
? path.resolve(raw)
|
|
192
|
+
: {
|
|
193
|
+
ios: raw.ios ? path.resolve(raw.ios) : undefined,
|
|
194
|
+
android: raw.android ? path.resolve(raw.android) : undefined,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const preview =
|
|
198
|
+
typeof resolved === 'string'
|
|
199
|
+
? resolved
|
|
200
|
+
: `ios=${resolved.ios ?? 'auto'}, android=${
|
|
201
|
+
resolved.android ?? 'auto'
|
|
202
|
+
}`;
|
|
203
|
+
logOnce(`🔧 [expo-iap] Enabling local OpenIAP: ${preview}`);
|
|
204
|
+
result = withLocalOpenIAP(result, {localPath: resolved});
|
|
169
205
|
}
|
|
170
206
|
} else {
|
|
171
207
|
// Ensure iOS Podfile is set up to resolve public CocoaPods specs
|