{"id":57,"date":"2026-01-14T16:55:43","date_gmt":"2026-01-14T16:55:43","guid":{"rendered":"https:\/\/balamurali.in\/blog\/?p=57"},"modified":"2026-02-23T14:25:57","modified_gmt":"2026-02-23T14:25:57","slug":"how-i-published-my-first-ios-app-a-guide-to-avoiding-app-store-rejections-case-study-paisaflow","status":"publish","type":"post","link":"https:\/\/balamurali.in\/blog\/learn-with-me\/how-i-published-my-first-ios-app-a-guide-to-avoiding-app-store-rejections-case-study-paisaflow\/","title":{"rendered":"How I Published My First iOS App: A Guide to Avoiding App Store Rejections (Case Study: PaisaFlow)"},"content":{"rendered":"\n<p>Building an iOS app is hard work, but submitting it to the App Store can feel like a whole different battle. Recently, I went through the process of publishing <strong>PaisaFlow<\/strong>, a privacy-focused expense tracker tailored for the Indian market.<\/p>\n\n\n\n<p>While the coding part was straightforward, the submission process threw several curveballs\u2014from bundle ID confusion to screenshot automation and encryption compliance.<\/p>\n\n\n\n<p>If you are about to hit that &#8220;Submit for Review&#8221; button, read this first. Here is a breakdown of the specific issues I faced and how you can avoid them to get your app approved faster.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. The Bundle ID Trap: App vs. Extensions<\/h2>\n\n\n\n<p>One of the first errors I encountered happened during the archiving process. My project includes the main app and a Widget extension. When I tried to upload the build, I saw two different bundle IDs listed (<code>com.paisaflow.app<\/code> and <code>com.paisaflow.app.widget<\/code>), which made me worry I had to register the widget separately in App Store Connect.<\/p>\n\n\n\n<p><strong>The Mistake:<\/strong><br>I initially tried to archive the Widget Extension scheme, thinking I needed to upload it individually.<\/p>\n\n\n\n<p><strong>The Fix:<\/strong><br>You only need to list the <strong>Main App Bundle ID<\/strong> in App Store Connect.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>In Xcode, ensure your <strong>Scheme<\/strong> is set to the main application (e.g., <code>BudgetApp<\/code>), <em>not<\/em> the WidgetExtension.<\/li>\n\n\n\n<li>When you archive the main app, Xcode automatically embeds the widget extension inside it.<\/li>\n\n\n\n<li>You do not need to register the widget&#8217;s bundle ID separately in the App Store Connect portal.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">2. Validating &#8220;Privacy-First&#8221; Features (Face ID)<\/h2>\n\n\n\n<p>PaisaFlow uses Face ID to lock the Credit Card Vault. Even though the code was working perfectly in the simulator, I risked an immediate crash on real devices.<\/p>\n\n\n\n<p><strong>The Issue:<\/strong><br>I missed a critical key in the <code>Info.plist<\/code>. Without this key, the OS terminates your app immediately upon a Face ID request, which is an instant rejection during App Review.<\/p>\n\n\n\n<p><strong>The Fix:<\/strong><br>If you use Face ID (via the <code>LocalAuthentication<\/code> framework), you <em>must<\/em> add the <code>NSFaceIDUsageDescription<\/code> key to your <code>Info.plist<\/code>.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Key:<\/strong> <code>NSFaceIDUsageDescription<\/code><\/li>\n\n\n\n<li><strong>Value:<\/strong> A clear explanation of <em>why<\/em> you need it.\n<ul class=\"wp-block-list\">\n<li><em>Example:<\/em> &#8220;PaisaFlow uses Face ID to securely unlock your Credit Card Vault.&#8221;<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">3. The Screenshot Struggle: Automation is Key<\/h2>\n\n\n\n<p>App Store Connect requires screenshots for multiple device sizes (6.5&#8243; and 6.7&#8243;). Taking these manually for every screen, in both Light and Dark modes, is a nightmare.<\/p>\n\n\n\n<p><strong>My Approach:<\/strong><br>Instead of manual capture, I set up an automated UI Test in Xcode (<code>XCUITest<\/code>).<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Dummy Data Seeder:<\/strong> I created a <code>DummyDataSeeder.swift<\/code> that runs only when a specific launch argument is passed (<code>-UITest_SeedDummyData<\/code>). This populates the app with realistic data (e.g., &#8220;HDFC Infinia&#8221; cards, &#8220;Swiggy&#8221; transactions) so the screenshots don&#8217;t look empty.<\/li>\n\n\n\n<li><strong>Keyboard Issues:<\/strong> A common mistake is taking screenshots with the keyboard covering half the UI. In my tests, I added code to explicitly dismiss the keyboard before the test snapped the picture.<\/li>\n\n\n\n<li><strong>Light Mode Enforcement:<\/strong> I realized halfway through that my simulator was stuck in Dark Mode. I added a flag to force <code>ThemeManager.shared.currentTheme = .light<\/code> during tests to ensure consistent marketing images.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">4. Export Compliance &amp; Encryption<\/h2>\n\n\n\n<p>When submitting, Apple asks a scary question: <em>&#8220;Does your app use encryption?&#8221;<\/em><\/p>\n\n\n\n<p><strong>The Reality:<\/strong><br>PaisaFlow uses SwiftData, secure Keychain storage, and HTTPS. Technically, this <em>is<\/em> encryption.<\/p>\n\n\n\n<p><strong>The Answer:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Question:<\/strong> Does your app use encryption? <strong>Yes.<\/strong><\/li>\n\n\n\n<li><strong>Exemption:<\/strong> Does your app qualify for exemptions? <strong>Yes.<\/strong><\/li>\n\n\n\n<li><strong>Reason:<\/strong> Select <strong>&#8220;Standard encryption algorithms instead of, or in addition to, using or accessing the encryption within Apple&#8217;s operating system.&#8221;<\/strong><\/li>\n<\/ul>\n\n\n\n<p>Because we rely on standard iOS frameworks (Keychain, HTTPS) rather than custom proprietary encryption, we are exempt from submitting heavy export documentation to the US government.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">5. Privacy Policy &amp; Support URL (The &#8220;No Website&#8221; Problem)<\/h2>\n\n\n\n<p>You cannot submit an app without a Privacy Policy URL and a Support URL. Since PaisaFlow is an offline-only app, I didn&#8217;t have a dedicated website.<\/p>\n\n\n\n<p><strong>The Quick Fix:<\/strong><br>You don&#8217;t need to buy a domain or build a website just to launch.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Notion:<\/strong> I created two simple public Notion pages\u2014one for Privacy Policy and one for Support.<\/li>\n\n\n\n<li><strong>Content:<\/strong> The policy simply states: <em>&#8220;PaisaFlow does not collect, store, or transmit any user data. All financial information is stored locally on your device.&#8221;<\/em><\/li>\n\n\n\n<li><strong>Submission:<\/strong> I used these public Notion links in App Store Connect. Apple accepted them without issue.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">6. App Privacy Labels (The Questionnaire)<\/h2>\n\n\n\n<p>Filling out the &#8220;App Privacy&#8221; section in App Store Connect can be confusing for local-only apps.<\/p>\n\n\n\n<p><strong>The Confusion:<\/strong><br>The questionnaire asks if you collect &#8220;Contact Info&#8221; or &#8220;Financial Info.&#8221; Since my app lets users input this data, do I say &#8220;Yes&#8221;?<\/p>\n\n\n\n<p><strong>The Solution:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Technically, the data never leaves the device, so you <em>could<\/em> say &#8220;No Data Collected&#8221;.<\/li>\n\n\n\n<li>However, for transparency, I selected <strong>&#8220;Financial Info&#8221;<\/strong> and <strong>&#8220;Contacts&#8221;<\/strong>.<\/li>\n\n\n\n<li><strong>Crucial Step:<\/strong> For every data type, I answered <strong>&#8220;No&#8221;<\/strong> to:\n<ul class=\"wp-block-list\">\n<li>Is this data used to track the user?<\/li>\n\n\n\n<li>Is this data linked to the user&#8217;s identity?<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p>Since there is no user account or server, the data is not linked to an identity.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">7. The iPad Screenshot Block<\/h2>\n\n\n\n<p>When I first tried to submit, App Store Connect demanded iPad screenshots. I hadn&#8217;t optimized the UI for iPad yet.<\/p>\n\n\n\n<p><strong>The Fix:<\/strong><br>I changed the <code>TARGETED_DEVICE_FAMILY<\/code> in Xcode Build Settings from <code>1,2<\/code> (iPhone, iPad) to just <code>1<\/code> (iPhone). This removed the requirement for iPad screenshots and allowed me to submit an iPhone-only app.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Conclusion<\/h3>\n\n\n\n<p>Submitting an app is less about code and more about configuration and compliance. By automating my screenshots and understanding the nuances of Apple&#8217;s privacy questionnaire, I was able to move from a &#8220;Processing&#8221; build to &#8220;Ready for Sale&#8221; smoothly.<\/p>\n\n\n\n<p><strong>PaisaFlow<\/strong> is now live. If you are looking for a privacy-first way to track expenses, give it a try!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Building an iOS app is hard work, but submitting it to the App Store can feel like a whole different battle. Recently, I went through the process of publishing PaisaFlow,&#8230;<\/p>\n","protected":false},"author":1,"featured_media":146,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[8],"tags":[],"class_list":["post-57","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-learn-with-me"],"jetpack_featured_media_url":"https:\/\/balamurali.in\/blog\/wp-content\/uploads\/2026\/02\/ios_app_publishing.png","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/posts\/57","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/comments?post=57"}],"version-history":[{"count":1,"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/posts\/57\/revisions"}],"predecessor-version":[{"id":59,"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/posts\/57\/revisions\/59"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/media\/146"}],"wp:attachment":[{"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/media?parent=57"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/categories?post=57"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/tags?post=57"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}