The Apple-side companion to play-store-submission.md. Follow this in order; each step has a verification beat so you know it landed.
ios/ README:
cd ios && xcodegen generate
xcodebuild build -project Storehop.xcodeproj -scheme Storehop \
-destination 'platform=iOS Simulator,name=iPhone 17' CODE_SIGNING_ALLOWED=NO
The app needs to talk to your production Firebase project (the same one Android uses).
com.storehop.app (matches project.yml’s productBundleIdentifier).GoogleService-Info.plist.ios/Storehop/Resources/.xcodegen generate so Xcode picks it up.Verify: launch the app on the simulator (no -UITestE2E flag). The StorePicker should show the seeded store_lidl, store_continente, store_pingo_doce, store_aldi after Firestore’s first pull (a few seconds).
⚠ Do not commit
GoogleService-Info.plist. It’s gitignored already (matches Android’sgoogle-services.jsontreatment).
# ios/project.yml
settings:
base:
DEVELOPMENT_TEAM: ABC123XYZ4 # ← your Apple Developer Team ID
Find your Team ID at developer.apple.com/account → Membership details → Team ID (10 chars, all caps + digits).
After editing, cd ios && xcodegen generate.
In Xcode → project → Signing & Capabilities tab → Storehop target → enable Automatically manage signing. Xcode will create + download a provisioning profile on first try.
Verify: Product → Archive (or xcodebuild archive from CLI) completes without a code-signing error.
com.storehop.app (must match project.yml exactly)storehop-ios-001 works fine.You now have a placeholder record. The app icon, screenshots, description, and pricing get filled in steps 7 and 8.
premium_lifetime in-app productThis is the one-time $7.99 unlock that gates household-invite creation + CSV export. Mirrors Android’s Play Console product with the same ID (entitlements are platform-isolated; the matching ID is just naming convention).
Storehop Premiumpremium_lifetime (must match exactly — StoreKitManager.swift reads this).Storehop PremiumStorehop PremiumStorehop PremiumStorehop PremiumOne-time unlock for sharing your list with household members + exporting items / categories as CSV. Lifetime, no subscription.Single non-consumable IAP. Unlocks two features: generating household invite codes (otherwise locked) and exporting items/categories as CSV. Inviter-pays model: invitees can join + use a shared household unconditionally.Click Save. Apple needs the product and at least one screenshot for it to become buyable in TestFlight builds.
The IAP can sit in “Ready to Submit” / “Waiting for Review” indefinitely. It ships alongside the app binary at the same review.
PrivacyInfo.xcprivacy)Apple requires this from 2024+. It declares which “required-reason APIs” your app uses.
Create ios/Storehop/Resources/PrivacyInfo.xcprivacy:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeEmailAddress</string>
<key>NSPrivacyCollectedDataTypeLinked</key><true/>
<key>NSPrivacyCollectedDataTypeTracking</key><false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array><string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string></array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeName</string>
<key>NSPrivacyCollectedDataTypeLinked</key><true/>
<key>NSPrivacyCollectedDataTypeTracking</key><false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array><string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string></array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeUserID</string>
<key>NSPrivacyCollectedDataTypeLinked</key><true/>
<key>NSPrivacyCollectedDataTypeTracking</key><false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array><string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string></array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypePhotosorVideos</string>
<key>NSPrivacyCollectedDataTypeLinked</key><true/>
<key>NSPrivacyCollectedDataTypeTracking</key><false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array><string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string></array>
</dict>
</array>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array><string>CA92.1</string></array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array><string>C617.1</string></array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array><string>35F9.1</string></array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array><string>E174.1</string></array>
</dict>
</array>
</dict>
</plist>
The four NSPrivacyAccessedAPITypes reasons:
CA92.1 — UserDefaults for user prefs (theme, locale, sort)C617.1 — File timestamp inspection (CSV import-export bookkeeping)35F9.1 — System boot time (Firebase telemetry baseline)E174.1 — Disk space queries (GRDB checkpointing)Add it to project.yml’s resources: list if it doesn’t auto-pick up:
resources:
- path: Storehop/Resources/PrivacyInfo.xcprivacy
Then xcodegen generate and rebuild. The archive validator will flag a missing manifest with a clear error if anything’s wrong.
cd ios && xcodegen generate
xcodebuild archive \
-project Storehop.xcodeproj \
-scheme Storehop \
-destination 'generic/platform=iOS' \
-archivePath build/Storehop.xcarchive \
-allowProvisioningUpdates
This produces build/Storehop.xcarchive. The -allowProvisioningUpdates flag lets Xcode generate the provisioning profile if Step 2’s “Automatically manage signing” hasn’t been triggered yet.
Easier path (recommended for first submission): open the project in Xcode, select Any iOS Device as the destination, and click Product → Archive. Xcode’s Organizer window walks you through the rest of upload step by step.
xcodebuild -exportArchive \
-archivePath build/Storehop.xcarchive \
-exportOptionsPlist <(cat <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key><string>app-store-connect</string>
<key>teamID</key><string>ABC123XYZ4</string>
<key>uploadSymbols</key><true/>
</dict>
</plist>
EOF
) \
-exportPath build/export
xcrun altool --upload-app --type ios \
--file build/export/Storehop.ipa \
--apiKey YOUR_API_KEY_ID \
--apiIssuer YOUR_ISSUER_ID
(API keys at App Store Connect → Users and Access → Keys.)
Before submitting for review, run the household-share flow on two real devices. This is the only end-to-end check the unit + UI test suite can’t do (no real Firebase + multi-device).
If any of those steps fail, do not submit for review. Either re-test after a fix, or back out the v0.8.1 changes and ship v0.8.0 to Apple.
Back in App Store Connect → your app:
Multi-store shopping listClick App Privacy → Get Started and answer the wizard. Use the same answers as Android’s play-store-submission.md Data Safety section, translated to Apple’s nomenclature:
| Data type | Collected? | Linked to user? | Used for tracking? | Purpose |
|---|---|---|---|---|
| Email Address | Yes (Google sign-in only) | Yes | No | App functionality, account management |
| Name | Yes (Google sign-in only) | Yes | No | App functionality, account management |
| User ID | Yes (Firebase UID) | Yes | No | App functionality |
| Photos | Yes (when user attaches one) | Yes | No | App functionality |
| Crash logs / Diagnostics / Advertising / Analytics | No | — | — | — |
Privacy Policy URL: same one as Play Store. If you haven’t deployed docs/privacy-policy.md via GitHub Pages yet, follow the Play Store doc’s section 1. The Apple-side URL is identical.
MARKETING_VERSION is)shopping list, grocery, multi-store, household, aisle, sage, cross-storeDesignSystemTourTest to generate the 8 light-mode screens, pick the best 3–10. Apple requires at minimum 3 screenshots per device size.AppIcon-1024.png.App Store Connect → your app → App Store tab → version → scroll to Build → select the TestFlight build you uploaded → Save → Add for Review → Submit for Review.
Apple’s review queue is typically 24–48 hours. You’ll be notified by email.
| Rejection | Pre-flight check |
|---|---|
| Privacy manifest missing | Step 5 must be done. |
| In-app purchase not approved | Step 4’s screenshot + description; IAP and binary submit together. |
| App crashes on launch | TestFlight install on a fresh device. Make sure Firebase + signing are right. |
| Sign-in doesn’t work for our reviewer | They use a fresh Apple ID; anonymous-first onboarding handles this. But if they pick the Google sign-in flow, the OAuth consent screen needs to be public-accessible (Firebase Console → Authentication → Sign-in method → Google → enable for production). |
| Demo account needed | Provide one in App Information → Sign-in required → Provide demo account. Easiest: a Google account dedicated to this purpose with Mike-style seeded data. |
| Functionality issues with IAP | Test sandbox purchases beforehand. Apple’s sandbox tester accounts: App Store Connect → Users and Access → Sandbox Testers → +. |
GoogleService-Info.plist in ios/Storehop/Resources/DEVELOPMENT_TEAM filled in project.ymlPrivacyInfo.xcprivacy shipped in the bundlecom.storehop.apppremium_lifetime IAP created + screenshot uploaded