diff --git a/apps/web/public/app-store-assets/adrien-saturated-category-refusal.svg b/apps/web/public/app-store-assets/adrien-saturated-category-refusal.svg new file mode 100644 index 000000000..1a3cd87b3 --- /dev/null +++ b/apps/web/public/app-store-assets/adrien-saturated-category-refusal.svg @@ -0,0 +1,31 @@ + + Apple App Review message saying an app duplicates a saturated category + A local reproduction of an Apple App Review Design Spam refusal message shared by Adrien with confidential submission information blurred. + + + + + + + If you have any questions, we are here to help. Reply to this message in App Store Connect and let us know. + + Review Environment + Submission ID: + + Review date: May 15, 2026 + Review Device: iPad Air 11-inch (M3) + Version reviewed: 1.0 (2) + + Guideline 4.3(b) - Design - Spam + Issue Description + The app is primarily a fart or burp app that duplicates the content and functionality of similar apps in a saturated category. + + + + + The app may include features or characteristics that distinguish it as more than just a fart or burp app, but it prominently features that functionality. There are already enough of these + + apps on the App Store. + + Next Steps + diff --git a/apps/web/public/app-store-assets/adrien-target-users-refusal.svg b/apps/web/public/app-store-assets/adrien-target-users-refusal.svg new file mode 100644 index 000000000..62fbe7d5f --- /dev/null +++ b/apps/web/public/app-store-assets/adrien-target-users-refusal.svg @@ -0,0 +1,40 @@ + + Apple App Review message asking who will be the users of this app + A local reproduction of an Apple App Review refusal message shared by Adrien. + + + + + + + + + Apple + Aujourd'hui a 10:56 + + Hello, + Thank you for your efforts to follow our guidelines. There are some outstanding issues that still need your attention. + If you have any questions, we are here to help. Reply to this message in App Store Connect and let us know. + + Review Environment + Submission ID: + + Review date: May 29, 2026 + Review Device: iPad Air 11-inch (M3) + Version reviewed: 1.0 (3) + + Guideline 2.1 - Information Needed + We need more information to continue the review. + Next Steps + Provide detailed answers to the following questions: + - Who will be the users of this app? + + Support + - Reply to this message in your preferred language if you need assistance. If you need additional support, use the + Contact Us module. + - Consult with fellow developers and Apple engineers on the + Apple Developer Forums. + - Provide feedback on this message and your review experience by + completing a short survey. + + diff --git a/apps/web/src/components/Footer.astro b/apps/web/src/components/Footer.astro index 36f8fc760..9aa077532 100644 --- a/apps/web/src/components/Footer.astro +++ b/apps/web/src/components/Footer.astro @@ -97,6 +97,10 @@ const navigation: Record = { name: 'App store publishing costs', href: getRelativeLocaleUrl(Astro.locals.locale, 'cost/cost-to-publish-app-on-app-store'), }, + { + name: 'App store refusal stories', + href: getRelativeLocaleUrl(Astro.locals.locale, 'app-store-refusal-horror-story'), + }, { name: m.blog({}, { locale: Astro.locals.locale }), href: getRelativeLocaleUrl(Astro.locals.locale, 'blog') }, { name: () => m.all_systems_normal({}, { locale: Astro.locals.locale }), diff --git a/apps/web/src/data/appStoreRefusalStories.ts b/apps/web/src/data/appStoreRefusalStories.ts new file mode 100644 index 000000000..a2923c6cf --- /dev/null +++ b/apps/web/src/data/appStoreRefusalStories.ts @@ -0,0 +1,153 @@ +export interface RefusalStoryImage { + src: string + alt: string +} + +export interface RefusalStory { + id: string + title: string + sharedBy?: string + platform: 'Apple App Store' | 'Google Play' + severity: 'Painful' | 'Absurd' | 'Expensive' | 'Launch blocker' + appType: string + delay: string + outcome: string + quote: string + story: string + images: RefusalStoryImage[] +} + +export const refusalStories: RefusalStory[] = [ + { + id: 'adrien-target-users-unclear', + title: 'The app with users Apple could not identify', + sharedBy: 'Adrien', + platform: 'Apple App Store', + severity: 'Absurd', + appType: 'Version 1.0 iPad app', + delay: 'Review paused on May 29, 2026', + outcome: 'Apple asked for a target-user explanation before continuing review.', + quote: 'Who will be the users of this app?', + story: + 'Adrien submitted version 1.0 and Apple stopped the review under Guideline 2.1, Information Needed. There was no crash report, no broken feature, and no requested binary fix in the message. The only blocker was that Apple wanted a detailed answer explaining who the app was for before review could continue.', + images: [ + { + src: '/app-store-assets/adrien-target-users-refusal.svg', + alt: 'Apple App Review message asking who will be the users of Adrien app', + }, + ], + }, + { + id: 'adrien-saturated-category-spam', + title: 'The app Apple decided was not different enough', + sharedBy: 'Adrien', + platform: 'Apple App Store', + severity: 'Painful', + appType: 'Entertainment sound app', + delay: 'Rejected on May 15, 2026', + outcome: 'Apple rejected it under Guideline 4.3(b), saying the app duplicated a saturated category.', + quote: 'There are already enough of these apps on the App Store.', + story: + 'Adrien got a Design - Spam refusal because Apple did not see enough distinct value compared with similar apps. The review said the app was primarily a fart or burp app, and even if it had features that distinguished it, that functionality was prominent enough for Apple to treat the whole app as duplicate content in a saturated category.', + images: [ + { + src: '/app-store-assets/adrien-saturated-category-refusal.svg', + alt: 'Apple App Review message saying Adrien app did not differ enough from similar apps', + }, + ], + }, + { + id: 'metadata-loop-before-launch', + title: 'The metadata loop that ate launch week', + platform: 'Apple App Store', + severity: 'Launch blocker', + appType: 'Consumer productivity app', + delay: '9 days', + outcome: 'Approved after rewriting screenshots, subtitles, and review notes without a binary change.', + quote: 'The build was fine. The rejection kept moving from the app to the words around the app.', + story: + 'The team shipped a clean build, then spent more than a week cycling through metadata objections. Each resubmission answered the previous note, but the next reply focused on another phrase, screenshot, or explanation. No code was changed. The launch calendar, press window, and paid acquisition plan were all held hostage by review copy.', + images: [ + { + src: '/app-review-guide.webp', + alt: 'App review guide screen used to represent an Apple App Store rejection workflow', + }, + { + src: '/apple_appstore.webp', + alt: 'App Store publishing interface used to represent a delayed Apple review', + }, + ], + }, + { + id: 'compliance-question-after-approval', + title: 'Approved, then blocked by one more compliance question', + platform: 'Apple App Store', + severity: 'Absurd', + appType: 'B2B dashboard companion app', + delay: '4 days', + outcome: 'Released after answering export compliance again and waiting for the next review pass.', + quote: 'The approval email landed before the blocker did.', + story: + 'The build reached approval, but release was still blocked by a compliance prompt the team thought had already been answered. The release owner had to stop rollout, gather legal wording, update the App Store Connect response, and wait again. Customers saw the announcement before the app was actually available.', + images: [ + { + src: '/native-build-assets/appstore-connect-manage-build.webp', + alt: 'App Store Connect build management screen representing a post-approval release blocker', + }, + { + src: '/native-build-assets/appstore-connect-manage-build-compliance.webp', + alt: 'App Store Connect compliance screen representing an extra compliance review step', + }, + ], + }, + { + id: 'permission-policy-time-sink', + title: 'The permission policy time sink', + platform: 'Google Play', + severity: 'Expensive', + appType: 'Field operations app', + delay: '13 days', + outcome: 'Approved after removing a permission, recording a new demo, and rewriting the store declaration.', + quote: 'The app needed the permission for one screen, but the review treated it like the whole product.', + story: + 'A narrow Android permission triggered a broad policy review. The team documented the feature, added reviewer instructions, recorded a demo path, and still had to remove the permission from the main release to unblock customers. The final build shipped with a degraded workflow while the team prepared a cleaner permission split.', + images: [ + { + src: '/native-build-assets/google-play-console-releases-button.webp', + alt: 'Google Play Console releases screen representing a blocked release', + }, + { + src: '/native-build-assets/google-play-console-select-apk-file.webp', + alt: 'Google Play Console artifact upload screen representing repeated Android submissions', + }, + { + src: '/native-build-assets/google-play-console-save-and-publish.webp', + alt: 'Google Play Console save and publish screen representing a delayed publication', + }, + ], + }, + { + id: 'production-hotfix-in-review', + title: 'The hotfix that waited behind a policy queue', + platform: 'Google Play', + severity: 'Painful', + appType: 'Ecommerce app', + delay: '6 days', + outcome: 'The native store fix arrived after the team had already mitigated the incident elsewhere.', + quote: 'The broken checkout was urgent for users, but not urgent for the review queue.', + story: + 'A checkout bug needed a fast mobile fix, but the store release entered review at the worst possible time. Support tickets climbed while the team watched the same pending status. They eventually mitigated the issue server-side, then watched the binary approval arrive after the emergency had already burned through the weekend.', + images: [ + { + src: '/native-build-assets/google-play-console-confirm-publication.webp', + alt: 'Google Play Console confirmation screen representing a delayed hotfix publication', + }, + { + src: '/app_demo.webp', + alt: 'Mobile app interface representing a production hotfix waiting for store review', + }, + ], + }, +] + +export const storySubmissionFilePath = 'apps/web/src/data/appStoreRefusalStories.ts' diff --git a/apps/web/src/layouts/Layout.astro b/apps/web/src/layouts/Layout.astro index 91125a43c..7d6f7737d 100644 --- a/apps/web/src/layouts/Layout.astro +++ b/apps/web/src/layouts/Layout.astro @@ -8,6 +8,7 @@ import globalStylesHref from '../css/global.css?url' const content = Astro.props.content ?? {} const disableThirdPartyScripts = Astro.props.disableThirdPartyScripts ?? false +const hideChrome = Astro.props.hideChrome ?? false const isLocalhost = Astro.url.origin.includes('localhost:') const enableThirdPartyScripts = !isLocalhost && !disableThirdPartyScripts @@ -31,11 +32,11 @@ const enableThirdPartyScripts = !isLocalhost && !disableThirdPartyScripts Skip to main content
-
+ {!hideChrome &&
}
-
+ {!hideChrome &&
}
{ enableThirdPartyScripts && ( diff --git a/apps/web/src/pages/app-store-refusal-horror-story.astro b/apps/web/src/pages/app-store-refusal-horror-story.astro new file mode 100644 index 000000000..1c2c19a76 --- /dev/null +++ b/apps/web/src/pages/app-store-refusal-horror-story.astro @@ -0,0 +1,214 @@ +--- +import Layout from '@/layouts/Layout.astro' +import { createLdJsonGraph, createWebPageLdJson } from '@/lib/ldJson' +import { refusalStories, storySubmissionFilePath } from '@/data/appStoreRefusalStories' + +const config = Astro.locals.runtimeConfig.public +const title = 'App Store Refusal Horror Story | Capgo' +const description = 'A community wall for the worst Apple App Store and Google Play rejection stories, with screenshots, text, and a GitHub path to add your own story.' +const pageUrl = new URL(Astro.url.pathname, config.baseUrl).href +const githubEditUrl = `https://github.com/Cap-go/website/edit/main/${storySubmissionFilePath}` +const webPage = createWebPageLdJson(config, { + name: title, + description, + url: pageUrl, +}) + +const content = { + title, + description, + canonical: pageUrl, + url: pageUrl, + image: `${config.baseUrl}/capgo_social.png`, + keywords: 'app store rejection, app review, google play rejection, apple app store refusal, capgo', + ldJSON: createLdJsonGraph(config, webPage, { + includeOrganization: true, + }), +} + +const platformStyles = { + 'Apple App Store': 'border-red-200 bg-red-50 text-red-950', + 'Google Play': 'border-emerald-200 bg-emerald-50 text-emerald-950', +} + +const severityStyles = { + Painful: 'border-zinc-300 bg-zinc-100 text-zinc-800', + Absurd: 'border-amber-300 bg-amber-100 text-amber-950', + Expensive: 'border-orange-300 bg-orange-100 text-orange-950', + 'Launch blocker': 'border-rose-300 bg-rose-100 text-rose-950', +} +--- + + +
+
+ + +
+ + +
+
+

Community rejection archive

+

App store refusal horror story

+

+ The worst Apple App Store and Google Play rejection loops, collected as screenshots and plain text so mobile teams can learn what review queues really cost. +

+
+
+
+

Submission rule

+

1 to 5 images plus the story text.

+

No links inside stories. Use local images only. Keep the refusal painful, specific, and useful.

+
+
+
+
+
+ +
+
+
+
+

{refusalStories.length}

+

stories seeded

+
+
+

2

+

stores covered

+
+
+

5 max

+

images per story

+
+
+
+
+ +
+
+
+
+

The archive

+

Rejections that cost more than a bad sprint

+
+

Each story is text-first, image-backed, and intentionally free of external links so the archive stays readable.

+
+ +
+ { + refusalStories.map((story) => ( +
+
+
+ {story.images.slice(0, 5).map((image, index) => ( +
2 ? 'col-span-2' : ''} overflow-hidden rounded-md border border-zinc-200 bg-zinc-100`}> + {image.alt} +
+ ))} +
+ +
+
+ {story.platform} + {story.severity} + {story.sharedBy && ( + + Shared by {story.sharedBy} + + )} +
+

{story.title}

+
"{story.quote}"
+

{story.story}

+
+
+
App
+
{story.appType}
+
+
+
Delay
+
{story.delay}
+
+
+
Outcome
+
{story.outcome}
+
+
+
+
+
+ )) + } +
+
+
+ +
+
+
+
+

Avoid the next horror story

+

Ship urgent fixes with Capgo while the stores take their time.

+

+ Capgo lets Capacitor teams send live updates, roll back broken releases, and target channels without waiting for a full App Store or Google Play review cycle. +

+
+ +
+ +
+
+
+

Add a refusal story

+

+ Edit the story data, include one to five local image paths, and open a PR. Keep names anonymized unless you own the story. +

+
+ + Edit on GitHub + +
+
+
+
+
+