Commit 909d31ba authored by Kourser's avatar Kourser
Browse files

fix(update): record the update prompt when it is answered

The version was marked "already prompted" the moment the check found it,
so an app killed before the alert rendered burned the one invitation that
version ever gets — silently, and for good. I hit exactly that while
testing: a launch happened behind a frozen simulator screen and the
prompt never came back.

Recording now happens in dismiss(), and the alert's presentation binding
is the single path that calls it. Both buttons only do their own work, so
neither can race the other in marking the version as seen.

Verified against the published appcast on an iOS 26 simulator, building
0.4.0 so the check fires: the alert offers 1.0.0; killing the app without
answering brings it back on the next launch; answering "Plus tard" writes
update.lastPromptedVersion = 1.0.0 and the next launch stays quiet.

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

## [Non publié]

### Corrigé
- **Invite de mise à jour perdue** : une version était marquée comme « déjà
  proposée » au moment où elle était détectée, et non quand l'utilisateur
  répondait. Une app fermée juste après le lancement consommait donc l'unique
  invitation prévue pour cette version, sans que rien ne s'affiche. Elle n'est
  enregistrée qu'une fois l'alerte refermée.

### Modifié
- **Appcast en 1.0.0** : la notification de mise à jour pointe désormais vers la
  1.0.0, disponible sur les deux plateformes. Les deux trains de versions sont
+6 −6
Original line number Diff line number Diff line
@@ -183,11 +183,11 @@ struct RootView: View {
            Text("« \(podcast.title) » et tous ses épisodes seront retirés de ta bibliothèque, avec ton historique de lecture. Tu pourras te réabonner plus tard.")
        }
        .alert("Mise à jour disponible", isPresented: updateAlertBinding, presenting: updateChecker.available) { update in
            Button("Plus tard", role: .cancel) { updateChecker.available = nil }
            Button("Mettre à jour") {
                openURL(update.storeURL)
                updateChecker.available = nil
            }
            // Both buttons close the alert, which flips the binding below; that
            // single path is what records the answer, so neither button can
            // race the other in marking the version as seen.
            Button("Plus tard", role: .cancel) {}
            Button("Mettre à jour") { openURL(update.storeURL) }
        } message: { update in
            Text("La version \(update.version) est disponible sur l'App Store.")
        }
@@ -353,7 +353,7 @@ struct RootView: View {

    private var updateAlertBinding: Binding<Bool> {
        Binding(get: { updateChecker.available != nil },
                set: { if !$0 { updateChecker.available = nil } })
                set: { if !$0 { updateChecker.dismiss() } })
    }

    private var reviewPromptBinding: Binding<Bool> {
+12 −2
Original line number Diff line number Diff line
@@ -79,16 +79,26 @@ final class UpdateChecker {
            let appcast = try JSONDecoder().decode(Appcast.self, from: data)
            guard let release = appcast.currentPlatformRelease,
                  Self.isNewer(release.version, than: Self.currentVersion),
                  // Once per version: don't renag for a version already prompted.
                  // Once per version: don't renag for a version already answered.
                  release.version != defaults.string(forKey: Self.lastPromptedKey)
            else { return }
            defaults.set(release.version, forKey: Self.lastPromptedKey)
            available = AvailableUpdate(version: release.version, storeURL: release.url)
        } catch {
            // Silent by design.
        }
    }

    /// Called when the user has answered the alert, and only then. Marking a
    /// version as prompted the moment it was *found* meant an app killed before
    /// the alert had a chance to render — or a launch where it never did —
    /// silently burned the one invitation that version ever gets.
    func dismiss() {
        if let version = available?.version {
            defaults.set(version, forKey: Self.lastPromptedKey)
        }
        available = nil
    }

    /// Compares dot-separated numeric versions ("0.3.0" vs "0.10.0"), padding
    /// the shorter with zeros. Non-numeric components sort as 0.
    static func isNewer(_ lhs: String, than rhs: String) -> Bool {