@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 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="ghp_...", owner="my-org", repo="my-app")\n' +
25
- ' GitLab: ci_save_config(provider="gitlab", token="glpat-...", owner="my-group", repo="my-app")');
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
- let current = packageDir;
142
- while (isWithin(root, current)) {
143
- const candidate = path.join(current, 'node_modules', 'react-native-iap');
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 project root; workspaces commonly hoist node_modules.
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
- async function reactNativeIapEvidence(root, manifestFile, manifestText, resolveTransitive) {
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
- const declaredVersion = dependencyGroups
169
- .map((group) => group['react-native-iap'])
170
- .find((value) => typeof value === 'string');
171
- if (!declaredVersion)
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 relativeManifest = path.relative(root, manifestFile).replace(/\\/g, '/');
174
- const installedDir = await nearestNodePackage(path.dirname(manifestFile), root);
175
- if (!installedDir) {
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: 'com.android.billingclient:billing',
179
- expression: `react-native-iap ${declaredVersion} is declared but not installed; transitive Billing version is unresolved`,
180
- source: 'unresolved',
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
- const text = await fs.readFile(candidate, 'utf8');
199
- const direct = [...text.matchAll(LITERAL_DEPENDENCY)][0];
200
- if (direct) {
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: relativeManifest,
203
- module: direct[0].slice(0, direct[0].lastIndexOf(':')),
204
- version: direct[1],
205
- expression: `react-native-iap ${installedVersion} native dependency`,
206
- source: 'transitive',
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
- catch {
211
- // Newer react-native-iap versions delegate Billing to the OpenIAP Maven artifact.
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
- file: relativeManifest,
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
- summary = `Billing Library ${detectedVersions.join(', ')} is below the submission minimum major ${policy.minimumSupportedMajor}.`;
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
- summary = 'A Billing dependency was found, but at least one version expression could not be resolved statically.';
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;