diff --git a/CHANGELOG.md b/CHANGELOG.md index ebc22f7b..c4bf7ec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,17 @@ # Changelog Developer-facing changes to OpenDocument Reader for iOS, in [Keep a -Changelog](https://keepachangelog.com/en/1.1.0/) format. Changes to the shared -OpenDocument core are listed under the release that shipped them. The shorter -"What's New" copy the store shows lives in -`fastlane/metadata//changelogs/`, written from these entries by -`scripts/store-copy.py`. - -Entries go under `Unreleased` in the pull request that makes the change. The -heading is cut when the release is **submitted**, in one pull request that also -writes the store copy for that version. - -A release run refuses a version with no section here, and makes that section the -body of the GitHub release it drafts. Until the release is out the section stays -open: **a second build under the same version goes under the already cut -heading, not back under `Unreleased`.** Date the heading and add its compare link -once the version tag exists. +Changelog](https://keepachangelog.com/en/1.1.0/) format. Changes to odrcore are +listed under the release that shipped them. The store's "What's New" text is +written from these entries by `scripts/store-copy.py`. + +Rules: + +- Add an entry under `Unreleased` in the pull request that makes the change. +- Cut the heading in the pull request that also writes the store copy. +- A second build of the same version goes under the cut heading, not under + `Unreleased`. +- Add the date and the compare link once the version tag exists. ## [1.47] diff --git a/README.md b/README.md index 13507f0c..d8383385 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,15 @@ # OpenDocument.ios ![](https://github.com/opendocument-app/OpenDocument.ios/actions/workflows/build_test.yml/badge.svg) ![](https://github.com/opendocument-app/OpenDocument.ios/actions/workflows/format.yml/badge.svg) -It's Android's first OpenOffice Document Reader... for iOS! -This is an iOS frontend for our C++ [OpenDocument.core](https://github.com/opendocument-app/OpenDocument.core) library. +The iOS app of [OpenDocument.core](https://github.com/opendocument-app/OpenDocument.core). +It opens and edits office documents and PDFs. ## Setup -Open `OpenDocumentReader.xcodeproj` in Xcode. Everything comes from Swift -Package Manager and is resolved by Xcode — odrcore included, as the prebuilt -`OdrCoreObjC.xcframework` the -[OdrCore](https://github.com/opendocument-app/OpenDocument.core) package -downloads from its release. There is no conan step and no C++ toolchain to set -up. +Open `OpenDocumentReader.xcodeproj` in Xcode. Swift Package Manager resolves +everything, including odrcore as the prebuilt `OdrCoreObjC.xcframework`. There +is no conan step and no C++ toolchain. -To try an unreleased odrcore, point the package reference at a local checkout +To use an unreleased odrcore, point the package reference at a local checkout and build the xcframework there: ```sh @@ -20,24 +17,19 @@ cd ../OpenDocument.core apple/build_xcframework.py slice && apple/build_xcframework.py assemble ``` -Its `Package.swift` then takes `ODR_XCFRAMEWORK=OdrCoreObjC.xcframework` from the -environment of every `xcodebuild` invocation instead of the release artifact. +Then set `ODR_XCFRAMEWORK=OdrCoreObjC.xcframework` in the environment of every +`xcodebuild` call. ## The two apps -Two targets, and what separates them is what they *link*: - -| target | scheme | bundle id | ad + consent sdks | +| target | scheme | bundle id | ad sdk | | --- | --- | --- | --- | | `OpenDocumentReader` | `ODR Full` | `at.tomtasche.reader` | no | | `OpenDocumentReader Lite` | `ODR Lite` | `at.tomtasche.reader.lite1` | yes | -Two targets rather than two configurations of one, because a Swift package -product is linked by a target and no build setting takes it back out. Pro's -release executable is 0.5 MB against Lite's 4.4 MB. - -Four folders, each a synchronized group, so adding a file is all it takes to -add it to the build: +They are two targets, not two configurations, because a linked package cannot +be removed by a build setting. Each folder is a synchronized group, so a new +file joins the build on its own: | folder | in | | --- | --- | @@ -45,316 +37,163 @@ add it to the build: | `Ads/` | Lite | | `NoAds/` | Full | | `OpenDocumentReaderTests/` | the test bundle | - -`Ads/` is the only place that names a type from an ad sdk. `AdSlot` (the banner -and the consent form) and `AdPrivacy` (the way back to that choice) have a no-op -twin of the same shape in `NoAds/`; a method added to one copy has to be added to -the other, which building both schemes catches. - -Code that has to *ask* reads `Features.withAds`, never the bundle id. `LINKS_ADS` -behind it sits in `Ads/` and `NoAds/` next to the classes it stands for, so the -flag cannot end up in a build whose code says otherwise. `AnalyticsManager` and -`CrashManager` take no switch at all - both write to `os.Logger` and nowhere -else, so there is nothing to withhold. - -Pro also has `Features.advancedEditing`, from `ADVANCED_EDITING` in the same -two files. Lite renders with the editing scope `paragraph`, so odrcore refuses -a change that reaches past one paragraph with `outOfScope`, and the app offers -Pro. - -**The gate is on the tool, not on the mode.** Both editions open every kind the -core calls editable, a PDF included. A locked `EditToolBar` dims what only -offers Pro and leaves the **highlighter** working under both its names - -`highlight` is the formatting style and the PDF's marking tool alike. Do not put -the whole-mode gate back. `OpenDocument.droid` draws the same line. - -`configs/full` and `configs/lite` hold each bundle's `Info.plist` and privacy -manifest, out of the synchronized folder, since anything left in there would be -copied into both apps. For the same reason `scripts/make-test-fixtures.py`, which -writes the small sample documents, sits outside the test folder: everything in -there is copied into the test bundle, and the tests want the documents, not the -script that made them. +| `configs/full`, `configs/lite` | `Info.plist` and privacy manifest of each app | + +Rules: + +- Only `Ads/` names a type from an ad sdk. `AdSlot` and `AdPrivacy` have a + no-op twin in `NoAds/`. Build both schemes after you change one of them. +- Code asks `Features.withAds` or `Features.advancedEditing`, never the bundle + id. The constants behind them live in `Ads/Linked.swift` and + `NoAds/Linked.swift`. +- Lite edits with the scope `paragraph`. odrcore refuses a larger change with + `outOfScope`, and the app offers Pro. +- The gate is on the tool, not on the mode. Both apps open every editable + document, and a locked `EditToolBar` dims the Pro tools. The highlighter + works in both apps. Do not gate the whole edit mode. +- `scripts/make-test-fixtures.py` writes the test documents. It stays outside + the test folder, because everything in there goes into the test bundle. ## How a document reaches the screen -`CoreWrapper` hands the file to odrcore, which returns an `HtmlService`: a -handle that knows which views the document has but has not rendered any of -them. That service is connected to odrcore's HTTP server, bound to `127.0.0.1` -on whichever port was free, and the web view is pointed at -`http://127.0.0.1:/file//.html`. odrcore renders a page when -the web view asks for it, on one of the server's threads. - -The same thing OpenDocument.droid does, and for the same reasons: rendering -happens off the thread that opened the document, only the pages that are looked -at are rendered at all, and turning a page is a navigation rather than another -translation. The `` changes on every translation, because the web view -caches by URL and a document re-translated after a password or an edit has to -land on an address it has not seen. - -There is no file-writing fallback: a socket that cannot be opened fails the -translate, and the document is reported as failed. The web view still loads -`file:` URLs, but only for the formats odrcore does not handle at all, which -`DocumentViewController` hands it directly. - -None of this needs a capability or prompts the user. A listening socket on -loopback takes no entitlement, and the local network permission introduced in -iOS 14 covers the local subnet and multicast, not `127.0.0.1`. It does need an -App Transport Security exception, since ATS blocks plain HTTP: -`NSAllowsLocalNetworking` in both `Info.plist`s, which is the narrow one for -local addresses and — unlike `NSAllowsArbitraryLoads` — needs no justification -in App Store review. +`CoreWrapper` gives the file to odrcore and gets an `HtmlService`. odrcore's +HTTP server binds to `127.0.0.1` on a free port, and the web view loads +`http://127.0.0.1:/file//.html`. odrcore renders a page +only when the web view asks for it. The `` changes on every +translation, because the web view caches by URL. + +If the socket cannot open, the translate fails. There is no file fallback. +`file:` URLs are used only for formats odrcore does not handle. + +This needs no entitlement and no permission prompt. It needs +`NSAllowsLocalNetworking` in both `Info.plist`s, because App Transport +Security blocks plain HTTP. ## Editing -Every document is rendered editable, so the pencil only calls -`odr.editing.enable()` and the page stays where it is. The same pencil edits a -document and marks up a PDF: the two never stand in the bar together, so the -label separates them. - -**The bar holds what is done to the document, the strip what is done to the -text.** Undo, redo and save are bar buttons, so they do not scroll away. Redo -stays out over a PDF, where a mark is never put back, and the magnifier stands -down while an edit is on to leave the three their width. - -`EditToolBar` is the formatting alone, so a sheet or a plain text file shows no -strip. A tap does the tool's one job and a **long press** opens the colours it -applies; the bar under the icon is what the next tap uses, not what the -selection is. The text colour and the text size open on a tap, having no state -to turn off. Do not put the chevrons back. The tools match the website and -OpenDocument.droid, and the page reports to the app through one -`WKScriptMessageHandler`. - -The glyphs are the system set's, bar one: it has no wavy underline, so the -squiggly mark is drawn. - -**A PDF's tools are the page's to arm.** `odr.annotation.press` marks a standing -selection and arms where there is none, and `markOnSelection` marks each -selection as it is made. Do not disarm on the app's side: a tool the reader -turned on is theirs to turn off. - -A save reads the page's log (`odr.editing.getOperations()`, or -`odr.annotation.getAnnotations()` for a PDF), and odrcore writes the file next -to the open one before it moves into place. The page then renders again and -stays in the edit. +- The pencil calls `odr.editing.enable()`. The page stays in place. The same + pencil marks up a PDF. +- The bar holds undo, redo and save. The strip (`EditToolBar`) holds the text + formatting. A sheet or a plain text file shows no strip. +- A tap uses a tool. A long press opens its colours. Do not add chevrons. +- Redo is hidden over a PDF. The magnifier is hidden during an edit. +- A PDF tool is armed by the page (`odr.annotation.press`), and the app never + disarms it. +- A save reads `odr.editing.getOperations()` or + `odr.annotation.getAnnotations()`. odrcore writes the file next to the open + one and then moves it into place. +- The page talks to the app through one `WKScriptMessageHandler`. ## Formatting -Swift sources are formatted with `swift-format` from the active Xcode -toolchain, configured in `.swift-format`. Run `scripts/format.sh` before -committing; CI runs `scripts/format.sh --check` and fails on any difference. +`scripts/format.sh` runs `swift-format` from the active Xcode toolchain, with +`.swift-format` as its configuration. Run it before you commit. CI runs +`scripts/format.sh --check`. ## Continuous integration | workflow | what it does | | --- | --- | -| `format` | `scripts/format.sh --check`, on every push and pull request | -| `build_test` | unit tests on the simulator plus a device build of both flavors | -| `release` | upload to App Store Connect, by hand, see below | - -`format` needs nothing but the Xcode toolchain and reports style breakage in a -minute, so it is kept apart from the build. +| `format` | `scripts/format.sh --check` on every push and pull request | +| `build_test` | unit tests on the simulator, plus a device build of both apps | +| `release` | upload to App Store Connect, started by hand | ## Releasing -The `release` workflow uploads a build to App Store Connect. It is dispatched by -hand, and never submits for review, so promoting a build stays a deliberate step -in App Store Connect: - ```sh gh workflow run release.yml -f version=1.38 ``` -It runs as five jobs: +The run uploads both apps and never submits for review. Its jobs: | job | what it does | | --- | --- | -| `build` | one run producing both signed `.ipa`s, archived on the run | -| `screenshots` | beside the build: photographs the app on two devices, in every locale | -| `upload` | one job per app, uploading its `.ipa` | -| `listing` | one job per app, writing what the store says and shows about it | -| `record` | once both landed: tag the build, draft the GitHub release | - -Both apps always go out together, and nothing chooses one: Pro and Lite are the -same sources built as two targets, one of which links no ad sdk. +| `build` | builds and signs both `.ipa`s | +| `screenshots` | one job per device, takes the store screenshots in every locale | +| `screenshot-set` | joins the two device halves, checks the set, archives it as `framed` | +| `upload` | one job per app, uploads its `.ipa` | +| `listing` | one job per app, writes the store text and screenshots | +| `record` | tags the build and drafts the GitHub release | -**If a `listing` job fails saying screenshots are not listed**, they are up: -App Store Connect was slow to list them and nothing was sent twice. Look before -submitting, and re-run the job if a locale really is short. +Before the run, do these steps in the pull request that cuts the version: -**To write a listing again from pictures already taken**, start a release with -`screenshots_from_run` set to that run's id - the `listing` jobs alone, against -its `framed` artifact, five minutes and no build number spent. Each locale is -cleared first, so it repairs a set in any state. +1. Cut the `Unreleased` heading in `CHANGELOG.md`. +2. Run `scripts/store-copy.py ` and read the diff. See + `fastlane/metadata/README.md`. -**If one app's upload fails, press "Re-run failed jobs".** Only that upload runs -again, against the `.ipa` already built and signed - build number included, since -it is baked in at archive time - and `record` runs behind it once it lands. +The run refuses a version without a changelog section or without release notes +in every locale. `scripts/resolve-version.py` and +`scripts/changelog_section.py` show what a dispatch would do. -### The release notes +Inputs: -The "What's New" text of every locale is written before the release, not typed -into App Store Connect during it: - -```sh -scripts/store-copy.py 1.41 -``` +| input | what it does | +| --- | --- | +| `version` | the marketing version, without `v` | +| `dry_run` | builds and signs, uploads nothing, needs no version | +| `screenshots_from_run` | runs the `listing` jobs alone, with the `framed` artifact of that run | -The English comes from the `CHANGELOG.md` section of that version - or from -`Unreleased`, where a version being cut still sits - and every other locale is -translated by an agent of its own, given that locale's store description and the -release before it, so the notes keep the words the listing already uses in that -language. A second agent reads each draft back against the English before it is -written. Read the diff, then commit it with the pull request that cuts the -heading. - -The copy lives in `fastlane/metadata//changelogs/1.41.txt`, one file per -version per locale, because App Store Connect keeps only the notes of the -submission in flight. `scripts/store_listing.py` checks it - the release run -refuses a version any locale is missing, before it builds anything - and stages -it into the shape `deliver` reads. - -The rest of the listing goes up with it: name, subtitle, description, keywords -and the URLs are written in `fastlane/metadata/` and pushed by the same job, so -the store says what is committed here rather than what someone last typed into -App Store Connect. Both apps say it. What they share is in `fastlane/metadata/` -and what one of them says instead is in `fastlane/metadata-pro/` or -`fastlane/metadata-lite/`, read in that order - which is the name outright, since -an app's name is unique in the store, and the one sentence about ads inside the -description. `review_information` and the categories are left out. See -`fastlane/metadata/README.md`. - -### The screenshots - -Taken during the release run rather than committed, because a screenshot is only -worth what the build it was taken from is worth: +Common cases: -```sh -bundle exec fastlane ios screenshots -``` +- One upload fails: press "Re-run failed jobs". The `.ipa` and its build number + are kept. +- A `listing` job says the screenshots are not listed: they are up. App Store + Connect was slow. Look, then re-run the job if a locale is short. -Six pictures per device - the folder, a text document, a spreadsheet, an edit -under way, a pdf and a Word file - on a 6.9" iPhone and a 13" iPad, in the nine -store locales the app is translated into. `hi` and `sv` are given the English -set, which is what those storefronts would show anyway. +### Version and build number -The documents in them are localized too. They are not committed: the lane runs -`scripts/make-screenshot-documents.py` before the build, because they are build -output and nothing but a screenshot run opens them. Building the app needs -none of it. +Nothing in the tree changes for a release: -Nothing is tapped to get there. The app takes `-ODRScreenshot ` in Debug -builds and puts itself on that screen, so the same picture comes out in every -language without driving Apple's document browser in eleven of them - see -`OpenDocumentReader/ScreenshotMode.swift` and the `ODR Screenshots` scheme. -`scripts/store_screenshots.py` checks the set against the sizes App Store -Connect takes, and the `listing` job hands it to `deliver` alongside the text. -The same set goes to both apps. +| | comes from | checked in | +| --- | --- | --- | +| `MARKETING_VERSION` | the `version` input | `0.0.0` | +| `CURRENT_PROJECT_VERSION` | one above the highest build of either app in App Store Connect | `1` | -The run archives them as the `screenshots` artifact, on a dry run too - which is -how to look at them before the store does. See `fastlane/screenshots/README.md`. +Both apps get the same build number, so one `(version, build)` pair names one +commit. -Nothing has to be committed to cut a release, and a release leaves no commit -behind either. Both halves of the version come from outside the tree: +### Tags -| | where it comes from | what is checked in | +| tag | who writes it | what it means | | --- | --- | --- | -| `MARKETING_VERSION` (`CFBundleShortVersionString`) | the `version` input | `0.0.0` | -| `CURRENT_PROJECT_VERSION` (`CFBundleVersion`) | one above the highest build either app has | `1` | - -The version in `project.pbxproj` is a placeholder that only local and CI builds -ever see; nobody bumps it, because a commit on `main` is not a release. The -version has to be above what is live in the store - App Store Connect is the only -thing that knows what that is, and it rejects the upload otherwise. +| `build/v/` | the workflow, after both uploads | this commit went up as that build | +| `v` | you, when you publish the draft release | this is what shipped | -The build number is resolved once and given to both apps, so one `(version, build)` -pair names one commit in both listings. App Store Connect only requires it to -increase, not to be contiguous, so whichever app was behind skips ahead. +No tag is pushed before a build, and no tag starts a build. Publish the draft +after App Store Connect shows the build as live: -`scripts/resolve-version.py` decides which version a run builds and -refuses runs that cannot name one; `scripts/changelog_section.py` refuses a version with -no `CHANGELOG.md` section, before anything is built, since that section becomes -the release body. Run either by hand to see what a dispatch would do. - -The `dry_run` input builds, signs and archives both `.ipa`s without uploading -either - the only way to exercise the signing path without putting a build on -TestFlight. It is also the only kind of run allowed to go without a version, and -the only one that leaves neither tag nor draft. +```sh +gh release edit v1.38 --draft=false +``` -It needs these repository secrets: +### Secrets | secret | what it is | | --- | --- | | `ASC_KEY_ID` | App Store Connect API key id | | `ASC_ISSUER_ID` | issuer id of that key | | `ASC_KEY_CONTENT` | the `.p8` private key, base64 encoded | -| `SIGNING_CERTIFICATE_P12` | Apple Distribution certificate + key as a base64 encoded `.p12` | +| `SIGNING_CERTIFICATE_P12` | Apple Distribution certificate and key, base64 encoded `.p12` | | `SIGNING_CERTIFICATE_PASSWORD` | password of that `.p12` | -The certificate is imported into a temporary keychain that is discarded with the -runner, and signing is manual: fastlane downloads the App Store provisioning -profile for the bundle id, and both the archive and the export use that -certificate and profile. Automatic signing would instead have Xcode mint -distribution assets of its own, which only an Admin key may do - anything less -fails the export with "Cloud signing permission error". - -Downloading a profile is something any key may do; creating one wants an Admin -key. So a lesser key works as long as both apps have an App Store profile -already - the run says so in its first seconds otherwise, and either an Admin key -or a profile made by hand in the developer portal gets past it. Profiles expire -after a year, which is the other moment this matters. +Signing is manual. The run downloads the App Store provisioning profile of +each bundle id, so both apps need one. A key below Admin cannot create a +profile, and the run stops in its first seconds if one is missing. Profiles +expire after a year. -The same lanes work locally once those variables are exported, and take the -version and the dry run the same way the workflow hands them over: +### Local lanes -```bash +```sh ODR_VERSION=1.36 bundle exec fastlane deployPro ODR_VERSION=1.36 bundle exec fastlane deployLite ODR_DRY_RUN=true bundle exec fastlane deployPro # build and sign only +bundle exec fastlane uploadListingPro # text, plus screenshots if present +bundle exec fastlane screenshots # see fastlane/screenshots/README.md ``` -`deployPro` is `buildPro` followed by `uploadPro`, which the workflow runs as -separate jobs. `uploadPro` takes the `.ipa` already in `build/` rather than making -one, and `resolveBuildNumber` prints the number both apps would get. - -`uploadListingPro` writes the text and, if `fastlane/screenshots` holds a set, -the pictures with it - so fixing a word in a description by hand does not cost a -quarter of an hour of simulators, while a release run, which always captures -first, sends both. - -### Tags - -Nothing is triggered by a tag, and no tag is pushed before a build: a version -often takes more than one build to get through review, so a tag pushed up front -names a commit that may never ship. That is what happened to `v1.37`. Tags are -written afterwards instead, in two kinds: - -| tag | who writes it | what it means | -| --- | --- | --- | -| `build/v/` | the workflow, once both apps are up | this commit was uploaded as that build | -| `v` | publishing the drafted release | this is what shipped | - -Both are prefixed with a `v`; the `version` input is not (`-f version=1.39` -writes `v1.39`). - -One build tag, not one per app, since both share a build number. It is never -moved: a rebuild gets the next number, so a version that takes three builds to -clear review leaves three build tags. A half uploaded release gets none, and -neither does a lane run locally. - -**The version tag is written neither by hand nor by the workflow.** `record` drafts -a GitHub release named `v` - the changelog section with the generated list -of pull requests below it - pointing at the built commit. A draft creates no tag; -publishing it does, at exactly that commit: - -```sh -gh release edit v1.38 --draft=false -``` - -That step stays human because App Store Connect is the only thing that knows a -build went live. A rebuild re-points the same draft rather than making a second -one, and if Pro clears review while Lite does not, wait: the build tags already -record what went out. +`deployPro` is `buildPro` and then `uploadPro`. `fastlane/README.md` lists all +lanes. ## License -This project is licensed under the [Mozilla Public License 2.0](LICENSE). +[Mozilla Public License 2.0](LICENSE). diff --git a/fastlane/metadata/README.md b/fastlane/metadata/README.md index 69a7d235..9a290e9a 100644 --- a/fastlane/metadata/README.md +++ b/fastlane/metadata/README.md @@ -1,109 +1,61 @@ # Store metadata -What App Store Connect shows about the apps, one directory per locale. +The text App Store Connect shows about the apps, one directory per locale. A +release run uploads what is committed here: name, subtitle, description, +keywords, the URLs and the release notes of the version. It does not upload +`review_information` or the category files. -This is where the listing is written, and a release run uploads it: name, -subtitle, description, keywords, the URLs, and the release notes of the version -going out. What the store says is what is committed here. +## Layout -Two things are left out of the upload on purpose. `review_information` is the -account's contact details and the note to the reviewer, and the category files -say where the app sits in the store - neither is release copy. -`scripts/store_listing.py` names what is staged, so adding a file to that list is -a decision rather than an accident. - -## The two apps - -Pro and Lite are the same app, and they say almost the same thing about -themselves. What is here is what they share. Where they have to differ: +Pro and Lite say almost the same thing. `scripts/store_listing.py` reads these +places in order, and the last one wins: | | | | --- | --- | -| `fastlane/metadata//` | what both say | +| `fastlane/metadata//` | what both apps say | | `fastlane/metadata-/all/` | what this app says instead, in every locale | -| `fastlane/metadata-//` | what this app says instead, here | +| `fastlane/metadata-//` | what this app says instead, in this locale | -Read in that order, last one wins. `` is `pro` or `lite`. +`` is `pro` or `lite`. -Only the name differs outright, and it has to: an app's name is unique in the -store, so one file each, `OpenDocument Reader Pro` and `OpenDocument Reader`. -There is no `name.txt` in this directory - the apps own their names. +- The name differs, because a store name is unique. Each app has one + `all/name.txt`, and this directory has none. +- The shared description holds `${ads}` and `${editing}`. Each app fills them + from its own `ads.txt` and `editing.txt` in that locale. Lite has an + `ads.txt`, Pro has none, and an empty fill-in leaves no space behind. +- `FILL_INS` in `scripts/store_listing.py` lists the allowed names, so a + misspelt fill-in is an error. -Two sentences differ inside otherwise shared text. The first is the advertising -line: Lite shows ads and Pro does not. Rather than keep two descriptions per -locale and let them drift, the shared one holds `${ads}` and each app fills it -in from its own `ads.txt` - Lite has one per locale, Pro has none, and a -fill-in nobody answers leaves nothing behind, the space in front of it -included. +## Limits -`${editing}` works the same way, but both apps fill it in. Pro alone adds new -paragraphs, formats text and marks a pdf past the highlighter. The shared -description says only what both apps do, and each `editing.txt` says the rest: -Lite's names it as Pro's, Pro's names it as its own. +| file | limit | +| --- | --- | +| `name.txt`, `subtitle.txt` | 30 characters | +| `keywords.txt` | 100 characters, commas included | +| `changelogs/.txt` | 4000 characters | -`FILL_INS` in `scripts/store_listing.py` lists the names one may have, so a -misspelt `${adds}` is an error rather than a sentence that quietly vanishes from -the store. +The name is the same in every locale. The local search words, `LibreOffice` +among them, go in the subtitle and the keywords. ## Release notes -`/changelogs/1.41.txt`, one file per marketing version per locale, -holding the "What's New" text of that submission. Written for people using the -app, not for this repository - the developer-facing record of the same release -is `CHANGELOG.md` at the root. - -Named by marketing version, not by build number. The build number is a live -query of what TestFlight already has, so it is not known until a release run -starts and cannot name a file committed ahead of it. +`/changelogs/.txt` holds the "What's New" text of one +marketing version. App Store Connect keeps only the notes of the current +submission, so these files are the history. `scripts/store_listing.py` stages +the version's file as `release_notes.txt` for `deliver`. -App Store Connect keeps only the notes of the version being submitted, so these -files are the history the store does not keep. The limit is 4000 characters per -locale. - -`deliver` does not read this layout. It reads one `release_notes.txt` per -locale, so `scripts/store_listing.py` stages the version's file under that name -into a throwaway directory at upload time, with the rest of the listing beside -it. - -## Writing them +Write them with: ```sh scripts/store-copy.py 1.41 ``` -The English text comes from the `CHANGELOG.md` section of that version, or from -`Unreleased` while the heading is still open; a file already written by hand is -left alone. Every other locale is then translated by an agent of its own, given -that locale's `description.txt` and the release before it, so the notes reach -for the words the listing already uses in that language. - -A second agent then reads that draft against the English, in the same language, -because what a first draft gets wrong is not something it can see: a word -borrowed for its sound rather than its sense reads fine to whoever wrote it. - -Both apps get the same notes, so they say what changed and not who gets it: no -"free", no "Pro". - -It writes; it does not upload, and it does not judge. Read the diff before -committing it - it goes to the store as written. - -The release run refuses a version any locale has no copy for, before it builds -anything. - -## Name, subtitle, keywords - -30 characters for the name, 30 for the subtitle, 100 for the keywords, counting -the commas. `scripts/store_listing.py` checks all three against what it stages -rather than against what is written here, since an app's own name is what -finally has to fit. - -The name is the same in every storefront and is not translated: `OpenDocument` -is the format's own name and goes untranslated in every language anyway, and one -name is one app that people can pass to each other. Nothing is lost to search by -it, because the App Store indexes name, subtitle and keywords alike - so the -local words, `LibreOffice` among them, live in the subtitle and the keywords -instead. That also means a keyword repeating a word from the name or from its -own subtitle is a wasted slot; none of these do. +1. The English comes from the `CHANGELOG.md` section of that version, or from + `Unreleased` while the heading is open. An existing file is kept. +2. One agent per locale translates it, with that locale's `description.txt` + and the previous notes as context. +3. A second agent reviews each draft against the English. +4. Read the diff, then commit it in the pull request that cuts the version. -None of it sells the app as an editor. It edits text, but that is young and does -not reach every document, so the listing says so once and calls itself a reader. +Both apps get the same notes, so the notes never say "free" or "Pro". The +release run refuses a version that has no notes in some locale. diff --git a/fastlane/screenshots/README.md b/fastlane/screenshots/README.md index 502a55bf..dbffa5f2 100644 --- a/fastlane/screenshots/README.md +++ b/fastlane/screenshots/README.md @@ -1,61 +1,52 @@ # Store screenshots -What App Store Connect shows of the app, one directory per locale - written -here by a capture run, and not committed. This directory is empty in the -repository on purpose. - -The store copy next door is text somebody wrote, so it lives in git and the -release uploads what is committed. A screenshot is not written, it is taken: -it is only worth what the build it was taken from is worth, and a picture of -1.38 sitting in git through 1.41 is a picture of an app nobody can install any -more. So they are taken during the release run, from the build going out. +The pictures App Store Connect shows of the app. Nothing here is committed. A +capture run writes the raw pictures to this directory and the framed set to +`fastlane/framed`, and the release run archives both as artifacts. Screenshots +are taken during the release run, from the build that goes out. ```sh -bundle exec fastlane ios screenshots +bundle exec fastlane ios screenshots # capture, then frame +scripts/frame-screenshots.py # frame again, without a capture ``` -That drives the app on both devices in every locale and writes them here. -`.gitignore` keeps what it wrote out of commits, and the release run archives -the same set as the `screenshots` artifact - which is how you look at what went -to the store, before it does on a dry run and after it on a real one. +The lane does these steps: + +1. `scripts/make-screenshot-documents.py` writes the localized sample + documents. They are build output and go into Debug builds only. +2. `fastlane snapshot` builds the `ODR Screenshots` scheme once and launches + the app once per screen and locale, with `-ODRScreenshot `. See + `OpenDocumentReader/ScreenshotMode.swift`. +3. `scripts/frame-screenshots.py` puts each capture on a device frame with a + headline from `fastlane/frames/frames.json`. It needs Pillow. +4. `scripts/store_screenshots.py` checks the framed set against the sizes the + store accepts. -## What is in a set +## The set -Six pictures per device, taken by relaunching the app onto one screen at a -time rather than by tapping through it. The screens, in the order the store -shows them: +Six screens per device, in store order: | | | | --- | --- | -| `01-browser` | the document browser, with one of each format sitting in it | -| `02-text` | a text document open, with a search under way | -| `03-sheet` | a spreadsheet, with the sheet tabs under the tool bar | -| `04-edit` | a document being edited, keyboard up | -| `05-pdf` | a pdf | -| `06-office` | the same reader on a Word file | - -Two devices, because an app that runs on iPhone and iPad has to hand in both: a -6.9" iPhone and a 13" iPad. `scripts/store_screenshots.py` holds the sizes App -Store Connect accepts and checks the set against them; `Fastfile` holds the -simulators to look for, newest first, because what a simulator is called -changes with every Xcode and what it is worth does not. +| `01-browser` | the document browser, with one file of each format | +| `02-text` | a text document | +| `03-sheet` | a spreadsheet, with its sheet tabs | +| `04-edit` | a document in edit mode, keyboard up | +| `05-pdf` | a PDF, with a search under way | +| `06-office` | a Word file | -## Locales +Two devices: a 6.9" iPhone and a 13" iPad. `Fastfile` lists the simulator +names to look for, newest first. `scripts/store_screenshots.py` lists the +pixel sizes the store accepts. -Eleven in the store, nine in the app. `de-DE`, `en-US`, `es-ES`, `fr-FR`, `it`, -`pl`, `pt-BR`, `ru` and `tr` are photographed in their own language. `hi` and -`sv` are given the English pictures, because the app has no Hindi or Swedish UI -either - that is what those storefronts would show whatever we upload. +## Locales -The documents in the pictures are localized too, which is most of what a reader -has to show. The lane writes them with `scripts/make-screenshot-documents.py` -before it builds, rather than keeping them in git: they are build output, and -they are bundled into Debug builds only. `-ODRScreenshot ` is how the -app is asked to open one. See `OpenDocumentReader/ScreenshotMode.swift`. +The store has eleven locales and the app has nine. `hi` and `sv` get the +English pictures, because the app has no Hindi or Swedish UI. +`scripts/store_screenshots.py --languages` prints the locales to capture. -## Both apps get the same pictures +## Both apps -Pro and Lite are one app built twice, and the one thing that differs on screen - -the banner Lite carries - is not in a screenshot either way. The set is taken -once, with the Pro scheme, which links no ad sdk and so cannot raise a consent -form in front of the camera. Both listings are then given it. +The set is taken once, with the `ODR Screenshots` scheme. That scheme builds +the Pro target, which links no ad sdk, so no consent form can appear. Both +listings get the same pictures.