Files
Felitendo 8b06b305f5 feat: give the manager the island the app has
FTStore Manager puts an app on a device through the same three stages FTStore
does, and had nothing to show for any of it once the screen was off. It has a
Live Activity now: the same widget the app carries, embedded as its own
extension under its own identifier, naming the app, filling a bar while the
bytes arrive, adding the rate and the time left, then Preparing and Installing.

It is the app's island with the pause button taken out. This download manager
has no pause and no screen to offer one from, so there are no intents to compile
into the extension and no paused state in the payload. The island is read-only
on every version rather than only on the older ones.

It ends when the installer answers, which is the part the app had to learn the
hard way. Open launches what was just installed and leaves the sheet standing
behind it, so ending on dismissal left the island describing an install that had
finished, until somebody came back and closed a receipt they were done with.
Dismissal still ends it, which is what covers a flow that never gets an answer
at all.

A dropped connection is now one of those answers. It used to remove the download
and tell nobody, leaving the flow spinning with no way to find out; that was
merely invisible before and would now be an island counting bytes that stopped
arriving.

None of this is visible from a build: an IPA whose extension is missing, misnamed
or misfiled installs, runs, and simply shows no island. Both of the Manager's
release workflows therefore run the same check the app's do against the built
IPA, and the ad-hoc signing step seals the nested extension. Two products now
build an extension of the same name out of projects that share their
identifiers, so the separate derived data paths carry more weight than before,
and the notes that explain them say so.
2026-09-01 08:35:02 +02:00

82 lines
4.7 KiB
Markdown

# FTStoreActivity — Live Activity / Dynamic Island (Manager)
This folder holds the **Live Activity** widget extension that shows the
download → sign → install progress in the Dynamic Island and on the lock screen
while FTStore Manager puts FTStore (or FTStore Admin) onto the device.
It is the app's island, forked the way the rest of `manager/` was: same shape,
same files, minus what the Manager does not have. See
`ios/FTStoreActivity/README.md` for the reasoning behind the design — this file
only records where the two differ and what is true here.
## What is different from the app's
- **No pause button, and no `isPaused` anywhere.** The Manager's `Download` has
no pause: there is nothing to hold a download with and no screen to offer it
from. So `InstallActivityIntents.swift` does not exist here, the extension
needs no `AppIntents`, and the island is read-only on every iOS version rather
than only below 17.
- **One install in practice, the same shape regardless.** The Manager tracks two
products and nothing stops both being started, so `ContentState` still
describes a set. It costs nothing and it is the only shape iOS renders well:
a second *activity* collapses the island to its minimal presentation.
- Bundle id: **`$(FTSTORE_PRODUCT_BUNDLE_IDENTIFIER).activity`** →
`app.ftstore.manager.activity`, which sits under the Manager's own
`app.ftstore.manager` exactly as iOS requires.
## Facts about the target
- The widget-extension **target exists in `FTStore.xcodeproj`** (added by hand to
`project.pbxproj`, synchronized-folder style, ids prefixed `57AC11…` — the
same ids the app's tree uses, because this project is a fork of that one down
to its GUIDs and keeping them equal is what keeps the two files diffable).
- This folder is a synchronized root group owned by the `FTStoreActivity`
target; `Info.plist` and `README.md` are membership exceptions.
- `FTStore/Backend/Observable/InstallActivityAttributes.swift` is shared into the
extension via an exception set on the `FTStore` folder: the widget needs the
attributes type to render. **It must stay in both targets.**
- iOS Deployment Target **16.1** (the app is 16.0; ActivityKit needs 16.1, and
`LiveActivityManager` uses the 16.2 `ActivityContent` API).
- The appex is **iOS-only**: the embed build file and the target dependency
carry `platformFilter = ios`, so Mac Catalyst builds skip it entirely.
- The app's `Info.plist` sets **`NSSupportsLiveActivities = YES`**.
- The Makefile ad-hoc codesign uses `--deep` so the nested unsigned appex
doesn't make sealing fail; on-device signing re-signs PlugIns anyway.
## How it's driven
- `LiveActivityManager` (in the app target) starts/updates/ends the activity.
- `AppInstallManager` calls it across the lifecycle: `start` when the download
begins, `update(.signing)` while signing, `update(.installing)` at install
hand-off, and `end` when the installer answers, on cancel, and on failure.
- **The island ends when the installer answers, not when the sheet closes.**
Tapping *Open* launches the app that was just installed and leaves the sheet
standing behind it, so ending on dismissal left the island saying "Installing"
until somebody came back and closed a receipt they were done with. Dismissal
still ends it, which is what covers a flow that never gets an answer at all.
- Everything is best-effort and availability-gated: with no widget target (or on
iOS < 16.2), all calls are safe no-ops and the rest of the app is unaffected.
## Don't let another product share this build's derived data
`manager/` is a fork of `ios/` down to the pbxproj GUIDs, and Xcode keys its
build database on those. When both Makefiles pointed `-derivedDataPath` at
`$(TMPDIR)/FTStore`, the app's build and this one swept each other's
intermediates; the app shipped an `.appex` left over from an older build and its
island showed nothing. Both Makefiles key the path on the product directory, and
now that *both* products carry an extension, that separation is the only thing
keeping them apart.
The symptom is invisible from the app side, so the release workflows run
`ios/scripts/verify-live-activity.sh` against the built IPA — this one included —
and fail on it. What it checks, and how to ask a device why the island is dead,
is documented in `ios/FTStoreActivity/README.md`; both hold here unchanged,
except that the log predicate is `subsystem BEGINSWITH "app.ftstore.manager"`.
## Verifying
Run on a device or simulator with iOS 16.2+. Tap **Install FTStore** on the home
screen: the Dynamic Island (iPhone 14 Pro and newer) and the lock-screen banner
should show the app name, a progress bar while downloading, then "Preparing" and
"Installing", and dismiss shortly after the install sheet answers.