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
..

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.