Skip to content

feat: Microsoft Store update notification for MSIX users#109

Merged
nelsonduarte merged 6 commits into
mainfrom
feat/msix-store-update-notify
Jun 28, 2026
Merged

feat: Microsoft Store update notification for MSIX users#109
nelsonduarte merged 6 commits into
mainfrom
feat/msix-store-update-notify

Conversation

@nelsonduarte

Copy link
Copy Markdown
Owner

Summary

Detects when PDFApps is running as an MSIX install (Microsoft Store) and shows a Store deep-link dialog when a newer version is published, instead of running the inappropriate NSIS self-updater that MSIX sandbox would block.

Why

Until now, MSIX users hit the GitHub Releases self-updater path even though:

  • MSIX sandbox forbids executing arbitrary installers
  • Even if it ran, it would create a parallel NSIS install alongside the MSIX, breaking single-source-of-truth
  • Microsoft Store has its own auto-update, but propagation lags 1-7 days behind our releases and users want to know sooner

Architecture

  • app/store_updater.py (new, ~170 LOC):

    • is_msix_install() — detect via PACKAGE_FULL_NAME env (best-effort) OR \WindowsApps\ in executable path (reliable).
    • get_store_version() — query displaycatalog.mp.microsoft.com/v7.0/products?bigIds=9P70QGR8BSMZ, parse Packages[*].Version, return highest (trailing-zero stripped).
    • check_for_store_update() — returns (has_update, latest) with defensive 4-tuple version compare and persistent dismiss guard.
    • store_deep_link()ms-windows-store://pdp/?productid=9P70QGR8BSMZ.
  • app/updater.py: is_system_install no longer short-circuits on Windows (MSIX detected separately); check_for_update returns {"msix": True, ...} payload for MSIX users before falling back to GitHub.

  • app/window.py: _check_for_updates_async no longer hard-skips on WindowsApps; _notify_update branches on payload.get("msix"); new _notify_store_update shows 3-button dialog (Open Store / Later / Don't show again for this version) with deep-link via QDesktopServices.

  • app/i18n.py: get_dismissed_store_version / set_dismissed_store_version persist user dismissal in config.json.

Anti-spam

Without dismiss persistence, the dialog would fire every startup until the Store propagated the new version (days). 3-button UX:

  • Open Microsoft Store — opens deep-link AND dismisses
  • Later — close dialog, show again on next startup
  • Don't show again for this version — persist dismissal in config.dismissed_store_version; only re-prompts when an EVEN newer version is published

i18n

5 new keys x 8 languages (en/pt/es/fr/de/it/nl/zh):

  • update.store.title
  • update.store.message (with {version} placeholder)
  • update.store.btn.open
  • update.store.btn.later
  • update.store.btn.dismiss

Tests

tests/test_store_updater.py35 tests, all passing:

  • MSIX detection (env var, path, non-Windows, NSIS-path negative)
  • Version parsing (3/4-part, prerelease tags, empty)
  • Store API parsing (highest wins, trailing-zero strip, network/empty/invalid)
  • Dismiss-branching (skip if dismissed, show if newer than dismissed, helpers existence)
  • Defensive 4-tuple padding
  • Deep-link format
  • Source-level guards (updater branches, window handler, no WindowsApps short-circuit)
  • i18n parity (5 keys x 8 langs, {version} placeholder)

Adversarial review

The review caught a notification-spam blocker: original implementation re-fired the dialog every startup. Fixed with persistent dismiss (commit 0bd62d7). Also addressed: defensive version compare to avoid latent tuple-length asymmetry, docstring inconsistency (exec is modal, not "non-blocking"), unused test imports, and added debug log of raw response on parse failure.

Validation

  • 6 atomic commits
  • pytest unchanged baseline + 35 new (all pass)
  • No APP_VERSION bump
  • Compatible with all merged PRs (A through M + security pins + L)
  • No new external dependencies (urllib.request, json, QtGui.QDesktopServices are stdlib/PySide6 bundled)

Manual QA before release

Recommend testing on a real MSIX install (install via Microsoft Store on a Windows machine, then patch APP_VERSION to an older value locally) to confirm:

  1. is_msix_install() returns True under \WindowsApps\
  2. Dialog appears with correct translated strings
  3. "Open Microsoft Store" launches the Store app on the listing
  4. "Don't show again" persists across restart (verify config.json has dismissed_store_version)

nelsonduarte and others added 6 commits June 28, 2026 13:31
Adds app/store_updater.py with helpers to detect MSIX-packaged
execution (PACKAGE_FULL_NAME env var or \WindowsApps\ exe path)
and to query the public DisplayCatalog API for the latest
published version of listing 9P70QGR8BSMZ. Network/parse failures
are logged and return None so a missing connectivity check never
blocks app start-up.
MSIX/Microsoft Store users previously short-circuited in
is_system_install() so check_for_update() returned None and they
saw no update notification at all. They now flow through a new
branch in check_for_update() that queries the Store via
app.store_updater and returns a {"msix": True, "latest_version",
"deep_link"} payload for the UI layer to render a Store-redirect
dialog (the NSIS installer cannot run in the AppContainer sandbox
and would create a parallel installation).
Removes the WindowsApps early-return in _check_for_updates_async
so MSIX users now flow through check_for_update(). _notify_update
branches on the new {"msix": True, ...} payload and routes to a
new _notify_store_update helper that opens a QMessageBox with
Open/Later buttons; Open launches the ms-windows-store://pdp/
deep-link via QDesktopServices. _show_update_dialog also branches
so the user can re-trigger the Store dialog from the update button.
Adds update.store.title, update.store.message (with {version}
placeholder), update.store.btn.open and update.store.btn.later
for the new Microsoft Store update-notification dialog. Parity
maintained across en/pt/es/fr/de/zh/it/nl.
27 new tests cover MSIX install detection (env var + path), version
parsing, DisplayCatalog response parsing (highest version wins,
trailing .0 stripped), network/parse failure paths, has-update vs
same vs older comparisons, deep-link format, source-level guards
that app/updater.py + app/window.py branch on the MSIX payload,
and i18n parity for the four new update.store.* keys across all
eight languages.
…polish

Round 11 review caught a notification-spam regression: the Store
update dialog fires on every startup until the user upgrades, which
in MSIX (where Store propagation lags 1-7 days behind our releases)
means users see the same dialog daily.

Changes:
- New dismissed_store_version persistent flag in config.json
  managed via get_dismissed_store_version / set_dismissed_store_version
  in app/i18n.py.
- check_for_store_update returns (False, None) if user already
  dismissed the current latest (or newer).
- _notify_store_update now offers 3 buttons: Open Store (opens +
  dismisses), Later (re-show), Don't show again for this version
  (persistent dismiss). Docstring corrected (was claimed non-blocking).
- New i18n key update.store.btn.dismiss x 8 languages.
- Defensive version compare: pad both sides to 4-tuple to avoid
  the latent tuple-length asymmetry bug (1.13.16.0 > 1.13.16).
- Removed unused 'import io' and 'import pytest' from tests.
- Documented PACKAGE_FULL_NAME detection as best-effort fallback.
- Added _log.debug of raw response snippet on parse failure.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@nelsonduarte nelsonduarte added the enhancement New feature or request label Jun 28, 2026
Comment thread app/store_updater.py
# because data may be unrepresentable.
try:
_log.debug("Raw response prefix: %s", str(data)[:500])
except Exception:
Comment thread app/window.py
t("update.store.btn.open"),
QMessageBox.ButtonRole.AcceptRole,
)
later_btn = box.addButton(
@nelsonduarte nelsonduarte merged commit 6098ada into main Jun 28, 2026
3 checks passed
@nelsonduarte nelsonduarte deleted the feat/msix-store-update-notify branch June 28, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants