# Guideline 2.3.1(a) - Performance: Review-Environment Detection via Remote Configuration

**Guideline:** 2.3.1 · **Store:** Apple App Store · **Severity:** high · **Fix difficulty:** medium · **Typical turnaround:** 1-3 days

Canonical URL: https://appstorereject.com/rejections/apple/2/guideline-231a-performance-review-environment-detection-via-remote-configuration

## Description

Apple is rejecting your app under Guideline 2.3.1(a) because it found logic that detects whether the app has a configurable state meant for App Review, with that behavior switched on or off through remote configuration.

Guideline 2.3.1(a) says apps must not include hidden, dormant, or undocumented features, and that an app's functionality should be clear to both end users and App Review. Code that lets the app look one way during review and another way once it ships is the clearest case of that.

This is a more serious variant than the general 2.3.1 'hidden features' rejection. That one is usually about a feature nobody mentioned in the review notes, and better notes fix it. Here the problem is the detection mechanism itself. Review can only approve what it sees, so the guideline treats any review-specific code path as making the review unreliable, whatever the intent behind it. 

The guideline also notes that egregious or repeated behavior can lead to removal from the Apple Developer Program, so treat this one as urgent.

Common triggers:
- A remote-config key such as `review_mode`, `isReview`, `audit`, `apple_review` or `hide_feature` that the app checks at runtime.
- Version gating: the server reports the 'approved' version and the app unlocks features only when its own version is higher, or only after approval.
- Checking for a sandbox receipt (`appStoreReceiptURL` ending in `sandboxReceipt`) or a TestFlight/review build, then changing what the app shows.
- Region, IP-range, timezone or locale checks aimed at where reviewers are.
- Date or launch-count delays that keep a feature dormant until after approval.
- A third-party SDK (ad mediation, attribution, 'A/B testing' or white-label app frameworks) that ships one of the patterns above.

The reason behind the code doesn't change the outcome. Teams often add a 'review mode' for understandable reasons: skipping a login wall, hiding an unfinished screen, turning off ads or a paywall, or getting around a region lock. Apple has standard ways to handle each of these (demo accounts, review notes, StoreKit sandbox), and the review-specific branch has to go.

## Common variations

- App contains review-environment detection functionality delivered through remote configuration
- App contains hidden features controlled by a remote 'review mode' flag
- App enables different functionality after approval based on a server-side version check
- App detects the review environment (sandbox receipt, region, or IP) and changes its behavior
- Third-party SDK in the app includes review-detection or post-approval feature switching
- App contains dormant features that activate after a date or launch count

## Example rejection email

```
Guideline 2.3.1(a) - Performance

Issue Description

The app contains hidden features. Specifically, we found the app contains review-environment detection functionality delivered through remote configuration.

Next Steps

- Learn more about accurate metadata requirements in guideline 2.3.1.
- Revise the app to follow these requirements.
- Once the app is fully compliant, resubmit the app for review.
```

## Resolution steps

## Quick Assessment
- **Risk level:** High (repeat findings can put your developer account at risk)
- **Resolution path:** Remove the detection logic and its remote-config switch, then resubmit
- **Typical turnaround:** 1-3 days of work + one review cycle

## The Fix

01. **Find every review-detection path** 
Search the codebase and every bundled SDK for checks on remote-config keys (`review`, `audit`, `isReview`, `review_mode`, `approved_version`), comparisons between the app version and a server-supplied 'live' version, `appStoreReceiptURL` / `sandboxReceipt` checks, TestFlight detection, and IP/region/timezone conditions. Also check the remote-config console (Firebase Remote Config, your own config endpoint, a feature-flag vendor) for conditions targeting review builds, versions awaiting approval, or Apple IP ranges.

02. **Delete the logic from the binary, not just the flag value** 
Setting `review_mode` to `false` on the server isn't enough. The branch is still in the binary and the key still shows up in the config payload, and either can be flagged again. Remove the code path that reads the flag and the behavior it gated.

03. **Delete the remote-config keys and targeting conditions** 
Remove the keys, audiences and conditions that existed only to tell review apart from real users. Keep ordinary feature flags only if they apply to all users the same way and don't depend on the review environment, app-approval status or reviewer location.

04. **Make every feature visible or remove it** 
Any feature that was hidden during review should ship enabled in the review build, or come out of the binary until it's ready. Shipping it now and enabling it later is exactly the pattern this rejection is about.

05. **Replace 'review mode' with the standard mechanisms** 
Login wall: give a demo account in App Review Information. Paywall or IAP: StoreKit sandbox already handles review purchases. Region or carrier lock: explain it in the review notes and give a way to see the content. Unfinished screens: remove them from the build.

06. **Audit third-party SDKs** 
If the detection came from an SDK, update to a version without it or remove the SDK. White-label app frameworks, some ad mediation layers and 'dynamic content' SDKs are frequent sources. Note the SDK and version you removed so you can say so in your reply.

07. **Reply in App Review with specifics, then resubmit** 
Say what the detection was, that you removed it from the binary and the remote configuration, and that the app now behaves the same for every user. Be accurate. Apple can compare the new build and config against what it found.

## Prevention
- Never branch app behavior on review status, approval status, sandbox receipts or reviewer location
- Keep feature flags uniform across all users, or document a staged rollout in the review notes
- Don't ship code for features you plan to turn on after approval. Submit them in an update.
- Audit new SDKs for remote 'switch' or cloaking functionality before integrating them
- Treat a 2.3.1(a) finding as an account-level risk, not a one-build issue

## Appeal guidance

Only appeal if the app truly has no review-specific behavior. For example, the flagged config key is an ordinary feature flag that applies to all users the same way, or a staged rollout that doesn't depend on review status, approval status or reviewer location. In that case, reply factually: name the key, explain exactly what it controls and who it applies to, and include a screen recording showing the same behavior in every environment. If the app or any bundled SDK changes behavior based on review, sandbox receipts, approval status or reviewer location, don't appeal. Remove the logic and resubmit. Disputing a finding that turns out to be accurate makes the situation worse.

## Before / after examples

**Before:** The app fetches `review_mode` from Firebase Remote Config. While it is true (set while a build is in review) the app hides the casino-style rewards screen and shows a plain catalog. The team flips it to false after approval.
**After:** The `review_mode` key and every code path that reads it are deleted. The rewards screen is either submitted openly with an explanation in the review notes or removed from the binary.
**Why it works:** The fix is removing the mechanism that let the app behave differently in review. Turning the flag off leaves the detection logic in place.

**Before:** On launch the app compares its own version with `live_version` from the server and unlocks a premium feature only when its version is at or below the already-approved one, so the build under review never shows it.
**After:** The version comparison is removed. The premium feature behaves the same in every build, and reviewers see it through a demo account listed in App Review Information.
**Why it works:** Version gating against approval status is review-environment detection. A demo account is the standard way to show reviewers gated functionality.

**Before:** A bundled ad-mediation SDK turns off certain ad formats when it sees a sandbox receipt or an IP in Apple's ranges.
**After:** The SDK is updated to a version without environment detection (or replaced), and the reply to App Review names the SDK and version removed.
**Why it works:** Apple holds the app responsible for what its SDKs do. Naming the specific SDK you removed shows you understand the finding and resolved it.

## Common questions

**Can you appeal a 2.3.1 rejection?**

Only appeal if the app truly has no review-specific behavior. For example, the flagged config key is an ordinary feature flag that applies to all users the same way, or a staged rollout that doesn't depend on review status, approval status or reviewer location. In that case, reply factually: name the key, explain exactly what it controls and who it applies to, and include a screen recording showing the same behavior in every environment. If the app or any bundled SDK changes behavior based on review, sandbox receipts, approval status or reviewer location, don't appeal. Remove the logic and resubmit. Disputing a finding that turns out to be accurate makes the situation worse.

**How long does this typically take to fix?**

Typical turnaround is 1-3 days (difficulty: medium). After resubmission, most re-reviews complete within 24-48 hours.

---
*Machine-readable source: https://api.appstorereject.com/api/rejections/detail?slug=guideline-231a-performance-review-environment-detection-via-remote-configuration*