Commit 404f9df7 authored by Kourser's avatar Kourser
Browse files

fix(update): read the published version per platform

macOS 0.5.1 shipped while iOS stayed on 0.5.0, and the appcast carried a
single version number for both. Bumping it to 0.5.1 would have offered
iOS users an update the iOS App Store cannot serve — the exact dead end
the release notes warn about — so the number now lives under
platforms.<ios|macos>.

The flat version/urls pair stays in the file: apps already installed only
know how to decode that shape, and a missing key would silently stop
their update check. It holds the lower of the two versions, so no client
is ever pointed at a release it cannot install. Verified by decoding the
new file with the old struct.

Also record the live state checked against the App Store Connect API:
macOS 0.5.1 READY_FOR_SALE, iOS still 0.5.0.

Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
parent 4bb7ffd2
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -7,6 +7,12 @@ adopte le [versionnage sémantique](https://semver.org/lang/fr/).

## [Non publié]

### Corrigé
- **Notification de mise à jour** : le numéro de version publié est désormais
  lu par plateforme. iOS et macOS ayant des trains de versions distincts sur
  l'App Store, un numéro unique finissait par proposer à une plateforme une
  mise à jour qu'elle ne pouvait pas installer.

## [0.5.1] - 2026-08-19

### Corrigé
+35 −12
Original line number Diff line number Diff line
@@ -3,13 +3,41 @@ import Observation

/// One entry from the published appcast: the latest released marketing
/// version and, per platform, where to get it on the App Store.
///
/// iOS and macOS are two separate App Store version trains and do drift apart
/// (0.5.0 vs 0.5.1 in Aug 2026), so the version lives under `platforms`. The
/// flat `version`/`urls` pair is the older shape, kept because apps already in
/// the wild only know how to read that one — the file must stay decodable by
/// them, and it must never announce a version a given platform cannot install.
private struct Appcast: Decodable {
    struct Release: Decodable {
        var version: String
        var url: URL
    }
    struct Platforms: Decodable {
        var ios: Release?
        var macos: Release?
    }
    struct StoreURLs: Decodable {
        var ios: URL?
        var macos: URL?
    }
    var version: String
    var urls: StoreURLs
    var version: String?
    var urls: StoreURLs?
    var platforms: Platforms?

    /// The release for the platform we are running on, preferring the
    /// per-platform entry and falling back to the legacy flat fields.
    var currentPlatformRelease: Release? {
        #if os(macOS)
        if let release = platforms?.macos { return release }
        guard let version, let url = urls?.macos else { return nil }
        #else
        if let release = platforms?.ios { return release }
        guard let version, let url = urls?.ios else { return nil }
        #endif
        return Release(version: version, url: url)
    }
}

/// A newer release the user hasn't been prompted about yet.
@@ -49,18 +77,13 @@ final class UpdateChecker {
        do {
            let (data, _) = try await session.data(from: Self.appcastURL)
            let appcast = try JSONDecoder().decode(Appcast.self, from: data)
            #if os(macOS)
            let storeURL = appcast.urls.macos
            #else
            let storeURL = appcast.urls.ios
            #endif
            guard let storeURL,
                  Self.isNewer(appcast.version, than: Self.currentVersion),
            guard let release = appcast.currentPlatformRelease,
                  Self.isNewer(release.version, than: Self.currentVersion),
                  // Once per version: don't renag for a version already prompted.
                  appcast.version != defaults.string(forKey: Self.lastPromptedKey)
                  release.version != defaults.string(forKey: Self.lastPromptedKey)
            else { return }
            defaults.set(appcast.version, forKey: Self.lastPromptedKey)
            available = AvailableUpdate(version: appcast.version, storeURL: storeURL)
            defaults.set(release.version, forKey: Self.lastPromptedKey)
            available = AvailableUpdate(version: release.version, storeURL: release.url)
        } catch {
            // Silent by design.
        }
+11 −1
Original line number Diff line number Diff line
{
  "version": "0.4.0",
  "version": "0.5.0",
  "urls": {
    "ios": "https://apps.apple.com/fr/app/skingomz/id6782177493",
    "macos": "https://apps.apple.com/fr/app/skingomz/id6782177493?platform=mac"
  },
  "platforms": {
    "ios": {
      "version": "0.5.0",
      "url": "https://apps.apple.com/fr/app/skingomz/id6782177493"
    },
    "macos": {
      "version": "0.5.1",
      "url": "https://apps.apple.com/fr/app/skingomz/id6782177493?platform=mac"
    }
  }
}
+12 −6
Original line number Diff line number Diff line
@@ -8,10 +8,11 @@ Ces éléments accompagnent la build **0.5.1 (build 6)**. Les fiches **iOS** et
**macOS** sont deux trains de version distincts dans App Store Connect, avec
chacun leurs métadonnées (`metadata/` et `metadata_macos/`).

> État au 2026-08-19 : macOS 0.5.1 est soumise à la review ; iOS reste en 0.5.0
> en ligne, le build 0.5.1 est uploadé mais aucune version iOS n'est créée. Les
> État au 2026-09-01 (vérifié via l'API App Store Connect) : **macOS 0.5.1 est
> en ligne** (`READY_FOR_SALE`), **iOS est resté en 0.5.0** — le build 0.5.1 est
> uploadé mais aucune version iOS n'a été créée. Les
> `metadata/<locale>/release_notes.txt` décrivent donc encore 0.5.0 — **à
> réécrire avant le prochain envoi iOS**.
> réécrire avant le prochain envoi iOS**, en même temps que le sous-titre.

## Contenu

@@ -192,9 +193,14 @@ The app collects no personal data (see the privacy manifest).

## Rappels

- **Ne pas oublier** : mettre à jour `docs/appcast.json` **seulement une fois la
  build réellement en ligne** (sinon le pop-up « Mettre à jour » pointe dans le
  vide). Toujours en 0.5.0 à ce jour.
- **`docs/appcast.json`** : à mettre à jour **seulement une fois la build
  réellement en ligne** (sinon le pop-up « Mettre à jour » pointe dans le vide).
  ⚠️ Les deux plateformes ont des trains de versions distincts et **divergent**
  (iOS 0.5.0, macOS 0.5.1) : le numéro vit sous `platforms.<ios|macos>`. Les
  champs plats `version`/`urls` sont conservés parce que les apps déjà
  installées ne savent lire qu'eux — y laisser **la plus basse des deux
  versions**, jamais la plus haute, sinon une plateforme se voit proposer une
  mise à jour qu'elle ne peut pas installer.
- **Sous-titre** : pas de référence au prix (guideline 2.3.7). « Free » est lu
  par App Review comme « gratuit », même quand il veut dire « libre » — c'est ce
  qui a fait rejeter macOS 0.5.0. Les autres langues utilisent des mots de