App Store
What has to be true before a build goes to App Store review, and the reasoning behind the parts that are not obvious.
What has to be true before a build goes to review, and the reasoning for the
parts that are not obvious. Run /app-store-review for the audit itself; this
file is what the audit found and what was decided.
Account deletion — the one that is not optional
Guideline 5.1.1(v): an app that lets someone create an account has to let them delete it from inside the app. Not a mail link, not a web form, and signing out is not deleting. It is the most commonly cited rejection there is, and this app shipped without it until now.
The row is in Profile, under Account, behind a confirm. The call is
deleteAccountAndEndSession in lib/core/session, which deletes server-side
FIRST and clears the device only once that succeeds — the other order reports
success while the account still exists, which is the exact lie the guideline is
about.
The backend capability this depends on
StatsHub uses a Convex + Better Auth provider system.
convex/auth.ts enables Better Auth's authenticated deleteUser operation and
registers a deletion trigger that removes the matching StatsHub profile. No
administrator credential or account id is shipped to the device.
What is deleted locally: favourites, watchlist entries, the local profile
and photo, notification settings, filter defaults, and route history. The
server account is deleted first; only then does clearLocalAccountData() remove
those account-scoped stores. Better Auth clears its secure native session. Theme,
onboarding and layout preferences remain because they describe the device, not
the deleted account. Any new account-owned Convex table must be added to the
Better Auth deletion trigger before release.
Age rating and 5.3
The app shows odds, bookmakers, implied probability, edge and hit rates. It does not link out to any bookmaker — there is no path from the app to placing a bet, which is the line between "sports information" and "facilitating gambling". Keep it that way: a single deep link to a sportsbook changes the app's category, brings licensing and geo-restriction requirements with it, and is not a change anyone should make casually.
In App Store Connect the rating questionnaire still has to declare simulated gambling / contests references, which puts this at 17+. A wrong rating is itself a rejection, and it is answered in the portal rather than in code.
Permission strings
Bonjour and Local Network keys are absent from the StatsHub manifest. Metro discovery is a responsibility of the separately installed shared Pitsi Dev Client; carrying its permission into the store app would send customers to a setting StatsHub never uses.
NSPhotoLibraryUsageDescription says what the photo is for and where it goes.
The framework default — "Allow $(PRODUCT_NAME) to access your photos" — is
rejected regularly for not explaining the use.
The camera string is removed (cameraPermission: false on the
expo-image-picker plugin). The app never opens the camera, and a permission
declared but unused is a question at review with no good answer.
Sign in with Apple and Google
Better Auth owns the OAuth redirect and stores the resulting native session
through @better-auth/expo. Apple and
Google are shown only when their Convex deployment credentials are present.
Accounts first created with email/password can link either provider after one
password confirmation, preventing duplicate identities for the same email.
The standalone app redirects through the Convex site and returns to
statshub:///…. The shared Pitsi Dev Client deliberately does not open social
OAuth because it is a preview shell with a different installed identity; test
email/password and guest there, and test Apple/Google in a standalone/TestFlight
StatsHub build. The Apple capability remains enabled on the StatsHub App ID.
Analytics and purchases — what they cost at review
Analytics is first-party and therefore needs no ATT prompt. PostHog, sending
to StatsHub's own project. Apple's word is TRACKING, and it means linking a
person to data from other companies' apps and sites, or handing data to a broker.
Measuring your own app is not that. The moment an ad SDK, an attribution partner
or the IDFA appears, that stops being true and both the prompt and
NSUserTrackingUsageDescription become mandatory.
It still has to be declared: Product Interaction on the privacy questionnaire, linked to the user (events are identified on sign-in).
RevenueCat is the source of StatsHub Pro access. The app has a custom paywall, purchase and restore flows, a Profile restore row, and Customer Center. The paywall reads localized products from the current RevenueCat offering and shows price/cadence, restore, privacy policy, and EULA before purchase. It adds Purchases and the app user id to the privacy questionnaire.
The RevenueCat project contains the StatsHub Pro entitlement and iOS product
records for com.panospitsi.statshub.yearly and
com.panospitsi.statshub.lifetime. This is not complete until matching products
exist in App Store Connect and the RevenueCat iOS app has its App Store Connect
API key and subscription key configured. Without those, a store build cannot
return purchasable packages even though the app UI is correct.
No session replay or IP geolocation. Both are off and should stay off — this app draws the players someone follows and the bets they are looking at, and neither a recording of that nor an inferred location is needed to understand whether its product flows work.
Native integration set
Notifications, Better Auth's Expo client (including expo-network), RevenueCat,
and PostHog are mirrored by the shared Pitsi Dev Client. A
native rebuild is required only when that shared native dependency set changes;
ordinary StatsHub work runs with bun run dev and is selected from the client.
Drilled and clean
Checked against the published review skills' React-Native-specific list: no
eval or downloaded executable code (2.5.2), no expo-updates feature delivery
(3.3.1), no custom rate-us prompt, no advertising identifier or cross-app
tracking (5.1.2), no IPv4 literals (2.5.20), and no user-generated content to
moderate (1.2). Purchases use StoreKit through RevenueCat (3.1.1). Privacy and
terms open in in-app WebView routes with working close controls; the privacy
policy is StatsHub's published page and the terms default to Apple's standard
EULA.
Before submitting
- StatsHub's Convex deployment is connected, pushed, and its Better Auth self-delete flow works end to end on a real build
- Apple and Google credentials are set in the Convex deployment and both providers complete in a standalone StatsHub/TestFlight build
- Matching yearly and lifetime products exist in App Store Connect and the RevenueCat StatsHub app has Apple API/subscription credentials
- Production EAS environment contains the public Convex, PostHog and RevenueCat client variables
- Age rating declares simulated gambling / contests
- App Review Notes name the guest path ("Continue as guest" needs no credentials) and carry a demo account anyway
- Screenshots match the current UI
- Launch tested on the newest iOS, not just the team's simulators — see the
UIScene work in
plugins/with-ios-scene-lifecycle.js
Checked and fine
Privacy manifest (ios/StatsHub/PrivacyInfo.xcprivacy) declares UserDefaults,
FileTimestamp and BootTime; dependency manifests are merged by Xcode at archive
time. ITSAppUsesNonExemptEncryption: false
is set, so uploads do not stall on export compliance. Sheets are form sheets
with grabbers and swipe-to-dismiss, so nothing traps the reader. A guest path
exists, so review can get in without credentials.
Found in the second pass — verify before submitting
The EAS environments currently have no client variables. EXPO_PUBLIC_CONVEX_URL
and EXPO_PUBLIC_CONVEX_SITE_URL are read at BUILD time and .env.local is gitignored, so EAS
does not see it. The build profiles now select development, preview, and
production EAS environments explicitly, but those environments were inspected
on 2026-08-20 and were empty. If they remain empty, the app deliberately opens
the account-free core instead of trapping someone at a sign-in wall, but sign-in,
account sync, analytics, and paid access are absent. That is still not the app
described by its store listing and is a 2.1 completeness risk.
Upload the existing public client values to all three EAS environments before a cloud build. Development/preview use the RevenueCat Test Store key; production uses the StatsHub iOS public SDK key. Then install a TestFlight build and test email, Apple, Google, guest, purchase, and restore.
Every EAS build runs scripts/check-release-env.mjs before dependency install.
For production, testflight, and appstore it rejects missing Convex or
PostHog variables and a RevenueCat Test Store key. Developer-tool visibility is
an explicit client-distribution choice and is not rejected by this guard. Run
bun run check:release-env to apply the same check to the current EAS production
environment without printing credential values.
"Match reminders" notified nobody — now built. The store recorded a
preference and nothing scheduled anything, so Profile showed a master switch and
a count that could only ever read zero; toggleFixtureNotify had no call sites
anywhere in the app, so a fixture could not even be SET to notify. That is 2.3.1
— the app promising what it does not do.
lib/core/match-reminders is the half that was missing: a local notification
fifteen minutes before kick-off, permission asked at the tap rather than on
launch, cancelled when un-toggled. The control is the bell in the fixture
screen's trailing slot, which is where fixture-stage always said it belonged.
Local rather than push, so there is no APNs certificate, no device token and no
remote-notification background mode to explain at review.
Open risks
Third-party sports IP — club crests, competition badges and player headshots come from the API. Apple rejects on 5.2.1/5.2.5 for this. The onboarding photos are Wikimedia-attributed with a credits screen, which is the right instinct; the question is whether the licence covers the crests too.