iap-preflight: audit your app for subscription-rejection risks before you submit
iap-preflight is a free, MIT-licensed tool that scans your app repository — read-only — for the store-rejection risks that agent-written subscription code most often contains, before you submit to the App Store or Google Play.
node preflight.mjs /path/to/your-app
- Zero dependencies, Node ≥ 18. It never writes to your repo and never touches the network.
- Output: one
PASS/WARN/FAIL/INFOline per finding, plus a summary. - Exit codes:
0= no FAIL ·1= at least one FAIL ·2= usage error. Wire it into CI and let a FAIL break the pipeline before a store submission build. - It discovers billing configs by filename (
paywall|iap|purchases|revenuecat|subscription+.config.{ts,tsx,js,json}), skippingnode_modules, native dirs, and dot-dirs.
Why a scanner at all
Because the failure modes don't announce themselves. Agent-written subscription code tends to be plausible-but-subtly-wrong in exactly the ways above — it type-checks, renders, and demos fine, and the missing restore affordance or the leftover placeholder surfaces only in review or in a revenue report weeks later. Reviewing code you can't fully read is the real bottleneck in AI-assisted development; the point of a scanner is to take the mechanical checks off your eyeballs so the human review that remains is judgment, not grep. Every finding the tool prints cites the store guideline it maps to — you're not trusting our judgment, you're reading the stores' own rules back.
What it checks
| Check | Looks for | Severity |
|---|---|---|
config-placeholders | Unfilled config values ({{…}}, YOUR_KEY, CHANGEME, …) — Apple 2.1(a) requires placeholder content scrubbed before submission | WARN per hit |
api-key | RevenueCat public SDK key values without the expected appl_/goog_/rc_ prefix | WARN |
product-ids | Identifiers containing characters stores reject (anything but letters, digits, _, .), or monthly and yearly pointing at the same product | FAIL |
restore | No restore-purchases affordance anywhere in code/strings while an IAP dependency is present — Apple 3.1.1 requires one | FAIL if absent |
expo-go | A native IAP dependency in an Expo project without expo-dev-client — guaranteed breakage in Expo Go | FAIL |
disclosure | Prints the subscription-disclosure checklist (price + period, what unlocks, auto-renew/cancel/trial terms, EULA + privacy links) with store requirement links | INFO |
The disclosure check is INFO on purpose: a script can't read your paywall screen like a reviewer, so it prints the checklist with links to the Apple App Review Guidelines and Google Play's subscriptions policy instead — the questions are yes/no by hand, the citations are inline.
A real run
Run against an Expo SDK 57 demo app with react-native-purchases 8.x and a config deliberately left on placeholders:
INFO iap-deps detected: react-native-purchases@^8.12.0 · project type: expo
WARN config-placeholders src/paywall/paywall.config.ts: revenueCatApiKey is still a placeholder ("{{REVENUECAT_PUBLIC_API_KEY}}") — scrub placeholder content before submission (Apple 2.1(a))
... 3 more WARN lines (entitlement + product IDs, same cause)
PASS restore found "restorePurchases, emitPaywallEvent, initPaywall } from ..." in src/paywall/PaywallScreen.tsx
PASS expo-go expo-dev-client present — dev build workflow, not Expo Go
INFO disclosure checklist — show the price AND billing period clearly before purchase (Apple 3.1.2(c))
... 3 more disclosure lines
Summary: 2 PASS · 4 WARN · 0 FAIL
inno9 iap-preflight: AUDIT PASS
The WARNs are correct behavior: that demo config is intentionally unfilled. A WARN never blocks; a FAIL should be fixed before submission. AUDIT PASS means no FAIL-level finding — not that review is guaranteed (see honesty section).
Honesty section (what this is not)
This is a heuristics audit for known rejection patterns, not a correctness proof of your billing code. It reads like a checklist because that's what it is. It cannot verify that purchases complete, that your entitlement logic is right, or that a reviewer will approve your app — it catches the mechanical, known-citable failure modes. Sandbox purchase testing on a device remains a human step, and the full rejection checklist covers what no scanner can.
The tool itself is tested — 19 tests over fixture apps built in temp dirs, asserting on audit output and CLI exit codes, no network, no repo writes.
Run it in CI
Because exit code 1 means "at least one FAIL," the CI line is one addition to your store-submission workflow:
- run: node preflight.mjs . && echo "preflight: no FAIL findings"
Fail the submission build, not the review queue.
If your app doesn't survive the audit — placeholders everywhere, no restore affordance, Expo Go workflow — the underlying paths are covered elsewhere: the rejection checklist explains each finding, and the React Native paywall guide walks the whole journey. And if you'd rather start from paywall code that passes these checks on day one, the inno9 paywall module ships pre-tested with its own install-verify receipt.
FAQ
Does it modify my code?
No. Read-only by design: it never writes to your repo and never touches the network. The worst it can do is print an inconvenient finding.
Does passing mean my app won't get rejected?
No. It means none of the known, mechanically-checkable rejection patterns are present. Store review also covers judgment calls — disclosure quality, listing accuracy, guideline areas no scanner can read. Treat AUDIT PASS as "mechanical risks cleared," not "approval guaranteed."
Does it work for Expo and bare React Native?
Both. It detects the project type (Expo vs bare RN vs neither) and applies the same checks; the Expo-Go check only fires for Expo projects with a native IAP dependency and no expo-dev-client.
What's the license, and what does it cost?
MIT, free — it's a standing part of the inno9 shelf's free tier. Run it on every project you ship.