Skip to content

[NRT-903] Claim the trial-ended message when showing the upsell - #1375

Merged
ddevdan merged 5 commits into
mainfrom
NRT-903-claim-trial-ended-message
Aug 25, 2026
Merged

[NRT-903] Claim the trial-ended message when showing the upsell#1375
ddevdan merged 5 commits into
mainfrom
NRT-903-claim-trial-ended-message

Conversation

@ddevdan

@ddevdan ddevdan commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Related issues

  • Closes NRT-903
  • Server side: AnkiHubSoftware/ankihub#3910 — removes the field from user details and adds the claim endpoint. Must be deployed before this PR is released.

Proposed changes

  • Add AnkiHubClient.claim_trial_ended_message() for POST /users/me/trial-ended-message/claim.
  • Claim the message when the upsell is displayed and pick the trial-ended title from the result. Restores copy that became unreachable once the server stopped sending show_trial_ended_message in user details.
  • Fail soft: a failed claim logs a warning and shows the generic copy. Needs an explicit .failure() handler because AddonQueryOp re-raises by default.
  • Report upgrade_cta_viewed / upgrade_cta_clicked with source and surface matching the web taxonomy. The native dialog never loads a webview, so the add-on CTA was previously invisible to analytics.

How to reproduce

  1. Sign in as a user without Smart Search access whose trial has ended.
  2. Open a deck and click the Smart Search button.
  3. The upsell shows "Your Trial Has Ended! 🎓✨"; reopening shows the generic title.

Further comments

Depends on the server endpoint, which is implemented but not yet in production — ship server first. Until then the fail-soft path shows the generic copy.

The tests/addon suite was not run locally (segfaults on macOS); CI is the first check on those three tests.

The trial-ended message is one-shot: reading the server-side property marks it
shown. It was serialized by GET /users/me, so any read of user state spent it
with no UI involved. The field has been removed from that endpoint, so the
add-on now claims the message explicitly at the point it displays it.

POST rather than GET because the call has that effect - a retry, prefetch or
proxy of a GET would spend a user's only message.
Restores the trial-ended upsell copy, which became unreachable once the server
stopped sending show_trial_ended_message in the user details. The claim runs in
the background and the dialog is built from its callbacks, so the UI thread is
never blocked and the message is only ever spent by the path that displays it.

AddonQueryOp replaces QueryOp's failure handler with one that re-raises into the
central error handler, so failing soft needs an explicit .failure() handler: a
failed claim logs a warning and shows the generic copy rather than an error
dialog. That also covers running against a server without the endpoint.

Also reports upgrade_cta_viewed and upgrade_cta_clicked to the product metrics
collector. The source property carries the copy the user actually saw and is
derived once per dialog, so the two events cannot disagree. Property names and
values mirror the web upsell's taxonomy so the channels can be reconciled.
Covers the three claim outcomes - owed, not owed, and a failed claim - asserting
the title, the source and surface on both events, and that a failure still opens
the dialog with the generic copy.

Also pins down two things the server cannot observe: that the access-granted path
never claims the message, and that dismissing the dialog reports no click.
@ddevdan
ddevdan requested a review from a team August 18, 2026 19:05
The parametrized claim_failed case built AnkiHubHTTPError around a Mock. The
failure handler logs str(exc), and AnkiHubHTTPError.__str__ reads the response
headers and body, which raised TypeError on a Mock. The handler died before it
could show the dialog, so the test timed out waiting for it.

Matches how the rest of this file builds error responses.

@cacoze cacoze left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two nits from reading this next to the server PR. Neither is a merge blocker — the claim-at-display-time path looks right.

_track_upsell_event("upgrade_cta_viewed", source)


def _show_upsell(user_details: dict, parent=aqt.mw) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user_details is unused now. I'd drop it from the signature and change the call site to lambda _: _show_upsell(parent).

Keeping the argument makes it look like the copy still comes from GET /users/me, which is the thing this PR is stopping.


# Third value in the web's `surface` taxonomy, alongside `web_app` (browser) and
# `anki_webview` (webview hosted in Anki), which are set in _analytics_state.html:17.
# Note that Smart Search's webview is also served inside the add-on and reports

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The anki_addon vs web_app / anki_webview distinction is worth keeping — that's the contract.

The pointer at _analytics_state.html:17 (and ModalUpsellContent.html:40,47 a few lines below) lives in the Django repo, so the path and line numbers will rot and aren't greppable from here. Naming the properties (source, surface) is enough.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed both

- `_show_upsell` no longer takes `user_details`; the copy comes from the
  claim endpoint, not from `GET /users/me`.
- Drop the Django-repo file/line pointers from the taxonomy comments; name
  the `source` and `surface` properties instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ddevdan
ddevdan requested a review from cacoze August 20, 2026 22:02
@cacoze

cacoze commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Safe to merge as https://github.com/AnkiHubSoftware/ankihub/pull/3910 was already deployed

@ddevdan
ddevdan merged commit 6ca4ee4 into main Aug 25, 2026
10 checks passed
@ddevdan
ddevdan deleted the NRT-903-claim-trial-ended-message branch August 25, 2026 16:33
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.

2 participants