Skip to content

Keep the license code when the server rejects it - #20

Merged
shazzad merged 2 commits into
mainfrom
fix/preserve-license-code-on-invalid
Aug 5, 2026
Merged

shazzad merged 2 commits into
mainfrom
fix/preserve-license-code-on-invalid

Conversation

@shazzad

@shazzad shazzad commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Closes #19

Summary

invalid_license is returned by the server for any unmatched code + product_id pair — a site pointed at the wrong product, a license row removed by mistake, a product recreated — not only for a genuinely revoked license. Enforcement is server-side regardless (UpdatesController blanks the package URL, DownloadController 404s), so deleting the customer's only copy of the key achieved nothing while turning a recoverable state into a support round-trip.

  • Tracker::sync_license_data() and Admin::handle_sync() now call the new Integration::mark_license_invalid(), which sets status: invalid on the stored data and preserves everything else — notably renewal_url, so the panel can still offer a renewal link.
  • Admin::handle_save() no longer touches the stored license when a submitted key is rejected. The submitted key is only written in the success branch (src/Admin.php:203), so the old failure path deleted the working key and stored nothing — one typo, or one server blip, and the site was left with no license at all.
  • The admin panel explains an unrecognised key instead of falling through to "Upgrade package file missing, unable to upgrade", which is what an invalid license used to render.
  • The deliberate empty-field deactivation (src/Admin.php:171) is unchanged — that is a user asking to remove their license.

Outage handling was already correct and is untouched: a 502 decodes to wprepo_api_fail, a timeout to http_request_failed, a deactivated repo plugin to rest_no_route, and the sales-channel providers return wprepo_api_error on upstream failure. None reach this branch.

Test plan

  • invalid_license marks data invalid and keeps the code (TrackerSyncLicenseTest)
  • A transient API failure touches neither the code nor the data
  • A valid response still stores the returned license data
  • mark_license_invalid() preserves renewal_url/buyer_email, works with no stored data, and never calls delete_option
  • Full suite: 68 tests, 123 assertions, green
  • Verified the new tests fail against the old source (3 errors + 1 failure) and pass with the change
  • phpcs unchanged except one added short-array-syntax hit matching the file's existing style

Not included

Deferred per discussion: gating automatic clearing behind N consecutive failures. With deletion removed entirely there is nothing left to debounce.

🤖 Generated with Claude Code

invalid_license is returned for any unmatched code/product pair — a
site pointed at the wrong product, a license row removed by mistake —
not only for a revoked license. Enforcement is server-side either way,
so deleting the customer's only copy of the code achieved nothing while
turning a recoverable state into a support round-trip.

- Mark stored license data status: invalid instead of deleting, keeping
  renewal_url and the rest intact
- Leave a previously working license untouched when a newly submitted
  key is rejected; the submitted key is only stored once it verifies, so
  the failure path was deleting the old one and storing nothing
- Keep the deliberate empty-field deactivation as the one path that
  still deletes
- Explain an unrecognised key on the admin panel rather than reporting a
  missing upgrade package

Refs #19

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@shazzad
shazzad merged commit 42714ca into main Aug 5, 2026
1 check passed
@shazzad
shazzad deleted the fix/preserve-license-code-on-invalid branch August 5, 2026 00:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deleting the stored license code on invalid_license destroys recoverable state

1 participant