@yoonion/mimi-seed-mcp 0.21.3 → 0.21.5
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/dist/ci/config.js +2 -2
- package/dist/core/checks/billing.d.ts +34 -0
- package/dist/core/checks/billing.js +273 -52
- package/dist/core/checks/gradle-text.d.ts +28 -0
- package/dist/core/checks/gradle-text.js +212 -0
- package/dist/core/checks/lockfile.d.ts +21 -0
- package/dist/core/checks/lockfile.js +185 -0
- package/dist/core/checks/release-doctor-render.js +18 -7
- package/dist/core/checks/release-doctor.d.ts +45 -0
- package/dist/core/checks/release-doctor.js +2470 -261
- package/dist/core/lang.d.ts +7 -1
- package/dist/core/lang.js +29 -5
- package/dist/jenkins/config.js +1 -1
- package/dist/jenkins/credentials.js +2 -1
- package/dist/registers/ci.js +2 -2
- package/package.json +2 -2
package/dist/ci/config.js
CHANGED
|
@@ -21,8 +21,8 @@ export function requireCiConfig() {
|
|
|
21
21
|
throw new Error('CI 설정이 없습니다.\n' +
|
|
22
22
|
'ci_save_config 도구로 먼저 설정해주세요.\n' +
|
|
23
23
|
'예시:\n' +
|
|
24
|
-
' GitHub: ci_save_config(provider="github", token
|
|
25
|
-
' GitLab: ci_save_config(provider="gitlab", token
|
|
24
|
+
' GitHub: ci_save_config(provider="github", token=<GitHub token>, owner="my-org", repo="my-app")\n' +
|
|
25
|
+
' GitLab: ci_save_config(provider="gitlab", token=<GitLab token>, owner="my-group", repo="my-app")');
|
|
26
26
|
}
|
|
27
27
|
return cfg;
|
|
28
28
|
}
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
declare const REACT_NATIVE_IAP_BILLING: ReadonlyArray<{
|
|
2
|
+
from: readonly [number, number, number];
|
|
3
|
+
module: 'com.android.billingclient:billing' | 'com.android.billingclient:billing-ktx';
|
|
4
|
+
version: string;
|
|
5
|
+
/** The app's root `ext.playBillingSdkVersion` replaces the default. */
|
|
6
|
+
overridable?: true;
|
|
7
|
+
}>;
|
|
8
|
+
/** The Billing Library a stable react-native-iap release bundles by default, or null when it is not in the verified table. */
|
|
9
|
+
export declare function reactNativeIapBundledBilling(version: string): (typeof REACT_NATIVE_IAP_BILLING)[number] | null;
|
|
10
|
+
/** The first react-native-iap release whose bundled Billing major is at least `minimumMajor`. */
|
|
11
|
+
export declare function reactNativeIapUpgradeTarget(minimumMajor: number): {
|
|
12
|
+
version: string;
|
|
13
|
+
billing: string;
|
|
14
|
+
} | undefined;
|
|
1
15
|
export type BillingComplianceStatus = 'pass' | 'warning' | 'blocker' | 'unresolved' | 'not_used';
|
|
2
16
|
export interface BillingEvidence {
|
|
3
17
|
file: string;
|
|
@@ -5,6 +19,11 @@ export interface BillingEvidence {
|
|
|
5
19
|
version?: string;
|
|
6
20
|
expression?: string;
|
|
7
21
|
source: 'literal' | 'variable' | 'version_catalog' | 'transitive' | 'unresolved';
|
|
22
|
+
/** The IAP wrapper package that brings Billing in transitively (e.g. react-native-iap), when there is one. */
|
|
23
|
+
wrapper?: {
|
|
24
|
+
name: string;
|
|
25
|
+
version: string;
|
|
26
|
+
};
|
|
8
27
|
}
|
|
9
28
|
export interface BillingComplianceResult {
|
|
10
29
|
projectPath: string;
|
|
@@ -41,6 +60,21 @@ export declare function billingVersionFromPom(pom: string): {
|
|
|
41
60
|
module: string;
|
|
42
61
|
version: string;
|
|
43
62
|
} | null;
|
|
63
|
+
/**
|
|
64
|
+
* An app's root `ext.playBillingSdkVersion`, which react-native-iap 12.5.1–13.x use instead of their default:
|
|
65
|
+
* a literal `version`, or the `expression` when it is set to something Release Doctor cannot evaluate.
|
|
66
|
+
*/
|
|
67
|
+
export interface ReactNativeIapBillingOverride {
|
|
68
|
+
file: string;
|
|
69
|
+
version?: string;
|
|
70
|
+
expression?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* `playBillingSdkVersion` set in a root build script, in Groovy (`ext { x = … }`, `ext.x = …`) or Kotlin DSL
|
|
74
|
+
* (`extra["x"] = …`, `extra.set("x", …)`). A same-file `def/val/var name = "x.y.z"` is followed once.
|
|
75
|
+
*/
|
|
76
|
+
export declare function playBillingSdkOverride(text: string): Omit<ReactNativeIapBillingOverride, 'file'> | undefined;
|
|
44
77
|
export declare function checkBillingCompliance(projectPath: string, now?: Date, options?: {
|
|
45
78
|
resolveTransitive?: BillingTransitiveResolver;
|
|
46
79
|
}): Promise<BillingComplianceResult>;
|
|
80
|
+
export {};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { removeBlocks, stripGradleComments } from './gradle-text.js';
|
|
4
|
+
import { lockedPackageVersion, lockfileDirectories } from './lockfile.js';
|
|
3
5
|
const BILLING_MODULE = /com\.android\.billingclient:billing(?:-ktx)?/;
|
|
4
6
|
const LITERAL_DEPENDENCY = /com\.android\.billingclient:billing(?:-ktx)?:([0-9]+(?:\.[0-9A-Za-z_-]+){0,3})/g;
|
|
5
7
|
const VARIABLE_DEPENDENCY = /com\.android\.billingclient:billing(?:-ktx)?:\$\{?([A-Za-z_][A-Za-z0-9_.-]*)\}?/g;
|
|
@@ -11,6 +13,57 @@ const KNOWN_OPENIAP_BILLING = new Map([
|
|
|
11
13
|
['2.1.0', { module: 'com.android.billingclient:billing-ktx', version: '8.3.0' }],
|
|
12
14
|
['2.4.1', { module: 'com.android.billingclient:billing', version: '9.1.0' }],
|
|
13
15
|
]);
|
|
16
|
+
// react-native-iap release -> the Play Billing Library its Android module builds with by default, so a version
|
|
17
|
+
// pinned in a lockfile can be judged without node_modules. Verified 2026-10-01 against every stable release from
|
|
18
|
+
// 4.0.0 to 16.7.2 on npm (each tarball's android/build.gradle, android/gradle.properties and openiap-versions.json):
|
|
19
|
+
// - 4.0.0–12.5.0 and 14.0.0–14.2.x: a literal com.android.billingclient dependency in android/build.gradle.
|
|
20
|
+
// - 12.5.1–13.x: `billing-ktx:$playBillingSdkVersion`, defaulting to `RNIap_playBillingSdkVersion` in the
|
|
21
|
+
// package's android/gradle.properties; the app can override it with a root `ext.playBillingSdkVersion`.
|
|
22
|
+
// - 14.3.0+: the io.github.hyochan.openiap:openiap-google version it pins (android/build.gradle, later
|
|
23
|
+
// openiap-versions.json), resolved through that artifact's POM on Maven Central.
|
|
24
|
+
// Each row applies from its `from` version up to the next row. A release newer than REACT_NATIVE_IAP_VERIFIED_THROUGH
|
|
25
|
+
// or a prerelease stays unresolved instead of being guessed.
|
|
26
|
+
const REACT_NATIVE_IAP_VERIFIED_THROUGH = [16, 7, 2];
|
|
27
|
+
const REACT_NATIVE_IAP_BILLING = [
|
|
28
|
+
{ from: [4, 0, 0], module: 'com.android.billingclient:billing', version: '2.0.3' },
|
|
29
|
+
{ from: [4, 6, 0], module: 'com.android.billingclient:billing', version: '3.0.0' },
|
|
30
|
+
{ from: [6, 0, 4], module: 'com.android.billingclient:billing', version: '3.0.3' },
|
|
31
|
+
{ from: [7, 0, 0], module: 'com.android.billingclient:billing', version: '4.0.0' },
|
|
32
|
+
{ from: [9, 0, 0], module: 'com.android.billingclient:billing-ktx', version: '5.0.0' },
|
|
33
|
+
{ from: [12, 5, 1], module: 'com.android.billingclient:billing-ktx', version: '5.1.0', overridable: true },
|
|
34
|
+
{ from: [12, 10, 6], module: 'com.android.billingclient:billing-ktx', version: '5.2.1', overridable: true },
|
|
35
|
+
{ from: [12, 11, 0], module: 'com.android.billingclient:billing-ktx', version: '6.0.1', overridable: true },
|
|
36
|
+
{ from: [12, 13, 1], module: 'com.android.billingclient:billing-ktx', version: '6.1.0', overridable: true },
|
|
37
|
+
{ from: [12, 15, 0], module: 'com.android.billingclient:billing-ktx', version: '7.0.0', overridable: true },
|
|
38
|
+
{ from: [14, 0, 0], module: 'com.android.billingclient:billing-ktx', version: '8.0.0' },
|
|
39
|
+
{ from: [14, 6, 0], module: 'com.android.billingclient:billing-ktx', version: '8.1.0' },
|
|
40
|
+
{ from: [14, 6, 3], module: 'com.android.billingclient:billing-ktx', version: '8.2.1' },
|
|
41
|
+
{ from: [14, 6, 4], module: 'com.android.billingclient:billing-ktx', version: '8.3.0' },
|
|
42
|
+
{ from: [15, 4, 0], module: 'com.android.billingclient:billing', version: '9.1.0' },
|
|
43
|
+
];
|
|
44
|
+
function compareTriples(left, right) {
|
|
45
|
+
for (let index = 0; index < 3; index++) {
|
|
46
|
+
const difference = (left[index] ?? 0) - (right[index] ?? 0);
|
|
47
|
+
if (difference !== 0)
|
|
48
|
+
return difference;
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
/** The Billing Library a stable react-native-iap release bundles by default, or null when it is not in the verified table. */
|
|
53
|
+
export function reactNativeIapBundledBilling(version) {
|
|
54
|
+
const match = version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)$/);
|
|
55
|
+
if (!match)
|
|
56
|
+
return null; // ranges, tags, and prereleases are not mapped
|
|
57
|
+
const triple = match.slice(1, 4).map((part) => Number.parseInt(part, 10));
|
|
58
|
+
if (compareTriples(triple, REACT_NATIVE_IAP_VERIFIED_THROUGH) > 0)
|
|
59
|
+
return null;
|
|
60
|
+
return [...REACT_NATIVE_IAP_BILLING].reverse().find((row) => compareTriples(triple, row.from) >= 0) ?? null;
|
|
61
|
+
}
|
|
62
|
+
/** The first react-native-iap release whose bundled Billing major is at least `minimumMajor`. */
|
|
63
|
+
export function reactNativeIapUpgradeTarget(minimumMajor) {
|
|
64
|
+
const row = REACT_NATIVE_IAP_BILLING.find((candidate) => Number.parseInt(candidate.version, 10) >= minimumMajor);
|
|
65
|
+
return row ? { version: row.from.join('.'), billing: row.version } : undefined;
|
|
66
|
+
}
|
|
14
67
|
const BILLING_SUPPORT_SCHEDULE = [
|
|
15
68
|
{ major: 5, submissionDeadline: '2024-08-31', extensionDeadline: '2024-11-01' },
|
|
16
69
|
{ major: 6, submissionDeadline: '2025-08-31', extensionDeadline: '2025-11-01' },
|
|
@@ -137,24 +190,62 @@ export function billingVersionFromPom(pom) {
|
|
|
137
190
|
}
|
|
138
191
|
return null;
|
|
139
192
|
}
|
|
140
|
-
async function nearestNodePackage(packageDir, root) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const candidate = path.join(
|
|
193
|
+
async function nearestNodePackage(packageDir, root, dependencyName = 'react-native-iap') {
|
|
194
|
+
// Workspaces hoist node_modules toward the root — past the scan root when --path points into a monorepo.
|
|
195
|
+
for (const directory of await lockfileDirectories(packageDir, root)) {
|
|
196
|
+
const candidate = path.join(directory, 'node_modules', dependencyName);
|
|
144
197
|
try {
|
|
145
198
|
if ((await fs.stat(candidate)).isDirectory())
|
|
146
199
|
return candidate;
|
|
147
200
|
}
|
|
148
201
|
catch {
|
|
149
|
-
// Keep walking toward the
|
|
202
|
+
// Keep walking toward the repository root.
|
|
150
203
|
}
|
|
151
|
-
if (current === root)
|
|
152
|
-
break;
|
|
153
|
-
current = path.dirname(current);
|
|
154
204
|
}
|
|
155
205
|
return null;
|
|
156
206
|
}
|
|
157
|
-
|
|
207
|
+
function displayPath(root, file) {
|
|
208
|
+
return path.relative(root, file).replace(/\\/g, '/');
|
|
209
|
+
}
|
|
210
|
+
const LITERAL_VERSION = /^["']([0-9]+(?:\.[0-9A-Za-z_-]+){0,3})["']$/;
|
|
211
|
+
/**
|
|
212
|
+
* `playBillingSdkVersion` set in a root build script, in Groovy (`ext { x = … }`, `ext.x = …`) or Kotlin DSL
|
|
213
|
+
* (`extra["x"] = …`, `extra.set("x", …)`). A same-file `def/val/var name = "x.y.z"` is followed once.
|
|
214
|
+
*/
|
|
215
|
+
export function playBillingSdkOverride(text) {
|
|
216
|
+
// `subprojects { … }` sets extras on the subprojects, not on rootProject, so it never reaches react-native-iap.
|
|
217
|
+
// Braces are matched on lexed text, so `println("{")` inside the block cannot swallow the root `ext { … }`.
|
|
218
|
+
const code = removeBlocks(stripGradleComments(text), /\bsubprojects\s*\{/);
|
|
219
|
+
const forms = [
|
|
220
|
+
// Not a local variable that happens to share the name (`def playBillingSdkVersion = …`).
|
|
221
|
+
/(?<!\b(?:def|val|var)\s+)\bplayBillingSdkVersion\s*=(?!=)\s*([^\n;}]+)/,
|
|
222
|
+
/\bextra\s*\[\s*["']playBillingSdkVersion["']\s*\]\s*=(?!=)\s*([^\n;}]+)/,
|
|
223
|
+
/\b(?:extra|ext)\.set\(\s*["']playBillingSdkVersion["']\s*,\s*([^\n;)]+)/,
|
|
224
|
+
];
|
|
225
|
+
for (const form of forms) {
|
|
226
|
+
const match = form.exec(code);
|
|
227
|
+
if (!match)
|
|
228
|
+
continue;
|
|
229
|
+
const value = match[1].trim();
|
|
230
|
+
const literal = value.match(LITERAL_VERSION)?.[1];
|
|
231
|
+
if (literal)
|
|
232
|
+
return { version: literal };
|
|
233
|
+
const identifier = value.match(/^([A-Za-z_]\w*)$/)?.[1];
|
|
234
|
+
// Follow a local only when it is defined exactly once in the file (several scopes could define it differently).
|
|
235
|
+
const definitions = identifier
|
|
236
|
+
? code.match(new RegExp(`\\b(?:def|val|var)\\s+${identifier}\\b`, 'g'))?.length ?? 0
|
|
237
|
+
: 0;
|
|
238
|
+
if (identifier && definitions === 1) {
|
|
239
|
+
const local = new RegExp(`\\b(?:def|val|var)\\s+${identifier}\\s*(?::\\s*String\\s*)?=\\s*(["'][^"'\\n]*["'])`).exec(code)?.[1];
|
|
240
|
+
const resolved = local?.match(LITERAL_VERSION)?.[1];
|
|
241
|
+
if (resolved)
|
|
242
|
+
return { version: resolved };
|
|
243
|
+
}
|
|
244
|
+
return { expression: `playBillingSdkVersion = ${value}` };
|
|
245
|
+
}
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
async function reactNativeIapEvidence(root, manifestFile, manifestText, resolveTransitive, overrides) {
|
|
158
249
|
let manifest;
|
|
159
250
|
try {
|
|
160
251
|
manifest = JSON.parse(manifestText);
|
|
@@ -165,50 +256,165 @@ async function reactNativeIapEvidence(root, manifestFile, manifestText, resolveT
|
|
|
165
256
|
const dependencyGroups = ['dependencies', 'devDependencies', 'optionalDependencies']
|
|
166
257
|
.map((key) => manifest[key])
|
|
167
258
|
.filter((value) => Boolean(value) && typeof value === 'object');
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
.
|
|
171
|
-
|
|
259
|
+
// `"react-native-iap": "…"`, or react-native-iap installed under another name (`"iap": "npm:react-native-iap@^12"`).
|
|
260
|
+
const entries = dependencyGroups.flatMap((group) => Object.entries(group))
|
|
261
|
+
.filter((entry) => typeof entry[1] === 'string');
|
|
262
|
+
const entry = entries.find(([name]) => name === 'react-native-iap')
|
|
263
|
+
?? entries.find(([, spec]) => /^npm:react-native-iap@/.test(spec))
|
|
264
|
+
// A fork installed under another name (`"iap": "npm:@acme/react-native-iap@…"`): reported as unresolved below.
|
|
265
|
+
?? entries.find(([, spec]) => /^npm:(?:@[^/@]+\/)?react-native-iap(?:-[\w.-]+)?@/.test(spec));
|
|
266
|
+
if (!entry)
|
|
172
267
|
return null;
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
268
|
+
const [dependencyName, declaredVersion] = entry;
|
|
269
|
+
// The version range react-native-iap itself is declared with, and whether the name points at a fork.
|
|
270
|
+
const declaredRange = declaredVersion.replace(/^npm:react-native-iap@/, '');
|
|
271
|
+
const forkTarget = declaredVersion.match(/^npm:((?:@[^/@]+\/)?[^@]+)/)?.[1];
|
|
272
|
+
const declaredFork = forkTarget !== undefined && forkTarget !== 'react-native-iap';
|
|
273
|
+
const relativeManifest = displayPath(root, manifestFile);
|
|
274
|
+
// This app's own root build script (inside the package that declares react-native-iap).
|
|
275
|
+
const override = overrides
|
|
276
|
+
.filter((candidate) => isWithin(path.dirname(manifestFile), path.join(root, candidate.file)))
|
|
277
|
+
.sort((left, right) => left.file.length - right.file.length)[0];
|
|
278
|
+
const unresolved = (expression, wrapperVersion = declaredVersion) => ({
|
|
279
|
+
file: relativeManifest,
|
|
280
|
+
module: 'com.android.billingclient:billing',
|
|
281
|
+
expression,
|
|
282
|
+
source: 'unresolved',
|
|
283
|
+
wrapper: { name: 'react-native-iap', version: wrapperVersion },
|
|
284
|
+
});
|
|
285
|
+
// The verified react-native-iap -> Billing table (see REACT_NATIVE_IAP_BILLING).
|
|
286
|
+
const fromTable = (version, origin) => {
|
|
287
|
+
const row = reactNativeIapBundledBilling(version);
|
|
288
|
+
if (!row)
|
|
289
|
+
return null;
|
|
290
|
+
const wrapper = { name: 'react-native-iap', version };
|
|
291
|
+
if (row.overridable && override && !override.version) {
|
|
292
|
+
return {
|
|
293
|
+
file: override.file,
|
|
294
|
+
module: row.module,
|
|
295
|
+
expression: `react-native-iap ${version} from ${origin}, with ${override.expression} replacing its default ${row.version}; Release Doctor cannot evaluate that expression`,
|
|
296
|
+
source: 'unresolved',
|
|
297
|
+
wrapper,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (row.overridable && override) {
|
|
301
|
+
return {
|
|
302
|
+
file: override.file,
|
|
303
|
+
module: row.module,
|
|
304
|
+
version: override.version,
|
|
305
|
+
expression: `react-native-iap ${version} from ${origin}, with ext.playBillingSdkVersion = ${override.version} replacing its default ${row.version}`,
|
|
306
|
+
source: 'variable',
|
|
307
|
+
wrapper,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
176
310
|
return {
|
|
177
311
|
file: relativeManifest,
|
|
178
|
-
module:
|
|
179
|
-
|
|
180
|
-
|
|
312
|
+
module: row.module,
|
|
313
|
+
version: row.version,
|
|
314
|
+
expression: `react-native-iap ${version} from ${origin} bundles ${row.module}:${row.version}${row.overridable ? ' by default' : ''}`,
|
|
315
|
+
source: 'transitive',
|
|
316
|
+
wrapper,
|
|
181
317
|
};
|
|
318
|
+
};
|
|
319
|
+
const packageDir = path.dirname(manifestFile);
|
|
320
|
+
const installedDir = await nearestNodePackage(packageDir, root, dependencyName);
|
|
321
|
+
if (!installedDir) {
|
|
322
|
+
// Not installed (a fresh clone, or CI before `install`): the lockfile still says which release ships.
|
|
323
|
+
for (const directory of await lockfileDirectories(packageDir, root)) {
|
|
324
|
+
const locked = await lockedPackageVersion(dependencyName, packageDir, directory, declaredVersion);
|
|
325
|
+
if (!locked)
|
|
326
|
+
continue;
|
|
327
|
+
const origin = displayPath(root, path.join(directory, locked.lockfile));
|
|
328
|
+
if (locked.aliasOf && locked.aliasOf !== 'react-native-iap') {
|
|
329
|
+
// `"react-native-iap": "npm:@fork/…"`: the version is the fork's, so the upstream table does not apply.
|
|
330
|
+
return unresolved(`react-native-iap is an alias of ${locked.aliasOf}${locked.version ? ` ${locked.version}` : ''} (${origin}); the react-native-iap -> Play Billing table does not apply to a fork, so install dependencies so its Gradle file can be read`, locked.version || declaredVersion);
|
|
331
|
+
}
|
|
332
|
+
return fromTable(locked.version, origin) ?? unresolved(`react-native-iap ${locked.version} from ${origin} is not in the embedded react-native-iap -> Play Billing table; install dependencies so its Gradle file can be read`, locked.version);
|
|
333
|
+
}
|
|
334
|
+
if (declaredFork) {
|
|
335
|
+
return unresolved(`react-native-iap is declared as an alias (${declaredVersion}) and is neither installed nor pinned in a lockfile; transitive Billing version is unresolved`);
|
|
336
|
+
}
|
|
337
|
+
return fromTable(declaredRange, 'declared') ?? unresolved(`react-native-iap ${declaredVersion} is declared but neither installed nor pinned in a lockfile; transitive Billing version is unresolved`);
|
|
182
338
|
}
|
|
183
339
|
let installedVersion = declaredVersion;
|
|
340
|
+
// An `npm:` alias installs a fork under this name; its Gradle files are read as usual, but the upstream
|
|
341
|
+
// react-native-iap table must not be applied to the fork's version.
|
|
342
|
+
let aliased = declaredFork;
|
|
184
343
|
try {
|
|
185
344
|
const installedManifest = JSON.parse(await fs.readFile(path.join(installedDir, 'package.json'), 'utf8'));
|
|
186
345
|
if (typeof installedManifest.version === 'string')
|
|
187
346
|
installedVersion = installedManifest.version;
|
|
347
|
+
if (typeof installedManifest.name === 'string')
|
|
348
|
+
aliased = installedManifest.name !== 'react-native-iap';
|
|
188
349
|
}
|
|
189
350
|
catch {
|
|
190
351
|
// The declaration still provides useful evidence when package metadata is unavailable.
|
|
191
352
|
}
|
|
353
|
+
const fromInstalledTable = (version) => (aliased ? null : fromTable(version, 'installed'));
|
|
354
|
+
const installedWrapper = { name: 'react-native-iap', version: installedVersion };
|
|
192
355
|
const directCandidates = [
|
|
193
356
|
path.join(installedDir, 'android', 'build.gradle'),
|
|
194
357
|
path.join(installedDir, 'android', 'build.gradle.kts'),
|
|
195
358
|
];
|
|
196
359
|
for (const candidate of directCandidates) {
|
|
360
|
+
let text;
|
|
197
361
|
try {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
362
|
+
text = await fs.readFile(candidate, 'utf8');
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
continue; // Newer react-native-iap versions delegate Billing to the OpenIAP Maven artifact.
|
|
366
|
+
}
|
|
367
|
+
const direct = [...text.matchAll(LITERAL_DEPENDENCY)][0];
|
|
368
|
+
if (direct) {
|
|
369
|
+
return {
|
|
370
|
+
file: relativeManifest,
|
|
371
|
+
module: direct[0].slice(0, direct[0].lastIndexOf(':')),
|
|
372
|
+
version: direct[1],
|
|
373
|
+
expression: `react-native-iap ${installedVersion} native dependency`,
|
|
374
|
+
source: 'transitive',
|
|
375
|
+
wrapper: installedWrapper,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
// 12.5.1–13.x: `billing-ktx:$playBillingSdkVersion`, defaulting to the package's own gradle.properties.
|
|
379
|
+
const variable = [...text.matchAll(VARIABLE_DEPENDENCY)][0];
|
|
380
|
+
if (variable?.[1] === 'playBillingSdkVersion') {
|
|
381
|
+
const module = variable[0].slice(0, variable[0].lastIndexOf(':'));
|
|
382
|
+
if (override && !override.version) {
|
|
201
383
|
return {
|
|
202
|
-
file:
|
|
203
|
-
module
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
384
|
+
file: override.file,
|
|
385
|
+
module,
|
|
386
|
+
expression: `react-native-iap ${installedVersion} with ${override.expression}; Release Doctor cannot evaluate that expression`,
|
|
387
|
+
source: 'unresolved',
|
|
388
|
+
wrapper: installedWrapper,
|
|
207
389
|
};
|
|
208
390
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
391
|
+
if (override) {
|
|
392
|
+
return {
|
|
393
|
+
file: override.file,
|
|
394
|
+
module,
|
|
395
|
+
version: override.version,
|
|
396
|
+
expression: `react-native-iap ${installedVersion} with ext.playBillingSdkVersion = ${override.version}`,
|
|
397
|
+
source: 'variable',
|
|
398
|
+
wrapper: installedWrapper,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
const properties = await fs.readFile(path.join(installedDir, 'android', 'gradle.properties'), 'utf8');
|
|
403
|
+
const version = properties.match(/^\s*RNIap_playBillingSdkVersion\s*=\s*([0-9]+(?:\.[0-9A-Za-z_-]+){0,3})\s*$/m)?.[1];
|
|
404
|
+
if (version) {
|
|
405
|
+
return {
|
|
406
|
+
file: relativeManifest,
|
|
407
|
+
module,
|
|
408
|
+
version,
|
|
409
|
+
expression: `react-native-iap ${installedVersion} default RNIap_playBillingSdkVersion`,
|
|
410
|
+
source: 'transitive',
|
|
411
|
+
wrapper: installedWrapper,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// Fall back to the verified table below.
|
|
417
|
+
}
|
|
212
418
|
}
|
|
213
419
|
}
|
|
214
420
|
let openIapVersion;
|
|
@@ -218,15 +424,11 @@ async function reactNativeIapEvidence(root, manifestFile, manifestText, resolveT
|
|
|
218
424
|
openIapVersion = versions.google;
|
|
219
425
|
}
|
|
220
426
|
catch {
|
|
221
|
-
// Older releases may not use OpenIAP; fall through to an unresolved, safe result.
|
|
427
|
+
// Older releases may not use OpenIAP; fall through to the verified table, then to an unresolved, safe result.
|
|
222
428
|
}
|
|
223
429
|
if (!openIapVersion || !/^[0-9A-Za-z][0-9A-Za-z._-]*$/.test(openIapVersion)) {
|
|
224
|
-
return
|
|
225
|
-
|
|
226
|
-
module: 'com.android.billingclient:billing',
|
|
227
|
-
expression: `react-native-iap ${installedVersion} detected; transitive Billing version is unresolved`,
|
|
228
|
-
source: 'unresolved',
|
|
229
|
-
};
|
|
430
|
+
return fromInstalledTable(installedVersion)
|
|
431
|
+
?? unresolved(`react-native-iap ${installedVersion} detected; transitive Billing version is unresolved`, installedVersion);
|
|
230
432
|
}
|
|
231
433
|
const coordinate = `io.github.hyochan.openiap:openiap-google:${openIapVersion}`;
|
|
232
434
|
const known = KNOWN_OPENIAP_BILLING.get(openIapVersion);
|
|
@@ -238,14 +440,10 @@ async function reactNativeIapEvidence(root, manifestFile, manifestText, resolveT
|
|
|
238
440
|
version: known.version,
|
|
239
441
|
expression: `react-native-iap ${installedVersion} -> ${coordinate} (embedded Maven metadata)`,
|
|
240
442
|
source: 'transitive',
|
|
443
|
+
wrapper: installedWrapper,
|
|
241
444
|
};
|
|
242
445
|
}
|
|
243
|
-
return {
|
|
244
|
-
file: relativeManifest,
|
|
245
|
-
module: 'com.android.billingclient:billing',
|
|
246
|
-
expression: `react-native-iap ${installedVersion} -> ${coordinate}; transitive lookup unavailable in repository-only mode`,
|
|
247
|
-
source: 'unresolved',
|
|
248
|
-
};
|
|
446
|
+
return fromInstalledTable(installedVersion) ?? unresolved(`react-native-iap ${installedVersion} -> ${coordinate}; transitive lookup unavailable in repository-only mode`, installedVersion);
|
|
249
447
|
}
|
|
250
448
|
try {
|
|
251
449
|
const resolved = await resolveTransitive(openIapVersion);
|
|
@@ -256,6 +454,7 @@ async function reactNativeIapEvidence(root, manifestFile, manifestText, resolveT
|
|
|
256
454
|
version: resolved.version,
|
|
257
455
|
expression: `react-native-iap ${installedVersion} -> ${coordinate}`,
|
|
258
456
|
source: 'transitive',
|
|
457
|
+
wrapper: installedWrapper,
|
|
259
458
|
};
|
|
260
459
|
}
|
|
261
460
|
}
|
|
@@ -269,14 +468,10 @@ async function reactNativeIapEvidence(root, manifestFile, manifestText, resolveT
|
|
|
269
468
|
version: known.version,
|
|
270
469
|
expression: `react-native-iap ${installedVersion} -> ${coordinate} (embedded Maven metadata fallback)`,
|
|
271
470
|
source: 'transitive',
|
|
471
|
+
wrapper: installedWrapper,
|
|
272
472
|
};
|
|
273
473
|
}
|
|
274
|
-
return {
|
|
275
|
-
file: relativeManifest,
|
|
276
|
-
module: 'com.android.billingclient:billing',
|
|
277
|
-
expression: `react-native-iap ${installedVersion} -> ${coordinate}; Maven Billing version lookup failed`,
|
|
278
|
-
source: 'unresolved',
|
|
279
|
-
};
|
|
474
|
+
return unresolved(`react-native-iap ${installedVersion} -> ${coordinate}; Maven Billing version lookup failed`, installedVersion);
|
|
280
475
|
}
|
|
281
476
|
function policyAt(now) {
|
|
282
477
|
const deadlineEnd = (date) => new Date(`${date}T23:59:59.999Z`);
|
|
@@ -338,11 +533,28 @@ export async function checkBillingCompliance(projectPath, now = new Date(), opti
|
|
|
338
533
|
.sort(([left], [right]) => path.dirname(right).length - path.dirname(left).length)
|
|
339
534
|
.map(([, variables]) => variables.get(key))
|
|
340
535
|
.find((value) => value !== undefined);
|
|
536
|
+
// react-native-iap 12.5.1–13.x read `rootProject.ext.playBillingSdkVersion` before their own default, so only a
|
|
537
|
+
// Gradle root build script (one next to settings.gradle) counts, and commented-out lines do not.
|
|
538
|
+
// (`RNIap_playBillingSdkVersion` in the app's root gradle.properties does not reach the library: Gradle lets the
|
|
539
|
+
// library project's own android/gradle.properties override the root project's values.)
|
|
540
|
+
const iapOverrides = [];
|
|
541
|
+
for (const [file, text] of texts) {
|
|
542
|
+
if (!/build\.gradle(?:\.kts)?$/.test(file))
|
|
543
|
+
continue;
|
|
544
|
+
const dir = path.dirname(file);
|
|
545
|
+
const isRootScript = (await Promise.all(['settings.gradle', 'settings.gradle.kts']
|
|
546
|
+
.map((name) => fs.stat(path.join(dir, name)).then(() => true, () => false)))).some(Boolean);
|
|
547
|
+
if (!isRootScript)
|
|
548
|
+
continue;
|
|
549
|
+
const override = playBillingSdkOverride(text);
|
|
550
|
+
if (override)
|
|
551
|
+
iapOverrides.push({ file: displayPath(root, file), ...override });
|
|
552
|
+
}
|
|
341
553
|
const evidence = [];
|
|
342
554
|
for (const [file, text] of texts) {
|
|
343
555
|
if (path.basename(file) !== 'package.json')
|
|
344
556
|
continue;
|
|
345
|
-
const transitive = await reactNativeIapEvidence(root, file, text, options.resolveTransitive);
|
|
557
|
+
const transitive = await reactNativeIapEvidence(root, file, text, options.resolveTransitive, iapOverrides);
|
|
346
558
|
if (transitive && !evidence.some((row) => row.expression === transitive.expression))
|
|
347
559
|
evidence.push(transitive);
|
|
348
560
|
}
|
|
@@ -435,8 +647,16 @@ export async function checkBillingCompliance(projectPath, now = new Date(), opti
|
|
|
435
647
|
}
|
|
436
648
|
else if (majors.some((major) => major < policy.minimumSupportedMajor)) {
|
|
437
649
|
status = 'blocker';
|
|
438
|
-
|
|
650
|
+
// Only the versions that fail: a compliant app next to a failing one must not read as failing too.
|
|
651
|
+
const failing = detectedVersions.filter((version) => (majorOf(version) ?? Infinity) < policy.minimumSupportedMajor);
|
|
652
|
+
summary = `Billing Library ${failing.join(', ')} is below the submission minimum major ${policy.minimumSupportedMajor}.`;
|
|
439
653
|
actions.push(`Upgrade to a supported Billing Library before submitting a new app or update.`);
|
|
654
|
+
const minimum = policy.minimumSupportedMajor;
|
|
655
|
+
const iapTarget = reactNativeIapUpgradeTarget(minimum);
|
|
656
|
+
if (iapTarget && evidence.some((row) => row.wrapper?.name === 'react-native-iap'
|
|
657
|
+
&& row.version && (majorOf(row.version) ?? minimum) < minimum)) {
|
|
658
|
+
actions.push(`react-native-iap brings Billing in transitively: upgrade it to ${iapTarget.version} or later (Billing ${iapTarget.billing}); it is a major-version upgrade, so follow its migration guide.`);
|
|
659
|
+
}
|
|
440
660
|
for (const major of [...new Set(majors.filter((value) => value < policy.minimumSupportedMajor))].sort()) {
|
|
441
661
|
const schedule = scheduleForMajor(major);
|
|
442
662
|
actions.push(schedule
|
|
@@ -446,7 +666,8 @@ export async function checkBillingCompliance(projectPath, now = new Date(), opti
|
|
|
446
666
|
}
|
|
447
667
|
else if (unresolved || majors.length === 0) {
|
|
448
668
|
status = 'unresolved';
|
|
449
|
-
|
|
669
|
+
const reasons = evidence.filter((row) => !row.version).map((row) => `${row.file}: ${row.expression ?? row.module}`);
|
|
670
|
+
summary = `A Billing dependency was found, but at least one version expression could not be resolved statically (${reasons.slice(0, 3).join('; ')}${reasons.length > 3 ? `; +${reasons.length - 3}` : ''}).`;
|
|
450
671
|
actions.push('Resolve the reported Gradle/version catalog expression or install the declared IAP package, then run the check again.');
|
|
451
672
|
}
|
|
452
673
|
else if (majors.some((major) => major === policy.minimumSupportedMajor)) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gradle source with comments removed and strings kept verbatim, so `/*` in a glob such as `pickFirst '**\/*.so'`
|
|
3
|
+
* or `//` in a URL is text, not a comment. Block comments become the newlines they spanned, so line-based matching
|
|
4
|
+
* keeps working.
|
|
5
|
+
*/
|
|
6
|
+
export declare function stripGradleComments(text: string): string;
|
|
7
|
+
/** Gradle source with comments blanked to spaces (same length and line breaks, so indexes still line up). */
|
|
8
|
+
export declare function blankComments(text: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Gradle source with comments and string contents blanked (same length, line breaks kept): only code is left to
|
|
11
|
+
* match tokens against, so `"targetSdk 33"` in a string or a comment is never read as an assignment. Each string
|
|
12
|
+
* literal keeps its first and last character so `x = "…"` still shows a value is there.
|
|
13
|
+
*/
|
|
14
|
+
export declare function maskStrings(text: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Like `maskStrings`, but string literals for which `keep(literal)` is true stay verbatim (`literal` includes its
|
|
17
|
+
* quotes). Comments are blanked. Same length and line breaks as `text`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function keepStrings(text: string, keep: (literal: string) => boolean): string;
|
|
20
|
+
/**
|
|
21
|
+
* Index of the `}` that closes the `{` at `open`, counting braces only in code (not inside strings or comments);
|
|
22
|
+
* -1 when it is never closed.
|
|
23
|
+
*/
|
|
24
|
+
export declare function closingBrace(text: string, open: number): number;
|
|
25
|
+
/** `text` without every block opened by `opener` (a regex ending at `{`), braces matched with `closingBrace`. */
|
|
26
|
+
export declare function removeBlocks(text: string, opener: RegExp): string;
|
|
27
|
+
/** The contents of the first block opened by `opener` (a regex ending at `{`), or undefined. */
|
|
28
|
+
export declare function blockContents(text: string, opener: RegExp): string | undefined;
|