Apple caps how many times a year an app can even show its native review prompt — call it too often and the remaining calls silently do nothing, no error, no signal, just a no-op that looks identical to success. That scarcity is why Influxx Mobile 1.2 didn't just add a "please rate us" popup; it added a throttling gate that decides whether asking right now is worth spending one of a small, finite number of chances, and it only fires at two moments in the app where a user just visibly succeeded at something.
A Prompt You Can't Get Back
StoreKit's in-app review request isn't a dialog your app controls — it's a system sheet Apple renders, and Apple's own guidelines throttle it to a small number of prompts per year per user, with zero API signal about whether a given call actually displayed anything or silently no-opped because the quota was spent. That asymmetry — you can call the API as often as you want, but you have no idea if it's working — makes naive integrations dangerous. An app that calls RequestInAppReview() on every launch, or every time a user does anything notable, will burn its entire yearly allotment on unremarkable moments and have nothing left when a user is actually delighted.
Influxx Mobile's approach treats each prompt opportunity as a scarce resource to be spent deliberately, not a UI element to sprinkle wherever it seems plausible.
The Throttling Rules, in Actual Numbers
The decision logic lives in a small, dependency-free function, shouldAskForInfluxxReview, kept separate from the AsyncStorage and StoreKit calls specifically so the throttling rules can be tested without touching either. Three conditions all have to hold before it says yes:
- StoreKit has to be available. Non-iOS platforms and any environment where the native module isn't linked return
unavailableimmediately — no attempt is made. - The user needs at least
INFLUXX_REVIEW_MIN_SESSIONSapp opens — three, in the current configuration — before the app will ask at all. A first-run prompt reads as spam; asking someone who just installed the app to rate it before they've formed an opinion wastes a call on a moment that isn't earned. - A 90-day cooldown (
INFLUXX_REVIEW_COOLDOWN_DAYS) has to have elapsed since the last time the app asked, tracked via a storedlastAskedAttimestamp. Even a user who keeps hitting success moments only gets asked, at most, a handful of times a year.
There's a small defensive detail worth calling out: if the stored lastAskedAt timestamp is ever in the future — a device clock change, a bad write, whatever the cause — the code deliberately treats that as "just asked" rather than letting it read as an already-expired cooldown. Without that clamp, a corrupted timestamp could flip from under-throttling straight into over-throttling, re-prompting on every trigger instead of respecting the cooldown at all.
Recording the Ask Before Making the Ask
The single most deliberate line in the implementation is the order of operations in maybeAskForInfluxxReview: the lastAskedAt timestamp is written to storage before the StoreKit call goes out, not after. That ordering is a direct consequence of the API's silence. Because Apple gives no signal about whether a prompt actually displayed once the yearly quota is exhausted, stamping the timestamp only on a confirmed success isn't possible — there is no confirmed success to check for. If the code waited to record the attempt until after the call resolved, a quota-exhausted no-op would look indistinguishable from "never asked," and the very next trigger — sometimes seconds later — would try again, and the one after that, burning through every remaining attempt in a rapid sequence instead of respecting the cooldown even once. Recording first means the cooldown always takes effect, whether or not the underlying prompt actually rendered.
"The API gives you a promise that resolves and tells you almost nothing. No 'shown', no 'declined', no 'quota exceeded' — just a boolean about whether the call itself succeeded, which isn't the same question. Once you accept you're flying blind on the actual outcome, stamping the attempt before the call is the only ordering that doesn't eventually spiral into asking on every single trigger back to back."
— Alex Furman, iOS Engineer, ETAPX
Two Moments, Not a Timer
Influxx Mobile fires the review check from exactly two places in the product, both wired through the same maybeAskForInfluxxReview(trigger) helper so all throttling logic stays centralized rather than duplicated at each call site.
The first is pr_created, called right after a pull request successfully ships from the phone in the source-control flow. Shipping a PR from a mobile device is, by a wide margin, the strongest "this actually worked" moment the app produces — a multi-step flow (stage changes, write a message, push, open the PR) resolving cleanly on a touchscreen is exactly the kind of moment where a user's opinion of the app is at its highest.
The second is worktree_created, fired after a new workspace is successfully created. It's a lower-stakes, more frequent success than shipping a PR — and the comment at the call site is explicit that without a second, more common trigger, most users would never accumulate enough of the rarer PR-shipping moments to ever reach a prompt at all.
Both call sites are fire-and-forget: void maybeAskForInfluxxReview(trigger), called after the user-facing success state is already set. Neither flow waits on it, blocks on it, or surfaces any error from it — the helper is designed to never throw, wrapping its entire body in a try/catch that resolves to false on any failure, so a broken review prompt can never take down the feature it's attached to.
"I shipped my first PR from my phone waiting for a flight and got the review prompt right after it landed, which felt earned rather than random — I'd genuinely just been impressed that it worked end to end from a phone. I've used apps that ask on literally the second screen you open and it always makes me trust the app less, not more."
— Priya Chandrasekaran, mobile engineering lead at a fintech startup, Influxx user
The Debugging Story Behind "Update Now"
A related but separate lesson shows up in how the force-update screen's "Update Now" button sends users to the App Store, and it's worth telling because it's a specific, reproducible iOS pitfall that has nothing to do with review prompts and everything to do with deep-linking assumptions.
The obvious first approach — and the one the team actually tried first — was the itms-apps:// URL scheme, following the standard iOS pattern of calling canOpenURL() to check first before opening. That's the textbook way to safely deep-link into the App Store app. The problem is that canOpenURL('itms-apps://…') can report true even in environments where nothing will actually handle the link — the Simulator being the clearest case. The check passes, the code proceeds confidently, and then the actual open attempt dead-ends: Safari reports it "cannot open the page because the address is invalid," which is a confusing failure for something that just claimed it could be opened.
The Fix Was Simpler Than the Original Approach
The eventual fix drops the scheme check entirely and uses a plain https://apps.apple.com/us/app/influxx/id6791351351 URL — a universal link rather than a custom scheme. On a real device, iOS recognizes apps.apple.com as a universal link and hands it directly to the App Store app, no intermediate check required. Anywhere that doesn't apply — the Simulator, an environment without the App Store app — it degrades gracefully to opening the URL as a web page instead of failing outright. The code comment left behind on this is direct about the lesson: the full /us/app/<slug>/id<id> path matters too, since a bare id<id> URL doesn't resolve the listing correctly. One canonical URL-building function now backs every "go update the app" surface in the codebase, so the force-update gate and any other update prompt can never drift onto different listing URLs.
"Both of these ended up being the same underlying discipline: don't trust an API's confidence about something it can't actually verify. StoreKit's canOpenURL says yes when it means 'maybe.' The review API resolves successfully whether or not anything appeared on screen. Once you stop trusting those signals and design around their absence instead, you get code that's boring and correct instead of code that looks correct until it hits the one environment where it isn't."
— Marcus Ito, Head of Product, ETAPX
Why Restraint Reads as Quality
Neither piece of this — the review throttling or the App Store link fix — is a large feature. Both are the kind of engineering that's invisible when it works: a review prompt that shows up rarely enough that a user takes it seriously, and an "Update Now" button that just works instead of dead-ending in a device-specific way nobody caught until the wrong tester tried it. The throttling gate's real job isn't asking for reviews — it's protecting a scarce, opaque resource from being wasted on moments that don't deserve it, so that when it does ask, it's because the app earned the interruption.
Frequently Asked Questions
How many times can Influxx Mobile ask for a review in a year?
The app enforces at minimum a 90-day cooldown between asks on its own side, on top of whatever yearly cap Apple's StoreKit API enforces independently — the app's cooldown doesn't override or increase Apple's own limit, it just prevents Influxx from burning through it needlessly.
Does the app know if the review prompt actually appeared on screen?
No. Apple's API gives no signal distinguishing a successful display from a silent no-op once the yearly quota is spent, which is exactly why the "asked" timestamp is recorded before the call rather than after.
Will a brand-new user get asked to review the app immediately?
No. The gate requires a minimum number of app opens — three — before it will ask at all, specifically so the prompt doesn't appear before a user has had a chance to form an opinion.
What triggers the review prompt in Influxx Mobile?
Two moments: successfully shipping a pull request from the phone, and successfully creating a new workspace. Both are chosen because they represent a user just getting concrete value out of the app, not an arbitrary timer or app-open count.
Why didn't the itms-apps:// scheme work for the App Store button?
Because canOpenURL() on that scheme can report true even in environments where nothing actually handles it, such as the Simulator, leading the open attempt to fail with a confusing "address is invalid" error despite the check having passed.
Can a failed review prompt attempt crash or interrupt the app?
No. The helper that requests a review wraps its entire logic in error handling and resolves to false on any failure, and neither call site waits on or reacts to its result.
Does asking for a review ever block the action that triggered it?
No. Both trigger points call the review helper after the user-facing success state — the shipped PR, the created workspace — is already resolved, and neither awaits it.
The unglamorous part of both stories is the same: an API that won't tell you the truth about what it did, and a design that stops assuming good faith from that API and starts building around its actual, documented limits instead.

