Licence object differs by surface: useProductContext() has no state, isEvaluation is always null, and its trialEndDate tracks read time instead of the trial end

While building licensing into a Forge app I read the licence from all three available surfaces in a single page load of a Jira admin page (UI Kit), with the app in a trial state. I expected three views of one object. They disagree, and the frontend one is unusable for the thing it looks most useful for.

Environment: staging, app.licensing.enabled: true, licence state simulated at install.

Field backend getAppContext() resolver req.context frontend useProductContext()
isActive true true absent
state "trial" "trial" absent
isEvaluation absent absent present, null
trialEndDate anchored same moves

1. The frontend cannot identify a trial

useProductContext().license has no state field, and isEvaluation is present but null. As far as I can tell that leaves no way to tell a trial from an ordinary paid licence in the frontend, except by the bare presence of trialEndDate.

Worth noting alongside that: isEvaluation is declared in the TypeScript definitions but was populated in none of the states I measured, while state — the only field that actually identifies a trial — appears in no type definition at all.

2. The frontend trialEndDate is not the trial end date, it tracks read time

This is the part that concerns me. Two loads of the same page, twelve minutes apart:

load 1   frontend trialEndDate = ...T14:01:28.613Z
load 2   frontend trialEndDate = ...T14:13:26.491Z

The delta is the wall-clock gap between the two reads. Over the same period the backend value stayed anchored at ...T12:40:13Z (install + 31 days) across four reads.

The obvious use for this field is a “your trial ends in N days” banner. Built on the frontend value, that banner shows the same N forever and never reaches zero, because the field advances with the clock. It fails silently, and it fails in the direction of telling the customer everything is fine.

One side effect worth flagging for anyone testing licensing: trial expiry cannot be reached by waiting on a simulated licence. The end never arrives. Testing expiry behaviour means a separate install with --license inactive.

Questions

  1. Is the frontend license object intentionally a reduced projection of the backend one, or is the missing state an oversight?
  2. Is the moving trialEndDate a known bug? It looks like a read-time value being returned where an install-anchored one is intended.
  3. Is the intended pattern that all licence state — including anything purely presentational, like a trial banner — is resolved in the backend and passed to the frontend through a resolver? That is what I have done and it works, but I would rather follow the intended pattern than my own workaround.

Happy to supply app and installation IDs privately if that helps.