Skip to main content
Glama

store-review-check

License: MIT Rules: 40 MCP server

Find App Store and Google Play rejection risks in an iOS, Android, Flutter, React Native or Expo project before you submit it. It runs as a CLI and as an MCP server, so you can use it from a terminal, from CI, or from an AI coding assistant.

npx store-review-check ./my-app

It reads Info.plist, entitlements, PrivacyInfo.xcprivacy, StoreKit configuration files, AndroidManifest.xml, Gradle files and version catalogs, pubspec.yaml, package.json and Expo app.json, plus your store listing text if you provide it. Each finding names the guideline section, links to the official Apple or Google page and says how to fix the problem.

The rule set leans toward problems dating and social apps run into, such as user-generated content without report and block tools, missing account deletion, social login without Sign in with Apple and subscription terms. Most rules apply to any app.

There is no AI in the checks. The same project gives the same report every time, and nothing leaves your machine.

Contents

Related MCP server: VibeGuard MCP Server

What it checks

  • iOS permissions: if the code or a library uses the camera, photos, location, contacts, microphone, tracking and so on, the matching NS…UsageDescription key has to exist and say why. Empty, placeholder and Expo default strings are flagged too.

  • iOS privacy manifest: whether the app has PrivacyInfo.xcprivacy, and whether your own Swift and Objective-C code uses required reason APIs (UserDefaults, file timestamps, disk space, boot time, active keyboards) without declaring them.

  • Accounts: account creation without in-app deletion, the Google Play web deletion link, third-party login without Sign in with Apple, and a missing demo account for reviewers.

  • User-generated content: report, block, content filtering, published contact details and terms acceptance, plus an age check for dating apps.

  • Payments: subscriptions shorter than seven days, missing restore purchases, missing terms and price details, third-party payment SDKs for digital goods, and an outdated Play Billing Library.

  • Android platform: targetSdk against the current Google Play requirement, restricted permissions (SMS, call log, all-files access and others), photo and video permissions, background location, and sensitive permissions with no disclosure screen.

  • Store listing: length limits, price and ranking claims, other apps' names in keywords, mentions of other platforms, emoji and ALL CAPS in Play titles, and wording tied to hookup or anonymous chat apps.

Sample output

Output of a run against the risky Flutter dating app in test/fixtures/flutter-dating-risky, errors only:

$ npx store-review-check test/fixtures/flutter-dating-risky --min-severity error
store-review-check 0.1.0 · rules 2026.10.15
Project: test/fixtures/flutter-dating-risky
Platforms: ios, android (flutter)
Listing data: store-review.yaml
Dating app: yes (app.category is "dating")
User content: yes (dating apps show profiles, photos and messages)
Accounts: yes (dependency firebase_auth (pubspec.yaml), lib/main.dart)

ERRORS (9)
  ✖ android.billing-library  Google Play Billing Library version is no longer accepted
      com.android.billingclient:billing:6.2.1 in android/app/build.gradle. Play requires Billing Library 8 or later since 2026-08-31 (extension until 2026-11-01).
      Files: android/app/build.gradle
      Fix: Update com.android.billingclient:billing (or billing-ktx) to 8.x or newer. If billing comes from a plugin (in_app_purchase, RevenueCat, react-native-iap), update the plugin to a release built on PBL 8+.
      See: Google Play · Google Play Billing Library version deprecation · https://developer.android.com/google/play/billing/deprecation-faq
  ✖ android.target-sdk  targetSdk is below the current Google Play requirement
      targetSdk is 34 (android/app/build.gradle). Google Play requires 36 or higher for new apps and updates since 2026-08-31 (extension requests until 2026-11-01). At 34 an already published app is also hidden from new users on newer Android versions. Wear OS and Android Automotive OS apps need API 35; Android TV and Android XR apps need API 34.
      Files: android/app/build.gradle
      Fix: Raise targetSdk (and compileSdk) to 36 in the app module's build.gradle, then test the Android 15 and 16 behavior changes.
      See: Google Play · Meet Google Play's target API level requirement · https://developer.android.com/google/play/requirements/target-sdk
           Google Play · Target API level requirements for Google Play apps · https://support.google.com/googleplay/android-developer/answer/11926878
  ✖ ios.privacy-manifest-required-reason  Required reason API used but not declared in the privacy manifest
      User defaults APIs are used in ios/Runner/AppDelegate.swift but NSPrivacyAccessedAPICategoryUserDefaults is not declared in the app (no PrivacyInfo.xcprivacy exists yet).
      Files: ios/Runner/AppDelegate.swift
      Fix: Add a dictionary to NSPrivacyAccessedAPITypes with NSPrivacyAccessedAPIType set to the category and NSPrivacyAccessedAPITypeReasons set to the approved reason code that matches your use (for example CA92.1 for UserDefaults read and written only by the app itself).
      See: Apple · Describing use of required reason API · https://developer.apple.com/documentation/bundleresources/describing-use-of-required-reason-api
           Apple · NSPrivacyAccessedAPIType · https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitype
  ✖ ios.purpose-string-missing  Protected resource used without a purpose string
      - Camera is used (dependency image_picker (pubspec.yaml)) but NSCameraUsageDescription is missing from ios/Runner/Info.plist.
        Files: ios/Runner/Info.plist, pubspec.yaml
      - Location (while in use) is used (dependency geolocator (pubspec.yaml)) but NSLocationWhenInUseUsageDescription is missing from ios/Runner/Info.plist.
        Files: ios/Runner/Info.plist, pubspec.yaml
      Fix: Add the key to Info.plist (or to INFOPLIST_KEY_* build settings, or expo.ios.infoPlist) with one specific sentence: what the app uses it for, e.g. "Your camera is used to take profile photos and verify your identity."
      See: Apple 5.1.1(ii) · Data Collection and Storage: Permission · https://developer.apple.com/app-store/review/guidelines/#5.1.1
           Apple · Requesting access to protected resources · https://developer.apple.com/documentation/uikit/requesting-access-to-protected-resources
  ✖ ios.subscription-min-period  Auto-renewable subscription shorter than seven days
      Subscription com.example.amoryx.boost3day renews every P3D (3 days) in ios/Runner/Products.storekit.
      Files: ios/Runner/Products.storekit
      Fix: Use a period of one week or longer. For short boosts (a common dating-app product) sell a consumable in-app purchase instead.
      See: Apple 3.1.2(a) · Subscriptions: Permissible uses · https://developer.apple.com/app-store/review/guidelines/#3.1.2a
  ✖ listing.field-limits  Store listing field is over the length limit
      appStore.name is 38 characters; the limit is 30.
      Fix: Shorten the field. App Store Connect and Play Console will not save values over the limit.
      See: Apple 2.3.7 · Accurate Metadata: app names · https://developer.apple.com/app-store/review/guidelines/#2.3.7
           Apple · App information (name, subtitle) · https://developer.apple.com/help/app-store-connect/reference/app-information/app-information
           Apple · Platform version information (keywords, description, promotional text) · https://developer.apple.com/help/app-store-connect/reference/app-information/platform-version-information
           Google Play · Best practice for your store listing · https://support.google.com/googleplay/android-developer/answer/13393723
  ✖ listing.privacy-policy  No privacy policy URL in the store listing data
      links.privacyPolicyUrl is not set.
      Fix: Publish a privacy policy that covers what you collect, who you share it with, retention and how to delete data. Set links.privacyPolicyUrl in store-review.yaml and link it in the app's settings.
      See: Apple 5.1.1(i) · Data Collection and Storage: Privacy Policies · https://developer.apple.com/app-store/review/guidelines/#5.1.1
           Google Play · Provide information for Google Play's Data safety section · https://support.google.com/googleplay/android-developer/answer/10787469
  ✖ listing.prohibited-dating-terms  Listing uses terms tied to hookup or compensated dating
      appStore.description contains "hookup".
      Fix: Remove these terms from the name, subtitle, keywords and descriptions, and make sure the product itself does not offer what they describe.
      See: Apple 1.1.4 · Objectionable Content: overtly sexual material · https://developer.apple.com/app-store/review/guidelines/#1.1.4
           Google Play · Inappropriate Content: sexual content · https://support.google.com/googleplay/android-developer/answer/9878810

Summary: 9 errors, 22 warnings, 5 info · 40 rules checked, 0 not applicable · 9 files scanned
27 findings below --min-severity not shown.
Static checks against public App Store and Google Play guidelines. A clean report does not guarantee approval: review is done by people, and policies change. Treat findings as a checklist, not a verdict.
$ npx store-review-check test/fixtures/flutter-dating-risky
store-review-check 0.1.0 · rules 2026.10.15
Project: test/fixtures/flutter-dating-risky
Platforms: ios, android (flutter)
Listing data: store-review.yaml
Dating app: yes (app.category is "dating")
User content: yes (dating apps show profiles, photos and messages)
Accounts: yes (dependency firebase_auth (pubspec.yaml), lib/main.dart)

ERRORS (9)
  ✖ android.billing-library  Google Play Billing Library version is no longer accepted
      com.android.billingclient:billing:6.2.1 in android/app/build.gradle. Play requires Billing Library 8 or later since 2026-08-31 (extension until 2026-11-01).
      Files: android/app/build.gradle
      Fix: Update com.android.billingclient:billing (or billing-ktx) to 8.x or newer. If billing comes from a plugin (in_app_purchase, RevenueCat, react-native-iap), update the plugin to a release built on PBL 8+.
      See: Google Play · Google Play Billing Library version deprecation · https://developer.android.com/google/play/billing/deprecation-faq
  ✖ android.target-sdk  targetSdk is below the current Google Play requirement
      targetSdk is 34 (android/app/build.gradle). Google Play requires 36 or higher for new apps and updates since 2026-08-31 (extension requests until 2026-11-01). At 34 an already published app is also hidden from new users on newer Android versions. Wear OS and Android Automotive OS apps need API 35; Android TV and Android XR apps need API 34.
      Files: android/app/build.gradle
      Fix: Raise targetSdk (and compileSdk) to 36 in the app module's build.gradle, then test the Android 15 and 16 behavior changes.
      See: Google Play · Meet Google Play's target API level requirement · https://developer.android.com/google/play/requirements/target-sdk
           Google Play · Target API level requirements for Google Play apps · https://support.google.com/googleplay/android-developer/answer/11926878
  ✖ ios.privacy-manifest-required-reason  Required reason API used but not declared in the privacy manifest
      User defaults APIs are used in ios/Runner/AppDelegate.swift but NSPrivacyAccessedAPICategoryUserDefaults is not declared in the app (no PrivacyInfo.xcprivacy exists yet).
      Files: ios/Runner/AppDelegate.swift
      Fix: Add a dictionary to NSPrivacyAccessedAPITypes with NSPrivacyAccessedAPIType set to the category and NSPrivacyAccessedAPITypeReasons set to the approved reason code that matches your use (for example CA92.1 for UserDefaults read and written only by the app itself).
      See: Apple · Describing use of required reason API · https://developer.apple.com/documentation/bundleresources/describing-use-of-required-reason-api
           Apple · NSPrivacyAccessedAPIType · https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacyaccessedapitypes/nsprivacyaccessedapitype
  ✖ ios.purpose-string-missing  Protected resource used without a purpose string
      - Camera is used (dependency image_picker (pubspec.yaml)) but NSCameraUsageDescription is missing from ios/Runner/Info.plist.
        Files: ios/Runner/Info.plist, pubspec.yaml
      - Location (while in use) is used (dependency geolocator (pubspec.yaml)) but NSLocationWhenInUseUsageDescription is missing from ios/Runner/Info.plist.
        Files: ios/Runner/Info.plist, pubspec.yaml
      Fix: Add the key to Info.plist (or to INFOPLIST_KEY_* build settings, or expo.ios.infoPlist) with one specific sentence: what the app uses it for, e.g. "Your camera is used to take profile photos and verify your identity."
      See: Apple 5.1.1(ii) · Data Collection and Storage: Permission · https://developer.apple.com/app-store/review/guidelines/#5.1.1
           Apple · Requesting access to protected resources · https://developer.apple.com/documentation/uikit/requesting-access-to-protected-resources
  ✖ ios.subscription-min-period  Auto-renewable subscription shorter than seven days
      Subscription com.example.amoryx.boost3day renews every P3D (3 days) in ios/Runner/Products.storekit.
      Files: ios/Runner/Products.storekit
      Fix: Use a period of one week or longer. For short boosts (a common dating-app product) sell a consumable in-app purchase instead.
      See: Apple 3.1.2(a) · Subscriptions: Permissible uses · https://developer.apple.com/app-store/review/guidelines/#3.1.2a
  ✖ listing.field-limits  Store listing field is over the length limit
      appStore.name is 38 characters; the limit is 30.
      Fix: Shorten the field. App Store Connect and Play Console will not save values over the limit.
      See: Apple 2.3.7 · Accurate Metadata: app names · https://developer.apple.com/app-store/review/guidelines/#2.3.7
           Apple · App information (name, subtitle) · https://developer.apple.com/help/app-store-connect/reference/app-information/app-information
           Apple · Platform version information (keywords, description, promotional text) · https://developer.apple.com/help/app-store-connect/reference/app-information/platform-version-information
           Google Play · Best practice for your store listing · https://support.google.com/googleplay/android-developer/answer/13393723
  ✖ listing.privacy-policy  No privacy policy URL in the store listing data
      links.privacyPolicyUrl is not set.
      Fix: Publish a privacy policy that covers what you collect, who you share it with, retention and how to delete data. Set links.privacyPolicyUrl in store-review.yaml and link it in the app's settings.
      See: Apple 5.1.1(i) · Data Collection and Storage: Privacy Policies · https://developer.apple.com/app-store/review/guidelines/#5.1.1
           Google Play · Provide information for Google Play's Data safety section · https://support.google.com/googleplay/android-developer/answer/10787469
  ✖ listing.prohibited-dating-terms  Listing uses terms tied to hookup or compensated dating
      appStore.description contains "hookup".
      Fix: Remove these terms from the name, subtitle, keywords and descriptions, and make sure the product itself does not offer what they describe.
      See: Apple 1.1.4 · Objectionable Content: overtly sexual material · https://developer.apple.com/app-store/review/guidelines/#1.1.4
           Google Play · Inappropriate Content: sexual content · https://support.google.com/googleplay/android-developer/answer/9878810

WARNINGS (22)
  ▲ account.deletion  Account creation without in-app account deletion
      Accounts are created (dependency firebase_auth (pubspec.yaml), lib/main.dart) but no delete-account flow was found in source or strings.
      Fix: Add Settings > Delete account that deletes the account and its data server-side (not just deactivation). If deletion lives in a WebView or on the server, declare it with features.accountDeletionInApp: true in store-review.yaml.
      See: Apple 5.1.1(v) · Data Collection and Storage: Account Sign-In · https://developer.apple.com/app-store/review/guidelines/#5.1.1v
           Apple · Offering account deletion in your app · https://developer.apple.com/support/offering-account-deletion-in-your-app/
           Google Play · Understanding Google Play's app account deletion requirements · https://support.google.com/googleplay/android-developer/answer/13327111
  ▲ account.deletion-web-link  No web link for account deletion (Google Play)
      No links.accountDeletionUrl in the store listing data. Google Play asks for this link in the Data safety form.
      Fix: Publish a page (for example https://example.com/delete-account) that explains the steps and accepts requests, then set links.accountDeletionUrl in store-review.yaml and add it in Play Console.
      See: Google Play · Understanding Google Play's app account deletion requirements · https://support.google.com/googleplay/android-developer/answer/13327111
  ▲ android.background-location  ACCESS_BACKGROUND_LOCATION declared
      ACCESS_BACKGROUND_LOCATION declared in android/app/src/main/AndroidManifest.xml.
      Files: android/app/src/main/AndroidManifest.xml
      Fix: Request location only while the app is in use. If background access is truly core, prepare the declaration form and a video of the disclosure flow.
      See: Google Play · Location Permissions · https://support.google.com/googleplay/android-developer/answer/9799150
  ▲ android.permission-rationale  Sensitive runtime permissions with no disclosure or rationale found
      Declared: CAMERA, ACCESS_FINE_LOCATION, ACCESS_BACKGROUND_LOCATION, READ_CONTACTS, RECEIVE_SMS, READ_MEDIA_IMAGES. No rationale or disclosure code found (shouldShowRequestPermissionRationale, a rationale dialog, or permission_handler's shouldShowRequestRationale).
      Files: android/app/src/main/AndroidManifest.xml
      Fix: Before each runtime request, show a screen that says what data you access and why, and handle the shouldShowRequestPermissionRationale case. Request permissions in context (camera when the user taps Add photo), not all at launch.
      See: Google Play · User Data: Prominent Disclosure & Consent Requirement · https://support.google.com/googleplay/android-developer/answer/10144311
           Google Play · Request runtime permissions · https://developer.android.com/training/permissions/requesting
  ▲ android.photo-video-permissions  READ_MEDIA_IMAGES / READ_MEDIA_VIDEO declared
      READ_MEDIA_IMAGES declared in android/app/src/main/AndroidManifest.xml.
      Files: android/app/src/main/AndroidManifest.xml
      Fix: Use the Android Photo Picker (androidx.activity 1.7.0+ PickVisualMedia; Flutter image_picker and Expo ImagePicker use it) and remove the permissions. If a library adds them through manifest merging, remove them with tools:node="remove".
      See: Google Play · Photo and Video Permissions (restricted permissions with minimum scope alternatives) · https://support.google.com/googleplay/android-developer/answer/14115180
  ▲ android.restricted-permissions  Restricted permission declared
      RECEIVE_SMS declared in android/app/src/main/AndroidManifest.xml. SMS and Call Log permissions are limited to default SMS/phone handlers and listed exceptions. For one-time codes use the SMS Retriever or SMS User Consent API.
      Files: android/app/src/main/AndroidManifest.xml
      Fix: Remove the permission (check merged manifests from libraries too), or prepare the declaration for the approved use case listed in the policy.
      See: Google Play · Permissions and APIs that Access Sensitive Information · https://support.google.com/googleplay/android-developer/answer/9888170
  ▲ dating.age-gate  Dating app with no age check found
      Dating app (app.category is "dating") with no date-of-birth or age check found.
      Fix: Ask for date of birth at sign-up and block under-18s (or use Apple's Declared Age Range API), state the minimum age in your terms, and declare it as age assurance in the rating questionnaires. Set features.ageGate: true when done.
      See: Apple 2.3.6 · Accurate Metadata: age rating · https://developer.apple.com/app-store/review/guidelines/#2.3.6
           Apple · Age ratings values and definitions · https://developer.apple.com/help/app-store-connect/reference/app-information/age-ratings-values-and-definitions
           Google Play · Content Ratings · https://support.google.com/googleplay/android-developer/answer/9898843
           Google Play · User Generated Content (incidental sexual content) · https://support.google.com/googleplay/android-developer/answer/9876937
  ▲ iap.subscription-disclosure  Subscription terms, price or legal links missing
      Subscriptions found: no links.termsOfUseUrl (Apple requires a Terms of Use link in the app and in App Store metadata); no plans listed under subscriptions, so price and period cannot be checked.
      Fix: Set links.termsOfUseUrl and links.privacyPolicyUrl, list each plan under subscriptions (price, period, trial) in store-review.yaml, and put the same facts on the paywall next to the buy button.
      See: Apple 3.1.2(c) · Subscriptions: Subscription Information · https://developer.apple.com/app-store/review/guidelines/#3.1.2c
           Apple · Auto-renewable subscriptions · https://developer.apple.com/app-store/subscriptions/
           Google Play · Subscriptions · https://support.google.com/googleplay/android-developer/answer/9900533
  ▲ iap.third-party-payments  Third-party payment SDK in an app that sells digital features
      Found dependency flutter_stripe (pubspec.yaml). Make sure it only charges for physical goods or real-world services. This is a dating app, so premium features count as digital goods.
      Files: pubspec.yaml
      Fix: Sell digital features through StoreKit / Play Billing. Keep Stripe or PayPal only for physical goods or real-world services, or follow the specific external-link programs for the storefronts you target.
      See: Apple 3.1.1 · In-App Purchase · https://developer.apple.com/app-store/review/guidelines/#3.1.1
           Google Play · Payments · https://support.google.com/googleplay/android-developer/answer/9858738
  ▲ ios.privacy-manifest-missing  No app privacy manifest (PrivacyInfo.xcprivacy)
      No PrivacyInfo.xcprivacy found for the app target. Expected something like ios/Runner/PrivacyInfo.xcprivacy.
      Fix: In Xcode choose File > New File > App Privacy, add it to the app target, and declare collected data types and each required reason API category your own code uses. Flutter: ios/Runner/PrivacyInfo.xcprivacy. Expo: expo.ios.privacyManifests in app.json.
      See: Apple · Privacy manifest files · https://developer.apple.com/documentation/bundleresources/privacy-manifest-files
           Apple · Describing use of required reason API · https://developer.apple.com/documentation/bundleresources/describing-use-of-required-reason-api
  ▲ ios.purpose-string-quality  Purpose string is empty, a placeholder or too vague
      NSPhotoLibraryUsageDescription is empty or a placeholder ("Photos") in ios/Runner/Info.plist.
      Files: ios/Runner/Info.plist
      Fix: Rewrite the string as a full sentence that names the feature, e.g. "Location is used to show people near you and the distance to them."
      See: Apple 5.1.1(ii) · Data Collection and Storage: Permission · https://developer.apple.com/app-store/review/guidelines/#5.1.1
  ▲ ios.restore-purchases  In-app purchases without a restore mechanism
      In-app purchases found (dependency purchases_flutter (pubspec.yaml)) but no restore call (AppStore.sync, restorePurchases, restoreCompletedTransactions).
      Files: pubspec.yaml
      Fix: Add a visible "Restore purchases" button that calls AppStore.sync() (StoreKit 2), restoreCompletedTransactions() (StoreKit 1) or your SDK's restore method (RevenueCat restorePurchases, in_app_purchase restorePurchases).
      See: Apple 3.1.1 · In-App Purchase · https://developer.apple.com/app-store/review/guidelines/#3.1.1
           Apple · Auto-renewable subscriptions: clearly describing subscriptions · https://developer.apple.com/app-store/subscriptions/
  ▲ ios.sign-in-with-apple  Third-party login without an equivalent privacy-friendly option
      Third-party login found (dependency google_sign_in (pubspec.yaml), Info.plist key GIDClientID (ios/Runner/Info.plist)) and no Sign in with Apple or other 4.8-compliant option.
      Files: pubspec.yaml, ios/Runner/Info.plist
      Fix: Add Sign in with Apple next to the other social buttons (entitlement com.apple.developer.applesignin plus the ASAuthorization flow), or confirm that one of the exceptions in 4.8 applies to your app.
      See: Apple 4.8 · Login Services · https://developer.apple.com/app-store/review/guidelines/#4.8
  ▲ ios.tracking-without-att  Ad or attribution SDK present, no App Tracking Transparency prompt found
      Found dependency google_mobile_ads (pubspec.yaml), but no App Tracking Transparency request and no NSUserTrackingUsageDescription.
      Files: pubspec.yaml
      Fix: If any SDK tracks users (IDFA, cross-app attribution), request permission with ATTrackingManager.requestTrackingAuthorization and add NSUserTrackingUsageDescription. If none does, configure the SDKs not to track and answer the App Privacy questions to match.
      See: Apple 5.1.2(i) · Data Use and Sharing · https://developer.apple.com/app-store/review/guidelines/#5.1.2
           Apple · App Tracking Transparency · https://developer.apple.com/documentation/apptrackingtransparency
  ▲ listing.competitor-names  Other app names in the App Store keywords or name
      appStore.keywords mentions "tinder", "bumble".
      Fix: Remove other apps' names from the name, subtitle and keywords; describe what your app does instead.
      See: Apple 2.3.7 · Accurate Metadata: names and keywords · https://developer.apple.com/app-store/review/guidelines/#2.3.7
  ▲ listing.other-platform-mentions  App Store listing mentions Android or Google Play
      appStore.description contains "Android", "Google Play".
      Fix: Remove mentions such as "also on Android" or "Google Play" from the App Store name, subtitle, keywords, description and promotional text.
      See: Apple 2.3.10 · Accurate Metadata: other platforms · https://developer.apple.com/app-store/review/guidelines/#2.3.10
  ▲ listing.promo-terms  Price, promotion or ranking claims in the name, subtitle, title or keywords
      - appStore.name contains "Free".
      - appStore.keywords contains "free".
      Fix: Move prices and offers into the paywall and promotional text; keep the title to the brand plus a plain description.
      See: Apple 2.3.7 · Accurate Metadata · https://developer.apple.com/app-store/review/guidelines/#2.3.7
           Google Play · Metadata · https://support.google.com/googleplay/android-developer/answer/9898842
  ▲ listing.title-format  Google Play title uses emoji, repeated symbols or ALL CAPS
      googlePlay.title "AMORYX 💘 DATING!!" uses emoji, repeated special characters, ALL CAPS (fine only if that is the brand name).
      Fix: Write the title in normal case without emoji or decorative symbols.
      See: Google Play · Metadata · https://support.google.com/googleplay/android-developer/answer/9898842
  ▲ review.demo-account  Login required, but no demo account for reviewers
      The app has a login (dependency firebase_auth (pubspec.yaml), lib/main.dart). Set review.demoAccountProvided: true once a reviewer account is entered in both consoles.
      Fix: Create a reviewer account with a finished profile, a few matches and a chat, enter it in App Review Information / App access, and set review.demoAccountProvided: true in store-review.yaml.
      See: Apple 2.1(a) · App Completeness · https://developer.apple.com/app-store/review/guidelines/#2.1
           Google Play · Prepare your app for review (App access) · https://support.google.com/googleplay/android-developer/answer/9859455
  ▲ ugc.moderation  User-generated content without report, block, filter or contact info
      No sign of filtering of objectionable text and photos, a way to report users and content, a way to block users in source or strings. User content detected: dating apps show profiles, photos and messages.
      Fix: Add Report and Block on every profile and in every chat, filter text and photos (word list plus a moderation API), and publish a support contact. If these live on the server or in a WebView, declare them under features in store-review.yaml.
      See: Apple 1.2 · User-Generated Content · https://developer.apple.com/app-store/review/guidelines/#1.2
           Google Play · User Generated Content · https://support.google.com/googleplay/android-developer/answer/9876937
  ▲ ugc.terms-acceptance  No terms of use that users accept before posting content
      No terms of use link (links.termsOfUseUrl) and no terms acceptance step found in source or strings.
      Fix: Show the terms (with a zero-tolerance line on objectionable content and abusive users) at sign-up and require a tap to accept before the first profile photo or message. Set links.termsOfUseUrl in store-review.yaml.
      See: Google Play · User Generated Content · https://support.google.com/googleplay/android-developer/answer/9876937

INFO (5)
  ● android.contacts-permission  READ_CONTACTS declared (new Contacts Permission policy)
      READ_CONTACTS declared in android/app/src/main/AndroidManifest.xml.
      Files: android/app/src/main/AndroidManifest.xml
      Fix: Use the Contact Picker for invite-a-friend flows. Keep READ_CONTACTS only if a core feature needs the full list, and prepare the declaration.
      See: Google Play · Contacts Permission (restricted permissions with minimum scope alternatives) · https://support.google.com/googleplay/android-developer/answer/14115180
  ● android.location-minimum-scope  Precise location declared; approximate may be enough
      ACCESS_FINE_LOCATION declared in android/app/src/main/AndroidManifest.xml. Check whether approximate location is enough.
      Files: android/app/src/main/AndroidManifest.xml
      Fix: Declare only ACCESS_COARSE_LOCATION unless a feature needs precise location, and say which feature in your Data safety answers.
      See: Google Play · Location Permissions · https://support.google.com/googleplay/android-developer/answer/9799150
  ● ios.saturated-category  Dating is a category Apple calls well established
      Detected as a dating app (app.category is "dating"). Expect App Review to ask what makes it different from existing dating apps.
      Fix: In the App Review notes, describe in two or three sentences what your app does that existing dating apps don't (audience, matching model, safety features). Don't ship several near-identical dating apps from one account.
      See: Apple 4.3(b) · Spam · https://developer.apple.com/app-store/review/guidelines/#4.3
  ● listing.age-rating-dating  Age rating answers to review for a dating app
      Dating app (app.category is "dating"): review the user-generated content, messaging and chat, age assurance and mature themes answers.
      Fix: Answer yes to user-generated content and messaging, describe your age assurance, and pick the frequency of mature themes honestly. Keep screenshots free of suggestive photos.
      See: Apple 2.3.6 · Accurate Metadata: age rating · https://developer.apple.com/app-store/review/guidelines/#2.3.6
           Apple 2.3.8 · Accurate Metadata: appropriate for all audiences · https://developer.apple.com/app-store/review/guidelines/#2.3.8
           Apple · Age ratings values and definitions · https://developer.apple.com/help/app-store-connect/reference/app-information/age-ratings-values-and-definitions
           Google Play · Content Ratings · https://support.google.com/googleplay/android-developer/answer/9898843
  ● privacy.data-safety-hints  Data types to declare in Data safety and App Privacy
      Likely data types: Location: Precise location; Location: Approximate location; Photos and videos: Photos; Contacts: Contacts; Messages: Other in-app messages; Personal info: Name, Email address, User IDs; Personal info: Other info (profile details such as date of birth and gender), and Sexual orientation or Political or religious beliefs if the profile asks for them; Device or other IDs; Financial info: Purchase history; Financial info: User payment info.
      Fix: Compare this list with your Data safety form and App Privacy answers. Set googlePlay.dataSafety.dataTypes in store-review.yaml to have this check compare them for you.
      See: Google Play · Provide information for Google Play's Data safety section · https://support.google.com/googleplay/android-developer/answer/10787469
           Apple · App privacy details on the App Store · https://developer.apple.com/app-store/app-privacy-details/

Summary: 9 errors, 22 warnings, 5 info · 40 rules checked, 0 not applicable · 9 files scanned
Static checks against public App Store and Google Play guidelines. A clean report does not guarantee approval: review is done by people, and policies change. Treat findings as a checklist, not a verdict.

Install and run

You need Node.js 22 or newer. No install is needed with npx:

npx store-review-check                 # current folder
npx store-review-check ./apps/mobile   # a subfolder of a monorepo
npx store-review-check . --json > store-review-report.json
npx store-review-check rules --tag dating

Or add it to the project:

npm install --save-dev store-review-check

Option

What it does

-p, --platform ios|android|all

Check one store only. Default: every platform found.

-m, --metadata <file>

Store listing data. Default: store-review.yaml in the project root.

--dating

Run the dating-app checks even if nothing in the project says it is one.

--json

Print the JSON report.

-o, --output <file>

Also write the report to a file. A .json name writes JSON.

--min-severity error|warning|info

Hide findings below this level. They still count in the summary.

--fail-on error|warning|info|never

Exit with 1 when there are findings at this level. Default: error.

-v, --verbose

List the rules that did not apply and why.

--no-color

Plain output. NO_COLOR works too.

Exit codes: 0 when nothing reaches the --fail-on level, 1 when something does, 2 for a usage error or a folder that doesn't exist.

In CI

# .github/workflows/store-review.yml
name: Store review check
on: [pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 22
      - run: npx store-review-check . --fail-on error

As a library

import { checkProject, checkListing, formatText } from 'store-review-check';

const report = checkProject('./my-app', { platform: 'ios' });
console.log(report.summary, report.findings);
console.log(formatText(report));

Store listing data

Some rules need facts that aren't in the code: your listing text, your privacy policy link, or the fact that reporting lives on your server. Put them in store-review.yaml (or .json) in the project root. Every field is optional.

app:
  name: Shelfmate
  category: dating            # "dating", "matchmaking" or "matrimony" turns on the dating checks
  hasAccounts: true
  hasUserGeneratedContent: true
  hasChat: true

links:
  privacyPolicyUrl: https://example.com/privacy
  termsOfUseUrl: https://example.com/terms
  supportUrl: https://example.com/support
  contactEmail: support@example.com
  accountDeletionUrl: https://example.com/delete-account   # Google Play asks for this

appStore:
  name: Shelfmate
  subtitle: Slow dating for book lovers
  keywords: books,reading,slow dating,book club
  description: Meet people through the books you love.
  ageRating: 18+

googlePlay:
  title: Shelfmate
  shortDescription: Meet people through the books you love.
  fullDescription: Every match starts with a shared favorite book.
  dataSafety:
    dataTypes: [Location, Photos and videos, Messages, Personal info]

# Things the code scan can't see, such as server-side moderation.
features:
  accountDeletionInApp: true
  reportContent: true
  blockUsers: true
  contentFiltering: true
  ageGate: true
  restorePurchases: true
  termsAcceptance: true
  permissionDisclosure: true

review:
  demoAccountProvided: true

subscriptions:
  - productId: com.example.shelfmate.premium.monthly
    name: Premium monthly
    price: $12.99
    period: 1 month
    trial: 7 days free

If the project uses fastlane, the tool also reads fastlane/metadata (App Store name, subtitle, keywords, description, privacy and support URLs) and fastlane/metadata/android (Play title and descriptions). Values in store-review.yaml win. Demo account passwords are never read.

Misspelled keys are reported as errors, so a typo can't silently switch a check off.

Use it as an MCP server

npx store-review-check mcp starts an MCP server over stdio. It has three read-only tools:

Tool

Input

What it returns

check_project

path, and optionally platform, metadataPath, dating, minSeverity

The text report plus structured findings

check_listing

listing (same shape as store-review.yaml), platform, dating, minSeverity

Listing problems, without a project folder

list_rules

platform, tag, id

Rules with severity, guideline section and source URL

check_listing is useful while an assistant drafts store copy: it flags length limits, price claims, other apps' names and risky dating wording before anything reaches App Store Connect.

Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "store-review-check": {
      "command": "npx",
      "args": ["-y", "store-review-check", "mcp"]
    }
  }
}

Cursor (.cursor/mcp.json in the project, or ~/.cursor/mcp.json):

{
  "mcpServers": {
    "store-review-check": {
      "command": "npx",
      "args": ["-y", "store-review-check", "mcp"]
    }
  }
}

Claude Code:

claude mcp add store-review-check -- npx -y store-review-check mcp

Then ask something like "Check this project for App Store rejection risks" or "Is this App Store description OK for a dating app?".

Rules

40 rules, checked against the official pages on 2026-10-15. Source of truth: rules/rules.json.

Rule

Severity

Platform

What it checks

Source

ios.purpose-string-missing

error

iOS

Protected resource used without a purpose string

Apple 5.1.1(ii)Apple: Requesting access to protected resources

ios.purpose-string-quality

warning

iOS

Purpose string is empty, a placeholder or too vague

Apple 5.1.1(ii)

ios.privacy-manifest-missing

warning

iOS

No app privacy manifest (PrivacyInfo.xcprivacy)

Apple: Privacy manifest filesApple: Describing use of required reason API

ios.privacy-manifest-required-reason

error

iOS

Required reason API used but not declared in the privacy manifest

Apple: Describing use of required reason APIApple: NSPrivacyAccessedAPIType

ios.privacy-manifest-tracking-domains

warning

iOS

NSPrivacyTracking is true but no tracking domains are listed

Apple: Privacy manifest files

ios.tracking-without-att

warning

iOS

Ad or attribution SDK present, no App Tracking Transparency prompt found

Apple 5.1.2(i)Apple: App Tracking Transparency

ios.sign-in-with-apple

warning

iOS

Third-party login without an equivalent privacy-friendly option

Apple 4.8

ios.siwa-entitlement

warning

iOS

Sign in with Apple code without the entitlement

Apple 2.1(a)Apple: Sign in with Apple entitlement

ios.subscription-min-period

error

iOS

Auto-renewable subscription shorter than seven days

Apple 3.1.2(a)

ios.restore-purchases

warning

iOS

In-app purchases without a restore mechanism

Apple 3.1.1Apple: Auto-renewable subscriptions: clearly describing subscriptions

ios.saturated-category

info

iOS

Dating is a category Apple calls well established

Apple 4.3(b)

ios.photo-picker

info

iOS

Full photo library access where the system picker may be enough

Apple 5.1.1(iii)

android.target-sdk

error

Android

targetSdk is below the current Google Play requirement

Google Play: Meet Google Play's target API level requirementGoogle Play: Target API level requirements for Google Play apps

android.target-sdk-unresolved

info

Android

targetSdk could not be read from the build files

Google Play: Meet Google Play's target API level requirement

android.billing-library

error

Android

Google Play Billing Library version is no longer accepted

Google Play: Google Play Billing Library version deprecation

android.photo-video-permissions

warning

Android

READ_MEDIA_IMAGES / READ_MEDIA_VIDEO declared

Google Play: Photo and Video Permissions (restricted permissions with minimum scope alternatives)

android.background-location

warning

Android

ACCESS_BACKGROUND_LOCATION declared

Google Play: Location Permissions

android.location-minimum-scope

info

Android

Precise location declared; approximate may be enough

Google Play: Location Permissions

android.restricted-permissions

warning

Android

Restricted permission declared

Google Play: Permissions and APIs that Access Sensitive Information

android.contacts-permission

info

Android

READ_CONTACTS declared (new Contacts Permission policy)

Google Play: Contacts Permission (restricted permissions with minimum scope alternatives)

android.permission-rationale

warning

Android

Sensitive runtime permissions with no disclosure or rationale found

Google Play: User Data: Prominent Disclosure & Consent RequirementGoogle Play: Request runtime permissions

account.deletion

warning

Both

Account creation without in-app account deletion

Apple 5.1.1(v)Apple: Offering account deletion in your appGoogle Play: Understanding Google Play's app account deletion requirements

account.deletion-web-link

warning

Android

No web link for account deletion (Google Play)

Google Play: Understanding Google Play's app account deletion requirements

ugc.moderation

warning

Both

User-generated content without report, block, filter or contact info

Apple 1.2Google Play: User Generated Content

ugc.terms-acceptance

warning

Both

No terms of use that users accept before posting content

Google Play: User Generated Content

iap.third-party-payments

warning

Both

Third-party payment SDK in an app that sells digital features

Apple 3.1.1Google Play: Payments

iap.subscription-disclosure

warning

Both

Subscription terms, price or legal links missing

Apple 3.1.2(c)Apple: Auto-renewable subscriptionsGoogle Play: Subscriptions

review.demo-account

warning

Both

Login required, but no demo account for reviewers

Apple 2.1(a)Google Play: Prepare your app for review (App access)

dating.age-gate

warning

Both

Dating app with no age check found

Apple 2.3.6Apple: Age ratings values and definitionsGoogle Play: Content RatingsGoogle Play: User Generated Content (incidental sexual content)

privacy.third-party-ai

info

Both

Personal data may go to a third-party AI service

Apple 5.1.2(i)

privacy.data-safety-hints

info

Both

Data types to declare in Data safety and App Privacy

Google Play: Provide information for Google Play's Data safety sectionApple: App privacy details on the App Store

listing.privacy-policy

error

Both

No privacy policy URL in the store listing data

Apple 5.1.1(i)Google Play: Provide information for Google Play's Data safety section

listing.field-limits

error

Both

Store listing field is over the length limit

Apple 2.3.7Apple: App information (name, subtitle)Apple: Platform version information (keywords, description, promotional text)Google Play: Best practice for your store listing

listing.prohibited-dating-terms

error

Both

Listing uses terms tied to hookup or compensated dating

Apple 1.1.4Google Play: Inappropriate Content: sexual content

listing.anonymous-chat-terms

warning

iOS

Listing promotes random or anonymous chat or rating people

Apple 1.2

listing.competitor-names

warning

iOS

Other app names in the App Store keywords or name

Apple 2.3.7

listing.promo-terms

warning

Both

Price, promotion or ranking claims in the name, subtitle, title or keywords

Apple 2.3.7Google Play: Metadata

listing.other-platform-mentions

warning

iOS

App Store listing mentions Android or Google Play

Apple 2.3.10

listing.title-format

warning

Android

Google Play title uses emoji, repeated symbols or ALL CAPS

Google Play: Metadata

listing.age-rating-dating

info

Both

Age rating answers to review for a dating app

Apple 2.3.6Apple 2.3.8Apple: Age ratings values and definitionsGoogle Play: Content Ratings

Severity means:

  • error: the stores block this at upload or reject for it, and the project shows it directly (a missing key, a version number, a field over its limit).

  • warning: a common rejection reason, or something the tool could not find evidence for. Check it.

  • info: a reminder or a likely question from review.

How it works, and what it can't see

The scan walks the project (skipping node_modules, Pods, build and similar folders), parses the build and config files, and searches your source and localization strings for specific API names and patterns. The patterns live in rules/rules.json next to the rules, so you can read exactly what triggers each finding.

What that means in practice:

  • Absence is a guess. "No report button found" means no matching code or string was found. If reporting lives in a WebView, on the server, or under an unusual name, say so in features and the warning goes away.

  • Library manifests aren't merged. Permissions that an Android library adds at build time don't show up unless you scan the merged manifest. Check app/build/intermediates/merged_manifests after a build.

  • Some values are only known at build time. flutter.targetSdkVersion, the Expo SDK default and app.config.js aren't evaluated; the report says so instead of guessing.

  • Binary property lists aren't parsed. Convert them with plutil -convert xml1.

  • String matching is English-first. Localized UI text in other languages may not count as evidence.

  • Passing isn't approval. App Review is done by people, and guidelines change. Treat the report as a checklist.

Contributing

Issues and pull requests are welcome, especially new rules, false positives and false negatives. Please don't include code or details from apps you don't own.

To add or change a rule:

  1. Find the requirement on an official page: the App Review Guidelines, Apple developer documentation, Play Console Help or developer.android.com. Rules without an official source are not accepted.

  2. Edit rules/rules.json: add the rule with its references, and any detection patterns under signals. If it needs new logic, add a check function in src/checks/ and register it in src/checks/index.ts.

  3. Add or extend a fixture under test/fixtures/ and assert the finding in test/fixtures.test.ts. Add a negative case too, so the rule doesn't fire where it shouldn't.

  4. Run:

    npm install
    npm test             # build + all tests
    npm run check:urls   # every source URL must load
    npm run docs:rules   # regenerate the rule table above
    npm run format:rules # keep rules.json tidy

When a store changes a deadline or a number (target SDK level, Billing Library version), update params in the rule and the verified date at the top of rules.json.

Credits

Built by Rocketech, a team that builds and ships dating and social apps for iOS and Android. If a review keeps bouncing your app, we can help.

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Local-first security check for AI coding agents — finds hardcoded secrets, exposed .env files, git-history leaks and vulnerable dependencies (OSV), entirely on your machine. Ask your agent "is this safe to ship?" and get a Launch Readiness score with a fix for every finding.
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables AI coding tools to scan projects for security vulnerabilities, hardcoded secrets, injection flaws, and privacy violations with 699 rules and 76 MCP tools, all running locally with zero telemetry.
    6 npm
    7
    MIT