228 lines
13 KiB
Markdown
228 lines
13 KiB
Markdown
# FuelBoard App Store Submission Plan
|
||
|
||
Updated: 2026-09-26 · Target: safe App Store submission.
|
||
Companion to SECURITY.md (code security) — this file is the *review-gate*
|
||
checklist: what must happen, who owns it, and the timeline.
|
||
|
||
## Status: SUBMITTABLE after the checklist below
|
||
|
||
The two 2026-08-11 blockers are RESOLVED by P0 (live chain):
|
||
- ~~LAN relay data source~~ → **app fetches from public HTTPS
|
||
(raw.githubusercontent.com mirror) with a bundled real 8,022-station dump
|
||
as the no-network fallback.** No private IP in the consumer path. Reviewers
|
||
on Apple's network see real data.
|
||
- ~~Always-location risk~~ → **Always is requested only when the user enables
|
||
Alerts** (ProximityMonitor escalates WhenInUse → Always on alert opt-in).
|
||
Onboarding requests WhenInUse only.
|
||
|
||
## StoreKit sandbox: verified working configuration and caveats
|
||
|
||
### Verified configuration
|
||
|
||
The FuelBoard consumable tip products are:
|
||
|
||
- `com.apt.fuelboard.tip099`
|
||
- `com.apt.fuelboard.tip299`
|
||
- `com.apt.fuelboard.tip499`
|
||
|
||
The app requests these exact **Product IDs** with StoreKit 2
|
||
`Product.products(for:)`; the UI names are display labels and are not used for
|
||
lookup. The app bundle ID, App Store Connect app, explicit App ID, development
|
||
team, and Sandbox Apple Account must belong to the same developer account.
|
||
|
||
Two test environments must remain distinct:
|
||
|
||
- **Local StoreKit testing:** select `/Users/apt/workspace/fuelboard/FuelBoardTips.storekit`
|
||
in Xcode, under Product > Scheme > Edit Scheme > Run > Options. This bypasses
|
||
App Store Connect and confirms the app's StoreKit loading and purchase code.
|
||
- **Apple sandbox testing:** select **None** for StoreKit Configuration, then
|
||
use Product > Run on a physical device with the development-signed build and
|
||
a Sandbox Apple Account signed in under Settings > Developer. This exercises
|
||
the real App Store Connect catalog.
|
||
|
||
The local configuration returning all three products does not prove that the
|
||
Apple sandbox catalog is ready. Conversely, a successful local run showing `$`
|
||
does not mean the App Store Connect price is USD: `Product.displayPrice` is
|
||
formatted for the active StoreKit/device storefront. Local StoreKit prices are
|
||
placeholders and do not upload to App Store Connect.
|
||
|
||
### Availability versus pricing
|
||
|
||
App Store Connect has separate controls:
|
||
|
||
- **Availability** controls the storefronts where the IAP is offered. The
|
||
verified FuelBoard configuration uses **United Kingdom only**.
|
||
- **Price schedule** may show generated prices for **175 countries and
|
||
regions**. This is expected and does not make the IAP available in all 175
|
||
storefronts; it is Apple's price conversion schedule.
|
||
|
||
The Sandbox Apple Account's App Store Country or Region must match an enabled
|
||
IAP storefront. After changing the sandbox tester's storefront, sign out and
|
||
back in under Settings > Developer so the storefront change activates.
|
||
|
||
### Important propagation/cache caveat
|
||
|
||
During sandbox diagnosis, `Product.products(for:)` completed successfully but
|
||
returned zero products with the IAP availability set to UK. Temporarily
|
||
changing one product's availability to all regions caused it to appear in the
|
||
sandbox catalog; changing it back to UK left it working. This indicates an
|
||
Apple-side catalog refresh or propagation/cache issue, not a change required in
|
||
the StoreKit code. Apple documents that product metadata changes can take up to
|
||
approximately one hour to appear in sandbox.
|
||
|
||
If an otherwise valid product returns zero products:
|
||
|
||
1. Confirm the Sandbox Apple Account belongs to the same developer team.
|
||
2. Confirm its storefront is an enabled availability region.
|
||
3. Sign out and back in to the sandbox account on the device.
|
||
4. Delete/reinstall the development build and retry with StoreKit Configuration
|
||
set to **None**.
|
||
5. If necessary, temporarily broaden one product's availability to all
|
||
regions, allow propagation, verify the product, then restore the intended
|
||
availability and allow propagation again.
|
||
|
||
Record whether the diagnostic result is **request failed**, **completed with
|
||
zero products**, or **a partial product set**. These are different failure
|
||
classes. Do not add fallback prices or change product lookup to display names.
|
||
|
||
Sandbox testing does not require prior App Review approval of the IAP. It does
|
||
require active developer membership, an active Paid Applications Agreement,
|
||
complete banking and tax information, valid product metadata and pricing, the
|
||
correct app/team association, and a matching Sandbox Apple Account.
|
||
|
||
## Hard blockers (must happen before ANY submission)
|
||
|
||
0. **[USER] Paid Applications Agreement in effect** — REQUIRED for paid IAP
|
||
configuration and sandbox testing. App Store Connect →
|
||
Business > Paid Applications Agreement → View and Agree. **This is the most
|
||
common cause of a 2.1(b) "unable to purchase item / error message" rejection
|
||
— without it every sandbox purchase fails.** (REJECTED once for this:
|
||
review date 2026-09-25, version 1.0 (21), iPad Air 11″ iPadOS 27.)
|
||
**Status 2026-09-25: agreement clicked Agree but shows "Pending user info" —
|
||
this is NOT in effect yet.** Complete the Banking (account + sort code/IBAN)
|
||
and Tax (W-8/W-9/UK tax ref, e.g. sole-trader UTR) sections under Business >
|
||
Payments & Tax until the agreement shows "Active" and every tab is green.
|
||
Only then do sandbox IAPs start working.
|
||
0. **[USER] Tip IAPs configured and available to the sandbox** — the three
|
||
consumables may exist but must ALSO have complete metadata, a current price,
|
||
and an enabled storefront availability. Prior App Review approval or
|
||
`Cleared for Sale` status is **not required for sandbox product discovery**.
|
||
They must be backed by the **In-App Purchase capability on the App ID**
|
||
`com.apt.fuelboard` (developer portal → Identifiers → App ID capability).
|
||
3-tier tip code is already correct (retry lookup `7cb23e0`); do NOT churn
|
||
SettingsView.swift. Test product discovery and purchase with a Sandbox
|
||
Apple Account first: Settings → Support FuelBoard.
|
||
|
||
1. **[USER] Apple Developer Program membership** — the app has never been
|
||
signed (all builds are `CODE_SIGNING_ALLOWED=NO` + ad-hoc re-sign for
|
||
SideStore). Requires a paid membership ($99/yr, up to 48 h to approve) and
|
||
a developer account.
|
||
2. **[USER] Register App ID `com.apt.fuelboard`** in the developer portal,
|
||
with the **App Group `group.com.apt.fuelboard`** capability AND the
|
||
**In-App Purchase** capability enabled (the widget + app-group cache
|
||
depend on the group; the tip jar needs IAP).
|
||
3. ~~[ME] `PrivacyInfo.xcprivacy`~~ — **DONE 2026-08-16** (app + widget
|
||
manifests added; UserDefaults `1C8F.1`, Location + Diagnostics declared).
|
||
4. **[ME] Real Distribution signing** — Distribution certificate + App Store
|
||
provisioning profile (or Xcode automatic signing once the account is in
|
||
Xcode); then `xcodebuild archive` (no more `CODE_SIGNING_ALLOWED=NO`).
|
||
5. **[USER] Privacy policy URL** in App Store Connect — mandatory; the app
|
||
collects location. Policy text is now in `PRIVACY.md`; publish it via the
|
||
FuelBoard marketing site or GitHub Pages and paste that public URL into App
|
||
Store Connect.
|
||
|
||
## Required tasks — owner [ME] (technical, do after account exists)
|
||
|
||
- [x] **PrivacyInfo.xcprivacy** — app + widget manifests added (2026-08-16):
|
||
UserDefaults reason `1C8F.1`; app also declares Location (linked, not
|
||
tracking) + Diagnostics (not linked, not tracking — the dev beacon)
|
||
- [ ] Distribution archive + TestFlight upload (needs Apple account)
|
||
- [x] **Version bump** → `1.0.1` (build 2), app + widget plists
|
||
- [x] **OGL attribution** — Settings → Data source footer now reads "Prices
|
||
use the UK Fuel Finder dataset (Open Government Licence) and refresh
|
||
up to twice a day." (2.3.1 hygiene)
|
||
- [x] **Out-of-UK empty state** — "No X stations within N miles" + UK-only
|
||
explainer + "Browse all UK stations" button (switches to Closest)
|
||
- [ ] ~~Gate dev surface behind #if DEBUG~~ **DECLINED — kept in Release on
|
||
purpose**: the user's home-testing installs the Release IPA and toggles
|
||
`fuelboard.relayFallback` via Settings → Debug (revealed only by tapping
|
||
About → Version ×5 — invisible to reviewers; no credentials involved).
|
||
Gating it out of Release would break that workflow for zero review gain.
|
||
- [ ] Final pass: `swift test` green (95 tests), archive smoke test on device
|
||
- [ ] ~~Capture screenshots from simulator~~ **RAW CAPTURES DONE (2026-08-16)** —
|
||
native 6.9″ 1320×2868 PNGs of all four tabs (Stations, Favourites,
|
||
Alerts, Settings) at `/tmp/fuelboard-shots/asc/`; assembled into ASC
|
||
frames by the user in the yuzu-hub App Store Screenshot Generator
|
||
(https://yuzu-hub.github.io/appscreen/). Screenshot harness: launch
|
||
args `-skipOnboarding` / `-tab stations|favourites|alerts|settings`
|
||
(ContentView, same pattern as `-showKeySheet`).
|
||
|
||
## Required tasks — owner [USER] (App Store Connect, cannot be done by me)
|
||
|
||
- [ ] Apple Developer Program membership + App ID (blockers #1–#2)
|
||
- [ ] **Tip jar: create the three consumable IAPs** in App Store Connect →
|
||
In-App Purchases (all type **Consumable**, exact product IDs, UK price
|
||
tier; tip jars are an accepted App Review pattern):
|
||
- `com.apt.fuelboard.tip099` — **£0.99** — "Splash & Dash" (Just enough to keep things moving.)
|
||
- `com.apt.fuelboard.tip299` — **£2.99** — "Half a Tank" (A generous top-up for development.)
|
||
- `com.apt.fuelboard.tip499` — **£4.99** — "Fill 'Er Up" (Keeping the app on the road.)
|
||
App-side is ready (`41e4c91`): Settings → Support FuelBoard shows all
|
||
three tiers with per-tier prices; sideloaded builds gracefully show
|
||
"not available in this build yet" until the products exist.
|
||
- [ ] App Store Connect app record: name, bundle ID `com.apt.fuelboard`,
|
||
category (Utilities), pricing (Free), availability (UK first?)
|
||
- [ ] Description, keywords, support URL, marketing URL, privacy policy URL
|
||
- [ ] Privacy nutrition labels questionnaire (location: yes, linked to user
|
||
for alerts/nearby; diagnostics: yes — the widget-diag beacon, not
|
||
linked, not used for tracking; no ATT prompt needed)
|
||
- [ ] Age rating questionnaire (4+; no objectionable content)
|
||
- [ ] Export compliance: HTTPS-only + keychain → standard encryption
|
||
exemption (5A992a), no documentation needed
|
||
- [ ] Approve the TestFlight build and do a real-device pass before submit
|
||
- [ ] Final "Submit for Review" click + stand by to answer review questions
|
||
|
||
## Review notes (copy-paste into App Store Connect)
|
||
|
||
> FuelBoard shows the cheapest petrol, diesel, and premium fuel near the user
|
||
> using the UK government Fuel Finder dataset (Open Government Licence).
|
||
> Prices are downloaded from the internet up to twice a day; the app filters
|
||
> 5/10/15-mile radii on-device and works offline from a bundled snapshot.
|
||
> Location is used to (a) rank stations by distance and (b) power the
|
||
> optional Alerts feature: when the user enables alerts, the app geofences
|
||
> the cheapest station in their radius and sends a notification when they
|
||
> approach it. Always-location is requested only when the user enables
|
||
> alerts. The app also provides a home-screen widget showing the current
|
||
> cheapest station nearby, and a price-history Trends chart for starred
|
||
> stations. All data is UK-wide (Fuel Finder covers England, Scotland, Wales
|
||
> and Northern Ireland); users outside the UK see an empty state explaining
|
||
> this.
|
||
|
||
## Review risks (know before submitting)
|
||
|
||
- **Always-location (5.1.1)** — the #1 question. The feature is visible
|
||
(Alerts tab) and opt-in; the review notes above say so explicitly.
|
||
- **Reviewer outside the UK** — empty radius; mitigated by the empty-state
|
||
task above + review notes.
|
||
- **"Cheapest" claims (2.3.1)** — mitigated by the OGL/footnote task.
|
||
- **Live Activities** — reviewers may ask why; answer: price alerts at a
|
||
glance. It is declared in the plist already.
|
||
- **Brand logos (Shell, BP, Tesco…)** — Simple Icons (CC0) + Wikimedia,
|
||
nominative use (cf. GasBuddy); low risk, keep sources documented.
|
||
- **GitHub raw as data source** — public data, no credentials; bundled dump
|
||
covers any regional GitHub unreachability.
|
||
|
||
## Timeline (best case)
|
||
|
||
| Day | Owner | Milestone |
|
||
|-----|-------|-----------|
|
||
| 0–2 | USER | Developer Program enrollment + App ID + app group capability |
|
||
| 2–3 | ME | Signing (automatic), PrivacyInfo.xcprivacy, attribution, empty state, version bump |
|
||
| 3–4 | ME | Archive → TestFlight; screenshots captured |
|
||
| 4–5 | USER | TestFlight install + real-device pass (onboarding, alerts, widget, offline) |
|
||
| 5–6 | USER | App Store Connect metadata + labels + age rating + privacy URL |
|
||
| 6–7 | USER/ME | Submit for review |
|
||
| 7–14 | Apple | Review (typically 1–3 days, can be a week); answer questions same-day |
|
||
|
||
Rollback note: nothing about submission changes the repo's rollback posture
|
||
(`main` history is intact; release builds are tagged per submission).
|