diff --git a/.ai/docs/store-launch-notes.md b/.ai/docs/store-launch-notes.md new file mode 100644 index 00000000..873f279b --- /dev/null +++ b/.ai/docs/store-launch-notes.md @@ -0,0 +1,117 @@ +# Store launch — notes towards a reusable flow + +Opened 2026-09-16 on Davide's ask: *"I want you to take notes so we can create a reusable flow to +publish other apps. It could be some scripts, some CI automation, or even only some skills. Remember +the thing that I struggle with and that we need to double-check."* + +The checklist being worked is [`store-launch.md`](store-launch.md). This file is the *residue* — what +the next app should not have to rediscover. **What it becomes is decided when Oltre is live**, not +now: a global skill, a script in `.github/scripts/`, a CI job, or nothing. + +Three kinds of entry, and each one says which it is: + +- **TRAP** — something that looks fine and is not. A wrong default, an irreversible choice, a gate + that fires late. +- **FRICTION** — something that cost time and could be automated or written down once. +- **CANDIDATE** — a concrete thing to build: a script, a workflow, a skill, a template. + +--- + +## Traps + +- **TRAP — Play App Signing's default mints a new key.** Choosing it, rather than *"Provide a copy of + your app signing key"*, changes the SHA-1 the app is signed with. Any Google Sign-In Android OAuth + client is bound to package + SHA-1, so every Play install would fail to sign in, silently, with the + app looking fine. Irreversible in the sense that matters: the fix is registering a third OAuth + client and shipping a new release. *(Recorded in Oltre's `identity-provisioning.md` before we + started — the note is what made it cheap. That is itself the argument for this file.)* +- **TRAP — the 12-testers rule is decided by an account type chosen once, at registration.** A + personal Play account created on or after 2023-11-13 needs 12 testers opted in **continuously for + 14 days** before production access; an organization account is exempt but needs a legal entity, a + D-U-N-S number and 2–4 weeks of verification. It is a calendar decision made in a dialog most + people click through in five seconds. +- **TRAP — an app that requires sign-in pulls in three obligations at once**, and none of them is + obvious from the code: Apple's mandatory Sign-In Information for the reviewer (2.1(a)), in-app + account deletion (5.1.1(v)), and Play's **web** account-deletion URL on the Data Safety form. A + login screen is not a feature, it is a compliance surface. +- **TRAP — typed names shown to strangers trigger Apple guideline 1.2** (filter, report, block, + published contact) and Play's UGC policy. The trigger is the first review round that carries the + feature, so the cost lands a release after the feature ships. +- **TRAP — Play takes an AAB, the GitHub-release path builds an APK.** A publishing pipeline that + only ever produced APKs has a hole in it that only shows up at upload time. +- **TRAP — a generated privacy policy describes an app that has analytics and ads.** Publishing one + that overstates collection is itself a false privacy label. +- **TRAP — the Apple agreements gate is invisible until submission.** TestFlight works with an + expired or unaccepted Paid/Free Apps agreement; App Store distribution does not. Apple re-issues + the Program License Agreement periodically and the account silently sits at *Pending* until + somebody clicks through it in Agreements, Tax, and Banking. Check it **first**, not at submission. +- **TRAP — the public contact address ends up being a personal mailbox.** It goes on a privacy page, + a deletion page, both listings and (for a UGC app) the address Apple expects report responses from, + so it has to be per-product and send-capable, not a forwarding alias nobody replies from. +- **TRAP — the age-rating questionnaire changes under you.** Apple overhauled it in 2025 (new bands + 4+/9+/13+/16+/18+) and added Social Media questions in July 2026, **required for any submission + from September 2026**. Blog summaries lag behind, so read the questions off Apple's own + *Age ratings values and definitions* page on the day. Two answers set the floor: Social Media + forces 13+, and Unrestricted Web Access forces 16+. An auth sheet is not a web view. + +- **TRAP — a deferred compliance item reads as done.** Account deletion shipped, and the Apple token + revocation Apple pairs with it was deferred to an issue that stayed open + ([#132](https://github.com/fardavide/oltre/issues/132)). The checklist first said both had + shipped. Before filing any store answer, verify each compliance claim against the code with a grep + (`/auth/revoke`, SDK imports, the schema), not against docs or memory. +- **TRAP — App Privacy has more data types than the obvious one.** A game that keeps a server-side + save collects **Gameplay Content** ("saved games"), on top of the account's User ID. A screen name + is a **User ID** in Apple's taxonomy, not a Name. + +## Friction + +- **FRICTION — store requirements live in eight places**: App Review guidelines, App Store Connect's + own required fields, Play policy, Play Console's listing form, Data Safety, IARC, the brand + guidelines for each badge, and the screenshot size tables that change yearly. Nothing assembles + them per-app. +- **FRICTION — screenshot sizes are a moving target and the sources are SEO blogs.** For 2026: 6.9-inch + iPhone 1320×2868 required, 13-inch iPad 2064×2752 required when the app targets iPad, Apple scales + down for everything smaller; Play wants each side between 320 and 3840 px. Committed UI baselines + (393×852 here) fit neither store raw. +- **FRICTION — the assets a listing needs are not the assets an app needs.** Play's 1024×500 feature + graphic exists for no other purpose and had to be made from scratch, while the 512×512 icon was + already generated. +- **FRICTION — the subtitle took three rounds.** It is copy plus search terms in 30 characters. + What worked was grounding it in a reference game's real store listing (OGame's Play subtitle + *"Strategy in space: build colonies, research & fight for resources"*). Then treat subtitle + + keywords as **one search-term budget**: a word dropped from the subtitle moves to keywords rather + than disappearing. Mind the locale's spelling too: en-US says *colonization*, so a British + *colonisation* would miss US searches. + +## Candidates + +- **CANDIDATE — a `store-release` skill**: the ordered checklist with the traps inline, parameterised + by "does the app have accounts / UGC / iPad support", since those three answers generate most of + the obligations. +- **CANDIDATE — a screenshot-frame script**: take committed UI baselines, render them onto the exact + canvases each store demands (and nothing else), fail loudly on a size no store accepts. +- **CANDIDATE — a listing-copy template** in the repository rather than typed into two web forms: + short description, full description, keywords, review notes — one source, two stores. +- **CANDIDATE — the privacy + deletion pages as a generated pair**, from a small declaration of what + the server stores, so the page, the App Privacy answers and the Data Safety answers cannot drift. +- **CANDIDATE — `bundleRelease` beside `assembleRelease`** in the release workflow, plus the + service-account publish step, once the first manual upload has proved the listing. + +## To double-check when we get there + +- Whether Xcode Cloud has a start condition excluding `site/**`. `decisions.md` says *"a typo fix in + a privacy policy must not be able to cut a TestFlight build"*, but the template lives on `main` and + every `main` commit archives. Either the condition exists in the App Store Connect UI or the + sentence is wrong. +- Whether Apple accepts request logs (IP, 30 days) going undeclared as a data type. Apple has no IP + category, and the logs are infrastructure logging, but this is a reading of the definitions, not + a ruling. + +- Whether Play still accepts a 9:19.5 phone screenshot, or whether the 1080×1920 canvas is actually + required. +- Whether Apple's reviewer accepts *"sign in with your own Apple Account"* for an app whose server + creates the account on first sign-in — the theory is yes, the evidence is a review round. +- Whether the Android OAuth client's SHA-1 survives the Play App Signing upload, proved by a Google + sign-in from a Play-installed build and nothing less. +- Whether the iOS build number and `MARKETING_VERSION` line up with what App Store Connect will + accept for a first public release, rather than a TestFlight one. diff --git a/.ai/docs/store-launch.md b/.ai/docs/store-launch.md new file mode 100644 index 00000000..27920561 --- /dev/null +++ b/.ai/docs/store-launch.md @@ -0,0 +1,221 @@ +# Store launch — the live to-do + +The working checklist for putting Oltre on the **App Store** and **Google Play**, and then putting +the two store buttons on the README and on oltre.space beside a smaller APK link. + +**How this file is used.** One line per item, each owned by either *Davide* (a portal, an account, a +payment, a decision) or *the build* (a repository change). Tick as we go; nothing is ticked on the +strength of "should work". Delete the file when the two listings are live and the buttons are up — +anything worth keeping moves into `identity-provisioning.md` or `decisions.md` first. + +Status legend: `[ ]` open · `[~]` in progress · `[x]` done · `[!]` blocked on a decision. + +**Order, Davide 2026-09-16: the App Store first.** Play Console would not load, so registration moves +to 2026-09-17; §4 and the two pages in §5 run ahead of §2 and §3. The public contact address is +`oltre@fardavide.dev` on Davide's own domain — pending his confirmation, and named in exactly one +place in the generated pages so it is one edit if it changes. + +--- + +## 0. What is already true + +Facts read out of the repository on 2026-09-16, so no step below re-establishes them. + +- Apple Developer Program membership exists — team `A7Q83J6LR4`. Xcode Cloud archives every `main` + commit to **TestFlight internal testing**, so an App Store Connect **app record already exists** + for `dev.fardavide.oltre`. Read off App Information on 2026-09-26: Name **Oltre**, SKU + `OLTRE-IOS`, Apple ID **`6798498388`**, primary language English (U.S.), standard licence + agreement. So the App Store button's URL is `https://apps.apple.com/app/id6798498388` — it + resolves only once the app is live. +- Where the data lives, checked 2026-09-26 for the privacy page: Neon project `Oltre` in + `aws-eu-central-1` (Frankfurt) with **6 hours** of point-in-time history, so a deleted account + survives in restore history for at most 6 hours; Cloud Run in `europe-west1`; Cloud Logging's + `_Default` bucket (request logs — IP address, user agent) keeps **30 days**. No analytics, crash + reporting or push SDK in the client. +- Android ships as a **signed APK on GitHub Releases** (`release-android.yml`), from the keystore in + `~/Documents/Keys/Oltre/` whose SHA-1 is bound to the Google Sign-In Android OAuth client. +- One identifier across both stores: `dev.fardavide.oltre`. Version `0.25.1`, `targetSdk 36`, + `minSdk 26`. +- **In-app account deletion ships** (`AuthEndpoints.deleteAccount`, Settings → Delete account), so + guideline 5.1.1(v) is satisfied. **Apple token revocation does not** — nothing calls + `/auth/revoke` yet, and it is tracked as [#132](https://github.com/fardavide/oltre/issues/132). + Apple has asked for it since June 2022 of any app offering deletion and Sign in with Apple, so it + is a pre-submission item in §4. *(Corrected 2026-09-26: this line first said revocation shipped.)* +- **Sign-in is mandatory to play** — *"No colony is created until you are in"*. That pulls in the + reviewer sign-in note (Apple 2.1(a)), Play Data Safety, and Play's web account-deletion URL. +- **The roster shows typed commander names and typed alliance names to strangers**, and moderation + was deferred with an explicit trigger: *"Its trigger is an App Review round, not a sprint"* + (`alliance-sheet.md` §6, §"outside the epic"). This submission is that round. See §1. +- No privacy policy page and no terms page exist. oltre.space is one generated `index.html`. + +--- + +## 1. Decisions — Davide's, taken 2026-09-16 + +- [x] **UGC moderation ships before the App Store round.** A small slice — report a name, block a + member, a name filter, published developer contact — rather than hiding typed names or accepting + a guideline 1.2 rejection. Scoped in §7. +- [x] **The build writes the privacy policy and the deletion page**, Davide reviews before they go + up. Not a generator: a generated policy describes an app that collects analytics and serves ads, + neither of which is true here, and a false privacy label is itself a rejection. +- [x] **First Play upload by hand.** The build produces a signed AAB; Davide uploads it. Automation + is a follow-up and cannot run before a first release exists anyway. +- [x] **Play registers as a personal account, and the closed test is decided later.** Davide, + 2026-09-16. So production access on Play is gated on a 12-tester / 14-day closed test that has not + started — everything else (listing, AAB, App Store, moderation, the site) proceeds regardless, and + **Play production is the last thing that can go live**, not the first. The $25 registration is + still unpaid; the account does not exist until it is. + + The fork as it stood, kept because the next app faces it too: + - **Personal** — instant, and then **12 testers opted in continuously for 14 days** before + production access. The 14-day clock can run under everything else, so it costs two weeks of + calendar, not two weeks of work. + - **Organization** — exempt from the tester requirement, but needs a legal entity and a D-U-N-S + number, and verification runs 2–4 weeks (longer if the D-U-N-S has to be requested first). + + Whichever is chosen, **register today**: on the personal path the clock starts only once a closed + test is live, and the App Store half proceeds in parallel either way. + +--- + +## 2. Google Play — the account and the irreversible choice + +- [~] Davide: the Google account for it exists; the **$25 registration is not paid**, so there is no + developer account yet. Pay it, choosing the type §1 settles, and note the creation date. +- [ ] Davide: create the app — *Oltre*, game, free, and record the default language. +- [ ] **Play App Signing: choose "Provide a copy of your app signing key" and upload the existing + keystore.** Not the default, and irreversible. Letting Google mint a new key changes the SHA-1 + the Android OAuth client is bound to, and **every Play install fails to sign in** until a third + Android OAuth client is registered. `identity-provisioning.md` records this trap verbatim. +- [ ] Build: add a signed **AAB** to the release path — Play takes an App Bundle, not the APK the + GitHub release publishes. `:androidApp:bundleRelease`, same signing config. +- [ ] Davide: upload the first AAB to **internal testing** and install it from Play on his own phone. +- [ ] Verify **Sign in with Google still works from that Play-installed build** — the one check that + proves the signing-key choice landed right. A failure here is the SHA-1, nothing else. + +## 3. Google Play — the listing + +- [ ] Store listing: short description (80 chars), full description (4000), app icon + (`art/icon/android/play-store-512.png`, already generated), **feature graphic 1024×500 — does + not exist yet, the build generates it from `art/icon/threshold.svg`**. +- [ ] Phone screenshots (2–8). The Roborazzi baselines are 393×852; Play wants each side between 320 + and 3840 px, so they are rendered onto a 1080×1920 canvas rather than uploaded raw. +- [ ] Tablet screenshots — only if the listing declares tablet support. +- [ ] **Data safety** form: account identifier (`sub`) collected, linked to the user, not used for + tracking, purpose App Functionality — the same answers as the Apple App Privacy label, which + `identity-provisioning.md` §51 already decided. +- [ ] **Account deletion URL** on the data safety form — the web page from §5. +- [ ] Content rating questionnaire (IARC), target audience, ads declaration (none), news (no), + government (no), financial features (none). +- [ ] Privacy policy URL from §5. +- [ ] Closed test with 12 testers for 14 days — **required, and deliberately not started yet** (§1). + Nothing else waits on it; Play *production* waits on nothing else. +- [ ] Apply for production access, then release to production. + +## 4. App Store — the listing and the review + +- [ ] Davide: in App Store Connect, open the existing `dev.fardavide.oltre` record and fill the **App + Store** tab (TestFlight has never needed it). +- [ ] Screenshots: **6.9-inch iPhone** (1320×2868) is required, and because + `TARGETED_DEVICE_FAMILY` is `"1,2"` a **13-inch iPad** set (2064×2752) is required too. Apple + scales these down for every smaller device. Captured from the simulator, not from Roborazzi. +- [ ] Name, subtitle, keywords, description, promotional text, category (Games → Strategy), support + URL, marketing URL, copyright. + - [x] Name: **Oltre** (reserved with the record). + - [x] Subtitle: **`Online space colonization`**. Davide chose it on 2026-09-26. It's in OGame's + register without naming it, with *online* added at his request. It uses US spelling + because the primary language is English (U.S.), and search does not match across + spellings. Saved. + - [ ] Keywords must carry **`strategy`**, which the subtitle gave up for *online*. Subtitle and + keywords are indexed together, so a word in one is wasted in the other. + - [x] Category: Games, subcategories Strategy + Simulation, no secondary category. Saved. + - [x] Content Rights: *No* third-party content. Saved. +- [~] **Age rating: 4+.** Every answer below was checked against the code on 2026-09-26: no combat, + weapons, chat, ads, rankings or web view in any player-facing string. + - Parental Controls No · Age Assurance No. + - Unrestricted Web Access No: the Google sign-in sheet is an auth session, not browsing. + - **User-Generated Content Yes.** Typed commander and alliance names are shown to strangers. + Declaring it costs nothing at 4+, and hiding it would contradict the moderation slice. + - **Social Media No.** Davide decided this on 2026-09-26: there's no feed, only exact search + by name. *Yes* would force 13+ and a Social Media descriptor. + - Messaging and Chat No · Advertising No. + - Every Mature Themes, Medical, Sexuality and Violence item: None / No. + - Gambling No · Simulated Gambling None · Contests None · Loot Boxes No. Probe results are + random but never bought. +- [~] **App Privacy.** Data is collected: **Yes**. Two data types, each **App Functionality** only, + **linked** to the user, **not** used for tracking: + - **Identifiers → User ID.** This covers the provider `sub`, the surrogate account id, and the + commander name, since Apple's definition includes *"screen name, handle"*. This is §51 of + `identity-provisioning.md`. + - **User Content → Gameplay Content.** The colony save plus alliance names, seats and + contributions. Apple's definition names *"saved games"* and *"user-generated content + in-game"*. §51 missed this one. + - Not declared: email, which is verified in the token and discarded, and no device ID, since no + IDFA/IDFV/ANDROID_ID is read. Also no diagnostics, crash data, usage data or location. + Cloud Run request logs (IP, 30 days) are infrastructure logging; they are stated in the + privacy policy and not filed as a data type. + - [ ] Privacy Policy URL `https://oltre.space/privacy.html`, which resolves once §5 merges. +- [ ] **Apple token revocation, [#132](https://github.com/fardavide/oltre/issues/132)**, lands + before submission. +- [ ] **Sign-In Information**: the app requires sign-in, so the field is mandatory. The server creates + an account on first sign-in, so a reviewer signing in with their own Apple Account works — say + exactly that in the notes, or a rejection under 2.1(a) is the default outcome. +- [ ] Review notes: the game is asynchronous, so nothing visible happens in the first minute by + design. Say so, and say what to tap. +- [ ] Pick the build already on TestFlight, submit, answer the review. + +## 5. The two web pages both stores demand — the build's + +- [x] `site/static/privacy.html` is drafted on branch `store-launch-pages`. Every claim is checked + against `schema.sql`, the live Neon project, the Cloud Logging buckets and the client (no + analytics/push SDK, local notifications only). It deliberately says nothing about Apple + revocation until [#132](https://github.com/fardavide/oltre/issues/132) lands. +- [x] `site/static/delete-account.html` is drafted: the in-app steps, what goes and what stays, and + an email route for someone locked out. Oltre stores no email, so that route asks for a + commander name and alliance rather than promising a lookup that cannot exist. +- [x] Both are static files, copied verbatim by `build_site.py`, and linked from the landing page + footer with the contact address. A local build writes them and resolves every footer link. +- [ ] Davide reads both. Two claims are his to confirm: he is the **controller**, and the page + says Oltre is **not directed at children under 13**, which Play's target-audience form will + ask about too. +- [ ] Merge, which also cuts a TestFlight build, since every `main` commit does. Then paste the URL + into App Privacy. + +## 6. The buttons — README and oltre.space + +Blocked until the two listings are live; a store button that leads nowhere is a dead control. + +- [ ] `site/index.html`: two primary buttons — **App Store** and **Google Play** — and the APK + demoted to a smaller text-only button with a background. The APK's version, size and URL stay + derived from the published release; the two store URLs are constants. +- [ ] Store badges: Apple's and Google's official badge artwork, both of which carry brand rules + about size and clear space. Committed under `art/`, copied in by `ASSETS` in `build_site.py` so + a missing file fails the build rather than the page. +- [ ] `README.md` §Install: the same three, in the same order. +- [ ] A screenshot test or a build assertion is not available for the site; check the rendered page. + +--- + +## 7. The moderation slice — the build's + +Decided in §1. It is the compliance surface `alliance-sheet.md` §6 deferred, and both stores want the +same four things of an app that shows one player's typed text to another. + +- [ ] **A name filter** on what can be typed — commander name and alliance name, on the server, so an + old client cannot route around it. +- [ ] **Report** — a member on the roster, and an alliance in search results. It has to reach + somewhere a human reads. +- [ ] **Block** — a blocked commander's name and alliance stop being shown to the blocker. +- [ ] **Published developer contact** — an address on oltre.space and in the App Store listing, which + the privacy page in §5 needs anyway. +- [ ] Design round trip for the report and block faces, per `.ai/rules/session-roles.md`. +- [ ] Balance/design calls that are Davide's, collected before the slice starts rather than during. + +--- + +## 8. Notes for a reusable flow + +Kept in [`store-launch-notes.md`](store-launch-notes.md), on Davide's ask (2026-09-16): every step +that was friction, every thing that needs double-checking, and every candidate for a script, a CI +job or a skill — so publishing the *next* app is not this document again. What the notes become is +decided at the end, not now. diff --git a/site/index.html b/site/index.html index ecc56757..199d068e 100644 --- a/site/index.html +++ b/site/index.html @@ -549,7 +549,12 @@

Latest release

diff --git a/site/static/delete-account.html b/site/static/delete-account.html new file mode 100644 index 00000000..40115ba6 --- /dev/null +++ b/site/static/delete-account.html @@ -0,0 +1,103 @@ + + + + + + + + Delete your account — Oltre + + + + + + + +
+ ← Oltre +

Delete your account

+ +

In the app

+
    +
  1. Open Oltre and tap the settings button at the top of the screen.
  2. +
  3. Tap Delete account.
  4. +
  5. Tap Delete account again to confirm.
  6. +
+

+ It happens straight away, and it cannot be undone. Signing in again afterwards with the + same Apple or Google account starts a new, empty colony. +

+ +

What is deleted

+ + +

What stays, and for how long

+ + +

If you cannot open the app

+

+ Write to oltre@fardavide.dev + and say whether you signed in with Apple or Google. Oltre does not store email addresses, + so it cannot find an account from one — include your commander name and your alliance, if + you had them, and we will work out with you which colony is yours before deleting it. + Requests are answered within 30 days. +

+

+ More about what Oltre stores is in the privacy policy. +

+ + +
+ + diff --git a/site/static/privacy.html b/site/static/privacy.html new file mode 100644 index 00000000..bd8405c7 --- /dev/null +++ b/site/static/privacy.html @@ -0,0 +1,164 @@ + + + + + + + + Privacy — Oltre + + + + + + + +
+ ← Oltre +

Privacy

+

Last updated 26 September 2026

+ +

+ Oltre is a game, and it keeps only what it needs to run your colony while you are away. No + analytics, no advertising, no tracking, and nothing is sold or shared for anyone's + marketing. This page says exactly what is stored, where, and for how long. +

+ +

Who is responsible

+

+ Oltre is made by Davide Farella, who is the controller of the data described here. Write to + oltre@fardavide.dev about anything on this page. +

+ +

What is stored, and why

+ +

+ On your device, the app keeps a copy of your colony and the session that keeps you signed + in. Notifications about fleets and probes are scheduled on the device itself; nothing is + sent to a push service. +

+ +

The legal basis

+

+ Everything above except the server logs is processed because it is necessary to provide + the game you signed up for. The logs are kept on the basis of a legitimate interest in + running a service that stays up and is not abused. +

+ +

Where it lives

+ +

+ These providers process data on Oltre's behalf and for no purpose of their own. Nothing is + shared with anyone else. +

+ +

How long it is kept

+ + +

Your rights

+

+ You can see and change your commander name in the app at any time, and + delete your account from the app's settings, which + removes everything above that belongs to you. You can also ask for a copy of your data, ask + for it to be corrected or erased, or object to how it is used, by writing to + oltre@fardavide.dev. If you think your data has + been mishandled, you can complain to the data protection authority where you live. +

+ +

Children

+

+ Oltre is not directed at children under 13 and does not knowingly hold data about them + beyond what is described here. +

+ +

Changes

+

+ If this page changes, the date at the top changes with it, and the previous versions stay + in the project's history. +

+ + +
+ +