@easygov/react-components 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -76,9 +76,10 @@ that runs it.
76
76
 
77
77
  ### Reading the header context
78
78
 
79
- The header resolves the signed-in user, the selected customer account, and the running process
80
- or the parent process a sub-process is being created under, from core-backend and publishes them to
81
- consumers:
79
+ The header sends the current screen URL (origin and path) to core-backend, which resolves the
80
+ signed-in user, the selected customer account, the ServiceApp the screen belongs to, and the running
81
+ process or the parent process a sub-process is being created under. The header publishes the result
82
+ to consumers:
82
83
 
83
84
  ```tsx
84
85
  import { useEasygovHeaderContext } from '@easygov/react-components';
@@ -86,26 +87,60 @@ import { useEasygovHeaderContext } from '@easygov/react-components';
86
87
  function ContactForm() {
87
88
  const { claims } = useEasygovHeaderContext();
88
89
  const account = claims?.customerAccount;
89
- const parentProcess = claims?.parentProcess;
90
+ const parentProcessId = claims?.parentProcessId;
90
91
 
91
92
  return (
92
93
  <form>
93
94
  <input defaultValue={claims?.currentUser.firstName ?? ''} />
94
95
  <input defaultValue={account?.label ?? ''} />
95
96
  {account?.accountType === 'ORGANIZATION' && <input defaultValue={account.uid ?? ''} />}
96
- <input defaultValue={parentProcess?.id ?? ''} />
97
+ <input defaultValue={parentProcessId ?? ''} />
97
98
  </form>
98
99
  );
99
100
  }
100
101
  ```
101
102
 
102
- `claims.process` and `claims.parentProcess` are mutually exclusive. `process` is the Behördengang
103
- the URL selects (`/process/{id}`). `parentProcess` is the Behördengang a sub-process is about to be
104
- created under (`/parentprocess/{id}`) — not a selected Behördengang. Neither is present when the
105
- URL addresses no process. Each `SelectedProcess` carries its own `parentProcessId`, the global
106
- identifier of its parent, absent for a top-level Behördengang. That reference is the only
107
- representation of a selected process's parent: the context carries no ancestor chain, because it
108
- is forwarded as an HTTP header where 4/8 KB size limits apply.
103
+ `claims.serviceApp` is the ServiceApp the current screen belongs to, resolved by core from the URL's
104
+ subdomain and path. It is absent on core and info pages and on a URL that matches no registered
105
+ ServiceApp.
106
+
107
+ `claims.process` and `claims.parentProcessId` are mutually exclusive. `process` is the Behördengang
108
+ the URL selects (`/process/{id}`); it references its ServiceApp by `serviceAppId` and its own parent
109
+ by `parentProcessId`, absent for a top-level Behördengang. `parentProcessId` at the top level is the
110
+ Behördengang a sub-process is about to be created under (`/parentprocess/{id}`) — not a selected
111
+ Behördengang. Neither is present when the URL addresses no process. The context carries no ancestor
112
+ chain and no full parent object, because it is forwarded as an HTTP header where 4/8 KB size limits
113
+ apply.
114
+
115
+ For an Agent (Mitwirkende/r), `customerAccount.managementRole.servicePermission` is the Agent's
116
+ permission on the current ServiceApp (`READ_ONLY`, `EDIT` or `AUTHORIZE`). It is absent for a
117
+ SuperAgent, on a screen without a current ServiceApp, for a ServiceApp with the individual permission
118
+ model (the fach module manages those rights itself), and when the Agent holds no permission on the
119
+ app. An absent permission never means "allowed".
120
+
121
+ The header refetches the context whenever the URL's pathname changes, including client-side router
122
+ navigations; a change of only the query string or fragment does not refetch.
123
+
124
+ #### The context is renewed before it expires
125
+
126
+ `contextJwt` is short-lived: core-backend signs it with an `exp` (5 minutes by default). Without a
127
+ renewal, a page that stays on one URL would send an expired token to its backend. So the header
128
+ fetches a new context about 60 seconds before `exp` and keeps doing so for as long as the user is
129
+ signed in. Plan for these consequences:
130
+
131
+ - **Forward `contextJwt` fresh.** Read it from the hook (or the latest event) when you make a
132
+ request; don't capture it once at mount.
133
+ - **A renewal is not a change.** Each renewal delivers a new `contextJwt`, but when the context
134
+ itself is unchanged, `claims` is the **same object** as before. An effect or memo keyed on
135
+ `claims` therefore does not re-run, and a form you prefilled from it keeps what the user typed.
136
+ Read `claims` for what the context says, `contextJwt` for the token.
137
+ - **Never reset user input from `onContextChange` or the `contextchange` event** without checking
138
+ whether the content changed (below). Both fire on every renewal, roughly every 4 minutes.
139
+ - **A failed renewal is retried quietly** every 15 seconds while the old token is still valid.
140
+ Only if the token expires without a successful renewal, or core refuses the request for good
141
+ (for example `customer-account-not-accessible`), the header shows its error dialog, once per
142
+ outage. The previous `claims` stay in place meanwhile. A failed fetch after a navigation shows
143
+ the dialog at once and is then retried the same way; a refusal for good is not retried.
109
144
 
110
145
  `claims` is `null` until the first response lands and again after logout — render a fallback,
111
146
  don't assume it is present on first paint. The hook reflects the last context an `EasygovHeader`
@@ -117,10 +152,22 @@ Outside React — including `<eg-easygov-header>` — listen for the event inste
117
152
 
118
153
  ```js
119
154
  document.addEventListener('contextchange', (e) => {
120
- const { contextJwt, claims } = e.detail;
155
+ const { contextJwt, claims, contentChanged } = e.detail;
156
+ if (contentChanged) {
157
+ prefillForm(claims); // a real change: new account, process, ...
158
+ }
159
+ // Always keep the newest token for requests to your backend.
160
+ latestContextJwt = contextJwt;
121
161
  });
122
162
  ```
123
163
 
164
+ `contentChanged` is `true` for the first context after sign-in and whenever `claims` differs from
165
+ the previous event; it is `false` when the event only delivers a renewed token. Prefill only when
166
+ it is `true`, otherwise every renewal overwrites what the user has typed since. In TypeScript the
167
+ detail is typed as `HeaderContextChangeDetail`, exported by both `@easygov/react-components` and
168
+ `@easygov/web-components`. The `onContextChange` callback has no such flag —
169
+ compare `claims` by identity there (`claims !== previousClaims`).
170
+
124
171
  **The claims are decoded, not verified.** Use them for prefilling and rendering. Any backend
125
172
  that receives `contextJwt` must verify its signature against core-backend's published JWKS
126
173
  before trusting a claim.
@@ -326,11 +326,7 @@ declare function getAuthStatus(): AuthStatus;
326
326
  declare function requireSession(): void;
327
327
 
328
328
  type ManagementRoleType = 'SUPER_AGENT' | 'AGENT';
329
- /**
330
- * Verification level of a customer account's identity, and — as `ServiceApp.requiredAccountStatus`
331
- * — the minimum level a service demands. One type for both because the knockout rule compares
332
- * them to each other.
333
- */
329
+ /** Verification level of a customer account's identity. */
334
330
  type AccountVerificationStatus = 'NOT_VERIFIED' | 'VERIFIED';
335
331
  /**
336
332
  * Verification of a management-role grant. Same values as `AccountVerificationStatus` today, but
@@ -339,16 +335,33 @@ type AccountVerificationStatus = 'NOT_VERIFIED' | 'VERIFIED';
339
335
  */
340
336
  type ManagementRoleStatus = 'NOT_VERIFIED' | 'VERIFIED';
341
337
  type ProcessStatus = 'PRELIMINARY' | 'REQUEST_IN_PROGRESS' | 'REQUEST_COMPLETED' | 'SERVICE_PROVISION_PENDING' | 'FINISHED' | 'CANCELED';
338
+ /** An Agent's permission level on one ServiceApp (Leistungsberechtigung). */
339
+ type PermissionLevel = 'READ_ONLY' | 'EDIT' | 'AUTHORIZE';
340
+ /**
341
+ * An Agent's ServicePermission for the current ServiceApp. Decoded from the context JWT, signature
342
+ * unverified: safe to render, never to authorize.
343
+ */
344
+ interface ServicePermission {
345
+ /** Always the current ServiceApp's id (`HeaderContextClaims.serviceApp.id`). */
346
+ serviceAppId: string;
347
+ permissionLevel: PermissionLevel;
348
+ }
342
349
  /**
343
350
  * The caller's management role on the selected account: SUPER_AGENT (Kontoverwalter/in,
344
- * unrestricted) or AGENT (Mitwirkende/r, restricted by per-service permissions, which this
345
- * context does NOT carry).
351
+ * unrestricted) or AGENT (Mitwirkende/r, restricted by per-service permissions).
346
352
  * @see HeaderContextClaims for unverified-claims boundary; roleType and status must not drive authorization.
347
353
  */
348
354
  interface ManagementRole {
349
355
  roleType: ManagementRoleType;
350
356
  /** Only present for SUPER_AGENT. */
351
357
  status?: ManagementRoleStatus;
358
+ /**
359
+ * Only for an AGENT, and only for the current ServiceApp. Absent for a SUPER_AGENT, on a screen
360
+ * without a current ServiceApp, for an app with the individual permission model (the fach module
361
+ * manages those rights), and when the Agent holds no permission on the app. Absent never means
362
+ * "allowed".
363
+ */
364
+ servicePermission?: ServicePermission;
352
365
  }
353
366
  interface CustomerAccountBase {
354
367
  id: string;
@@ -377,18 +390,23 @@ interface IndividualAccount extends CustomerAccountBase {
377
390
  */
378
391
  type SelectedCustomerAccount = OrganizationAccount | IndividualAccount;
379
392
  /**
380
- * The selected Behördengang with its resolved ServiceApp.
393
+ * The ServiceApp the current screen belongs to, resolved by core from the screen URL.
394
+ * Decoded from the context JWT, signature unverified: safe to render, never to authorize.
395
+ */
396
+ interface ServiceApp {
397
+ id: string;
398
+ /** The fach module, not core, manages the Agents' rights for this ServiceApp. */
399
+ individualServicePermissionModel: boolean;
400
+ }
401
+ /**
402
+ * The selected Behördengang. Its ServiceApp is a reference only; on a screen of that ServiceApp
403
+ * it equals `HeaderContextClaims.serviceApp.id`.
381
404
  * Decoded from the context JWT, signature unverified: safe to render, never to authorize.
382
405
  */
383
406
  interface SelectedProcess {
384
407
  id: string;
385
408
  status: ProcessStatus;
386
- serviceApp: {
387
- id: string;
388
- /** Minimum `CustomerAccount.status` the service requires (knockout). */
389
- requiredAccountStatus: AccountVerificationStatus;
390
- individualServicePermissionModel: boolean;
391
- };
409
+ serviceAppId: string;
392
410
  /**
393
411
  * Global identifier of this process's own parent. Absent for a top-level Behördengang. This
394
412
  * reference is the only representation of a selected process's parent: the context carries no
@@ -420,15 +438,35 @@ interface HeaderContextClaims {
420
438
  currentUser: UserProfile;
421
439
  customerAccount?: SelectedCustomerAccount;
422
440
  /**
423
- * The Behördengang the URL selects (`/process/{id}`). Mutually exclusive with `parentProcess`.
441
+ * The ServiceApp the current screen belongs to. Absent on core and info pages and on a URL that
442
+ * matches no registered ServiceApp.
443
+ */
444
+ serviceApp?: ServiceApp;
445
+ /**
446
+ * The Behördengang the URL selects (`/process/{id}`). Mutually exclusive with `parentProcessId`.
424
447
  */
425
448
  process?: SelectedProcess;
426
449
  /**
427
- * The Behördengang a sub-process is about to be created under (`/parentprocess/{id}`).
428
- * Mutually exclusive with `process`, and NOT a selected Behördengang. A selected process's own
429
- * parent is never included as an object — only as `process.parentProcessId`.
450
+ * The Behördengang a sub-process is about to be created under (`/parentprocess/{id}`), by id
451
+ * only. Mutually exclusive with `process`. A selected process's own parent is
452
+ * `process.parentProcessId`.
430
453
  */
431
- parentProcess?: SelectedProcess;
454
+ parentProcessId?: string;
455
+ }
456
+ /**
457
+ * `detail` of the `contextchange` CustomEvent. The event fires for every new JWT, which includes
458
+ * the background refresh that renews it shortly before it expires; `contentChanged` says whether
459
+ * `claims` differs from what the previous event delivered.
460
+ */
461
+ interface HeaderContextChangeDetail {
462
+ contextJwt: string;
463
+ claims: HeaderContextClaims;
464
+ /**
465
+ * `true` for the first context of a session and whenever `claims` differs from the previous
466
+ * event's. `false` for a refresh that only renewed the token: prefilling a form from `claims`
467
+ * on such an event would overwrite what the user has typed since.
468
+ */
469
+ contentChanged: boolean;
432
470
  }
433
471
 
434
472
  interface EasygovHeaderContext {
@@ -575,6 +613,11 @@ declare function useEasygovAuthStatus(): AuthStatus;
575
613
  * still describe a previously selected account after the URL has moved on. Only sign-out (or a
576
614
  * failed *first* fetch) leaves `claims` at `null`.
577
615
  *
616
+ * `contextJwt` is renewed shortly before it expires (5 minutes by default), so it changes about
617
+ * every 4 minutes while the page stays put. `claims` keeps its identity across a renewal that
618
+ * changed nothing else, so an effect or memo keyed on `claims` does not re-run — read
619
+ * `contextJwt` when forwarding the token, `claims` when rendering or prefilling.
620
+ *
578
621
  * IMPORTANT — trust boundary: `claims` is decoded, NOT signature-verified. Use it to prefill
579
622
  * forms and drive rendering; never to authorize. A backend receiving `contextJwt` MUST verify it
580
623
  * against core-backend's published JWKS.
@@ -606,6 +649,14 @@ interface EasygovHeaderProps {
606
649
  * `header-context` endpoint (mirrored by the `contextchange` CustomEvent
607
650
  * dispatched on the header element), together with its decoded `claims`.
608
651
  *
652
+ * IMPORTANT — fires on every renewal, not only when the context changes. The JWT is short-lived
653
+ * (5 minutes by default), so the header fetches a new one shortly before it expires; that
654
+ * arrives here as a new `contextJwt`. When the refreshed context equals the previous one,
655
+ * `claims` is the very same object as in the previous call, so `claims !== previousClaims`
656
+ * tells a real change from a renewal. Never reset user input (a prefilled form) from this
657
+ * callback without that check. The `contextchange` event carries the same answer as
658
+ * `detail.contentChanged`.
659
+ *
609
660
  * Both are always present when this fires: a payload that fails to decode never produces a
610
661
  * `contextJwt`, so a decode failure suppresses the call rather than delivering null claims.
611
662
  *
@@ -665,4 +716,4 @@ declare function getEasygovDisplayLanguage(): Locale;
665
716
  declare function setEasygovDisplayLanguage(locale: Locale): void;
666
717
  declare function subscribeEasygovDisplayLanguage(listener: () => void): () => void;
667
718
 
668
- export { type AuthStatus, BrandBase, BrandEasygov, BrandFacebook, BrandKdk, BrandLinkedin, type BrandProps, BrandSeg, BrandSgv, BrandSsv, BrandSwiss, BrandXing, BrandYoutube, Button, type ButtonProps, type DesktopSpan, Dropdown, DropdownChip, DropdownChipsOverflow, type DropdownChipsOverflowProps, DropdownChipsTrigger, type DropdownChipsTriggerProps, DropdownCollection, DropdownEmpty, DropdownGroup, DropdownGroupLabel, DropdownHelperText, type DropdownHelperTextProps, DropdownInput, DropdownItem, type DropdownItemProps, DropdownItemText, DropdownLabel, type DropdownLabelProps, DropdownList, DropdownPopup, type DropdownPopupProps, DropdownSeparator, DropdownToggle, DropdownTrigger, type DropdownTriggerProps, DropdownValue, EasygovFooter, EasygovHeader, type EasygovHeaderContext, type EasygovHeaderProps, type EasygovSession, type EasygovVersionInfo, ErrorDialog, type ErrorDialogProps, Grid, GridItem, type GridItemProps, type GridProps, type AccountVerificationStatus as HeaderContextAccountVerificationStatus, type HeaderContextClaims, type IndividualAccount as HeaderContextIndividualAccount, type ManagementRole as HeaderContextManagementRole, type ManagementRoleStatus as HeaderContextManagementRoleStatus, type ManagementRoleType as HeaderContextManagementRoleType, type OrganizationAccount as HeaderContextOrganizationAccount, type ProcessStatus as HeaderContextProcessStatus, type UserProfile as HeaderContextUserProfile, Heading, type HeadingProps, Input, type InputProps, type Locale, type MobileSpan, NavigationMenu, NavigationMenuAnchor, type NavigationMenuAnchorProps, NavigationMenuContent, type NavigationMenuContentProps, NavigationMenuItem, type NavigationMenuItemProps, NavigationMenuLink, type NavigationMenuLinkProps, NavigationMenuList, type NavigationMenuListProps, type NavigationMenuProps, NavigationMenuTrigger, type NavigationMenuTriggerProps, RequireCustomerAccount, type RequireCustomerAccountProps, RequireSession, type RequireSessionProps, Select, SelectContent, type SelectContentProps, SelectGroup, SelectGroupLabel, type SelectGroupLabelProps, SelectHelperText, type SelectHelperTextProps, SelectItem, type SelectItemProps, SelectLabel, type SelectLabelProps, SelectSeparator, type SelectSeparatorProps, SelectTrigger, type SelectTriggerProps, type SelectedCustomerAccount, type SelectedProcess, type SessionUser, type TabletSpan, Text, TextLink, type TextLinkProps, type TextProps, buttonVariants, getAuthStatus, getEasygovDisplayLanguage, headingVariants, requireSession, setEasygovBusinessModuleVersion, setEasygovDisplayLanguage, subscribeEasygovDisplayLanguage, textLinkVariants, textVariants, useEasygovAuthStatus, useEasygovDisplayLanguage, useEasygovHeaderContext, useEasygovSession, useEasygovVersionInfo, useRequireCustomerAccount };
719
+ export { type AuthStatus, BrandBase, BrandEasygov, BrandFacebook, BrandKdk, BrandLinkedin, type BrandProps, BrandSeg, BrandSgv, BrandSsv, BrandSwiss, BrandXing, BrandYoutube, Button, type ButtonProps, type DesktopSpan, Dropdown, DropdownChip, DropdownChipsOverflow, type DropdownChipsOverflowProps, DropdownChipsTrigger, type DropdownChipsTriggerProps, DropdownCollection, DropdownEmpty, DropdownGroup, DropdownGroupLabel, DropdownHelperText, type DropdownHelperTextProps, DropdownInput, DropdownItem, type DropdownItemProps, DropdownItemText, DropdownLabel, type DropdownLabelProps, DropdownList, DropdownPopup, type DropdownPopupProps, DropdownSeparator, DropdownToggle, DropdownTrigger, type DropdownTriggerProps, DropdownValue, EasygovFooter, EasygovHeader, type EasygovHeaderContext, type EasygovHeaderProps, type EasygovSession, type EasygovVersionInfo, ErrorDialog, type ErrorDialogProps, Grid, GridItem, type GridItemProps, type GridProps, type AccountVerificationStatus as HeaderContextAccountVerificationStatus, type HeaderContextChangeDetail, type HeaderContextClaims, type IndividualAccount as HeaderContextIndividualAccount, type ManagementRole as HeaderContextManagementRole, type ManagementRoleStatus as HeaderContextManagementRoleStatus, type ManagementRoleType as HeaderContextManagementRoleType, type OrganizationAccount as HeaderContextOrganizationAccount, type PermissionLevel as HeaderContextPermissionLevel, type ProcessStatus as HeaderContextProcessStatus, type ServiceApp as HeaderContextServiceApp, type ServicePermission as HeaderContextServicePermission, type UserProfile as HeaderContextUserProfile, Heading, type HeadingProps, Input, type InputProps, type Locale, type MobileSpan, NavigationMenu, NavigationMenuAnchor, type NavigationMenuAnchorProps, NavigationMenuContent, type NavigationMenuContentProps, NavigationMenuItem, type NavigationMenuItemProps, NavigationMenuLink, type NavigationMenuLinkProps, NavigationMenuList, type NavigationMenuListProps, type NavigationMenuProps, NavigationMenuTrigger, type NavigationMenuTriggerProps, RequireCustomerAccount, type RequireCustomerAccountProps, RequireSession, type RequireSessionProps, Select, SelectContent, type SelectContentProps, SelectGroup, SelectGroupLabel, type SelectGroupLabelProps, SelectHelperText, type SelectHelperTextProps, SelectItem, type SelectItemProps, SelectLabel, type SelectLabelProps, SelectSeparator, type SelectSeparatorProps, SelectTrigger, type SelectTriggerProps, type SelectedCustomerAccount, type SelectedProcess, type SessionUser, type TabletSpan, Text, TextLink, type TextLinkProps, type TextProps, buttonVariants, getAuthStatus, getEasygovDisplayLanguage, headingVariants, requireSession, setEasygovBusinessModuleVersion, setEasygovDisplayLanguage, subscribeEasygovDisplayLanguage, textLinkVariants, textVariants, useEasygovAuthStatus, useEasygovDisplayLanguage, useEasygovHeaderContext, useEasygovSession, useEasygovVersionInfo, useRequireCustomerAccount };