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
- Is the frontend
licenseobject intentionally a reduced projection of the backend one, or is the missingstatean oversight? - Is the moving
trialEndDatea known bug? It looks like a read-time value being returned where an install-anchored one is intended. - 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.