@owlmeans/web-marketing-consent 0.1.18-rc.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 +15 -0
- package/agent-meta/manifest.json +16 -0
- package/agent-meta/skills/web-marketing-consent/SKILL.md +245 -0
- package/build/@/components/ui/button.d.ts +11 -0
- package/build/@/components/ui/button.d.ts.map +1 -0
- package/build/@/components/ui/button.js +32 -0
- package/build/@/components/ui/button.js.map +1 -0
- package/build/@/components/ui/card.d.ts +10 -0
- package/build/@/components/ui/card.d.ts.map +1 -0
- package/build/@/components/ui/card.js +25 -0
- package/build/@/components/ui/card.js.map +1 -0
- package/build/@/components/ui/label.d.ts +5 -0
- package/build/@/components/ui/label.d.ts.map +1 -0
- package/build/@/components/ui/label.js +8 -0
- package/build/@/components/ui/label.js.map +1 -0
- package/build/@/lib/utils.d.ts +3 -0
- package/build/@/lib/utils.d.ts.map +1 -0
- package/build/@/lib/utils.js +6 -0
- package/build/@/lib/utils.js.map +1 -0
- package/build/append.d.ts +32 -0
- package/build/append.d.ts.map +1 -0
- package/build/append.js +28 -0
- package/build/append.js.map +1 -0
- package/build/bridge.d.ts +25 -0
- package/build/bridge.d.ts.map +1 -0
- package/build/bridge.js +69 -0
- package/build/bridge.js.map +1 -0
- package/build/components/fields.d.ts +18 -0
- package/build/components/fields.d.ts.map +1 -0
- package/build/components/fields.js +22 -0
- package/build/components/fields.js.map +1 -0
- package/build/components/index.d.ts +3 -0
- package/build/components/index.d.ts.map +1 -0
- package/build/components/index.js +3 -0
- package/build/components/index.js.map +1 -0
- package/build/components/preferences.d.ts +19 -0
- package/build/components/preferences.d.ts.map +1 -0
- package/build/components/preferences.js +33 -0
- package/build/components/preferences.js.map +1 -0
- package/build/components/screen.d.ts +11 -0
- package/build/components/screen.d.ts.map +1 -0
- package/build/components/screen.js +35 -0
- package/build/components/screen.js.map +1 -0
- package/build/consts.d.ts +26 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +27 -0
- package/build/consts.js.map +1 -0
- package/build/entrypoints.d.ts +12 -0
- package/build/entrypoints.d.ts.map +1 -0
- package/build/entrypoints.js +16 -0
- package/build/entrypoints.js.map +1 -0
- package/build/hooks/use-marketing-consent.d.ts +38 -0
- package/build/hooks/use-marketing-consent.d.ts.map +1 -0
- package/build/hooks/use-marketing-consent.js +96 -0
- package/build/hooks/use-marketing-consent.js.map +1 -0
- package/build/i18n/be.json +19 -0
- package/build/i18n/de.json +19 -0
- package/build/i18n/en.json +19 -0
- package/build/i18n/es.json +19 -0
- package/build/i18n/fr.json +19 -0
- package/build/i18n/pl.json +19 -0
- package/build/i18n/ru.json +19 -0
- package/build/i18n/uk.json +19 -0
- package/build/i18n.d.ts +2 -0
- package/build/i18n.d.ts.map +1 -0
- package/build/i18n.js +25 -0
- package/build/i18n.js.map +1 -0
- package/build/index.d.ts +11 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +11 -0
- package/build/index.js.map +1 -0
- package/build/landing.d.ts +22 -0
- package/build/landing.d.ts.map +1 -0
- package/build/landing.js +68 -0
- package/build/landing.js.map +1 -0
- package/build/service.d.ts +40 -0
- package/build/service.d.ts.map +1 -0
- package/build/service.js +80 -0
- package/build/service.js.map +1 -0
- package/build/step.d.ts +10 -0
- package/build/step.d.ts.map +1 -0
- package/build/step.js +16 -0
- package/build/step.js.map +1 -0
- package/components.json +21 -0
- package/package.json +77 -0
- package/src/@/components/ui/button.tsx +60 -0
- package/src/@/components/ui/card.tsx +93 -0
- package/src/@/components/ui/label.tsx +23 -0
- package/src/@/globals.css +57 -0
- package/src/@/lib/utils.ts +6 -0
- package/src/append.ts +62 -0
- package/src/bridge.ts +89 -0
- package/src/components/fields.tsx +101 -0
- package/src/components/index.ts +2 -0
- package/src/components/preferences.tsx +69 -0
- package/src/components/screen.tsx +90 -0
- package/src/consts.ts +31 -0
- package/src/entrypoints.ts +17 -0
- package/src/hooks/use-marketing-consent.ts +150 -0
- package/src/i18n/be.json +19 -0
- package/src/i18n/de.json +19 -0
- package/src/i18n/en.json +19 -0
- package/src/i18n/es.json +19 -0
- package/src/i18n/fr.json +19 -0
- package/src/i18n/pl.json +19 -0
- package/src/i18n/ru.json +19 -0
- package/src/i18n/uk.json +19 -0
- package/src/i18n.ts +26 -0
- package/src/index.ts +10 -0
- package/src/landing.ts +80 -0
- package/src/service.ts +131 -0
- package/src/step.ts +21 -0
- package/tests/bridge.spec.ts +100 -0
- package/tests/context.ts +48 -0
- package/tests/harness/frame.html +16 -0
- package/tests/harness/index.html +11 -0
- package/tests/harness/mount.tsx +108 -0
- package/tests/helpers.ts +88 -0
- package/tests/i18n.spec.ts +63 -0
- package/tests/package-boundary.spec.ts +19 -0
- package/tests/preferences.spec.ts +70 -0
- package/tests/screen.spec.ts +146 -0
- package/tests/service.spec.ts +80 -0
- package/tests/tsconfig.json +12 -0
- package/tsconfig.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!-- owlmeans:agent-guidance:start -->
|
|
2
|
+
## Agent guidance
|
|
3
|
+
|
|
4
|
+
This package ships embedded agent skills under `agent-meta/`. After installing your
|
|
5
|
+
`@owlmeans/*` packages, run the OwlMeans agent-skills installer to place them into
|
|
6
|
+
your project's skill store (`.agents/skills/`):
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.33
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The embedded files are version-matched to this package release. Do not edit them
|
|
13
|
+
directly — they are regenerated on each publish. To contribute guidance edits,
|
|
14
|
+
open a PR against the source monorepo.
|
|
15
|
+
<!-- owlmeans:agent-guidance:end -->
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"package": "@owlmeans/web-marketing-consent",
|
|
4
|
+
"version": "0.1.18-rc.0",
|
|
5
|
+
"generatedAt": "2026-09-22T17:37:10.922Z",
|
|
6
|
+
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
|
+
"entries": [
|
|
8
|
+
{
|
|
9
|
+
"kind": "skill",
|
|
10
|
+
"name": "web-marketing-consent",
|
|
11
|
+
"category": "package-specific",
|
|
12
|
+
"file": "skills/web-marketing-consent/SKILL.md",
|
|
13
|
+
"canonicalPath": ".agents/skills/web-marketing-consent/SKILL.md"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web-marketing-consent
|
|
3
|
+
description: How to use @owlmeans/web-marketing-consent — the browser half of @owlmeans/marketing-consent: the post-sign-in privacy-choices screen, a reusable MarketingConsentPreferences settings-card body, appendMarketingConsent's login step and landing hooks, the MarketingConsentClientService (fail-open, signed-out-safe), and the cookie-consent bridge to @owlmeans/consent. Auto-invoked when wiring the marketing-consent screen into a web app, rendering a settings card for privacy choices, reconciling cookie-consent decisions with saved account decisions, or diagnosing a person stuck on (or never shown) the marketing-consent step.
|
|
4
|
+
user-invocable: false
|
|
5
|
+
---
|
|
6
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
7
|
+
|
|
8
|
+
# @owlmeans/web-marketing-consent
|
|
9
|
+
|
|
10
|
+
**Layer:** Web (React, shadcn + Tailwind v4)
|
|
11
|
+
**Install:** `"@owlmeans/web-marketing-consent": "^0.1.18-rc.0"` in `dependencies`
|
|
12
|
+
**Contracts:** `@owlmeans/marketing-consent` — the catalogue, `consentStatus`, the protocol tree
|
|
13
|
+
**Server half:** `@owlmeans/server-marketing-consent` — `serveMarketingConsentEntrypoints` binds the
|
|
14
|
+
same `status`/`save`/`terms` protocols this package calls
|
|
15
|
+
**Login seam:** `@owlmeans/client-auth`'s `LoginStep`/`LoginLandingHook` registries (`./login`)
|
|
16
|
+
**Cookie bridge:** `@owlmeans/consent`'s `consentStore` (device-side cookie/tracking decisions)
|
|
17
|
+
|
|
18
|
+
Read `@owlmeans/marketing-consent`'s own skill first (the catalogue, `consentStatus`'s decision
|
|
19
|
+
table, the protocol tree) and `@owlmeans/server-marketing-consent`'s (the service, `subjectOf`, the
|
|
20
|
+
append-only log) — this package builds on both and repeats nothing they already document.
|
|
21
|
+
|
|
22
|
+
## Key exports
|
|
23
|
+
|
|
24
|
+
| Export | Description |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `MARKETING_CONSENT_CLIENT_SERVICE` | The client service alias (`context.service<MarketingConsentClientService>(...)`) |
|
|
27
|
+
| `MARKETING_CONSENT_LOGIN_STEP` · `MARKETING_CONSENT_LANDING_HOOK_TERMS` · `MARKETING_CONSENT_LANDING_HOOK_SYNC` | Registry aliases the step/hooks register under |
|
|
28
|
+
| `MARKETING_CONSENT_I18N` | Re-export of `@owlmeans/marketing-consent`'s OWN `MARKETING_CONSENT_I18N` — see "One shared i18n resource" below |
|
|
29
|
+
| `makeMarketingConsentClient(protocols, opts?)` · `appendMarketingConsentClient(ctx, protocols, opts?)` | Build/register the `MarketingConsentClientService` |
|
|
30
|
+
| `MarketingConsentClientService` | `status(opts?)`, `save(request)`, `recordTerms(acceptance)`, `last()`, `bridges()`, `preferences()` — see "Fail open, always" |
|
|
31
|
+
| `cookieConsentBridge(opts?)` | A `MarketingConsentBridge` over `@owlmeans/consent`'s `consentStore` |
|
|
32
|
+
| `marketingConsentStep(client, entrypointAlias)` | The `LoginStep` — pending while `status().pending` is true |
|
|
33
|
+
| `termsRecorder(loginTermsConfig, client, locale?)` · `landingSync(client)` | The two `LoginLandingHook`s |
|
|
34
|
+
| `appendMarketingConsent(ctx, opts)` | The ONE call an app makes — wires the client, the step and both landing hooks |
|
|
35
|
+
| `marketingConsentEntrypoints(protocols)` | `[bindScreen(protocols.screen, handler(MarketingConsentScreen))]` |
|
|
36
|
+
| `MarketingConsentScreen` | The post-sign-in screen (`RoutedComponent`) |
|
|
37
|
+
| `MarketingConsentPreferences` | The reusable settings-card body (`{ translate?, className?, onSaved? }`) |
|
|
38
|
+
| `useMarketingConsent(opts?)` | The headless model both components share — `{ loading, saving, error, gpc, groups, allChecked, allIndeterminate, toggleAll, toggle, save, skip }` |
|
|
39
|
+
|
|
40
|
+
## Wiring
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { appendMarketingConsent, marketingConsentEntrypoints } from '@owlmeans/web-marketing-consent'
|
|
44
|
+
import { makeMarketingConsentProtocols } from '@owlmeans/marketing-consent'
|
|
45
|
+
|
|
46
|
+
// shared package — the SAME object the server binds `serveMarketingConsentEntrypoints` to
|
|
47
|
+
export const marketingConsentProtocols = makeMarketingConsentProtocols({ parent: appProtocols.account.base })
|
|
48
|
+
|
|
49
|
+
// web app's makeContext
|
|
50
|
+
appendMarketingConsent(context, { protocols: marketingConsentProtocols })
|
|
51
|
+
// entrypoints: ...marketingConsentEntrypoints(marketingConsentProtocols)
|
|
52
|
+
// PLUS the app's own bindAll for base/status/save/terms, alongside the server's bindAll — this
|
|
53
|
+
// package binds only the SCREEN. Exactly the split @owlmeans/web-oauth's oauthEntrypoints draws
|
|
54
|
+
// for its own consent API; see that package's skill if this split is unfamiliar.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`appendMarketingConsent(ctx, opts)` does four things, each independently toggleable:
|
|
58
|
+
|
|
59
|
+
1. Registers the `MarketingConsentClientService` (`opts.bridges ?? [cookieConsentBridge()]`,
|
|
60
|
+
`opts.preferences`) — via `appendMarketingConsentClient`, so calling `appendMarketingConsent`
|
|
61
|
+
twice never double-registers.
|
|
62
|
+
2. `opts.step !== false` — `login.registerStep(marketingConsentStep(client, opts.protocols.screen.alias))`.
|
|
63
|
+
3. `opts.terms !== false` — reads `(context.cfg as CommonConfig).security?.auth?.login?.terms`
|
|
64
|
+
(the SAME typed path `@owlmeans/client-panel`'s `useLoginMethods` reads) and registers
|
|
65
|
+
`termsRecorder`.
|
|
66
|
+
4. Always registers `landingSync`.
|
|
67
|
+
|
|
68
|
+
`opts.config` (`MarketingConsentConfig`) is accepted for symmetry with the server's
|
|
69
|
+
`appendMarketingConsentService({ config })` call but is NOT read here — `resolveMarketingConsents`
|
|
70
|
+
only ever runs where the catalogue is enumerated (the server); this package's UI reads the
|
|
71
|
+
ALREADY-RESOLVED `MarketingConsentStatusItem.definition` back off the status response and never
|
|
72
|
+
re-resolves the catalogue client-side.
|
|
73
|
+
|
|
74
|
+
## One shared i18n resource, not two
|
|
75
|
+
|
|
76
|
+
`MARKETING_CONSENT_I18N` here is a re-export of `@owlmeans/marketing-consent`'s OWN constant
|
|
77
|
+
(`'marketing-consent'`) — this package does NOT register a second resource for its `screen.*`/
|
|
78
|
+
`preferences.*` strings. `@owlmeans/i18n`'s `addI18nLib` pushes a new resource ENTRY rather than
|
|
79
|
+
replacing one, and `client-i18n`'s `useI18nResource` merges every entry for a given
|
|
80
|
+
`(lng, resource, ns)` with `i18n.addResourceBundle(lng, ns, data, true, true)` (deep merge) before
|
|
81
|
+
it is ever read. The domain package's bundle (`group.*`, `consent.*`, `link.*`, `errors.*`) and
|
|
82
|
+
this package's own (`screen.*`, `preferences.*`) have disjoint top-level keys, so the two combine
|
|
83
|
+
cleanly. `ConsentFields` (the shared group/item body) reads BOTH halves through one `t` —
|
|
84
|
+
`t(item.definition.labelKey, ...)` for a domain key, `t('screen.updated', ...)` for one of ours,
|
|
85
|
+
`t('link.default', 'Learn more')` for the domain package's own link fallback text.
|
|
86
|
+
|
|
87
|
+
## Fail open, always
|
|
88
|
+
|
|
89
|
+
`MarketingConsentClientService` NEVER calls the API while signed out
|
|
90
|
+
(`AuthService.authenticated()` — no args, returns the token or `null` — checked before every call)
|
|
91
|
+
and NEVER throws into a caller: any rejected/thrown network call is swallowed, returning `null`
|
|
92
|
+
(`status`, `save`) or `false` (`recordTerms`) instead. A broken consent read/write must never block
|
|
93
|
+
sign-in or break the app — the same discipline `@owlmeans/web-oauth`'s `landAfterLogin`/
|
|
94
|
+
`continueLogin` already apply to a step/hook that hangs or throws (bound-and-swallow, timeout
|
|
95
|
+
included).
|
|
96
|
+
|
|
97
|
+
`status(opts?)` also caches: `opts.fresh !== true` and an already-cached `last()` skips the network
|
|
98
|
+
round trip entirely (a footer "Privacy choices" link wants a quick paint, not a forced refetch);
|
|
99
|
+
`marketingConsentStep`/`landingSync`/the two components all pass `{ fresh: true }` because a
|
|
100
|
+
post-sign-in decision must never be stale.
|
|
101
|
+
|
|
102
|
+
## The login step and the two landing hooks
|
|
103
|
+
|
|
104
|
+
`marketingConsentStep(client, entrypointAlias)` is `pending: async () => (await client.status({
|
|
105
|
+
fresh: true }))?.pending ?? false` — a fetch failure fails OPEN (not pending), because a broken
|
|
106
|
+
read must never permanently trap a signed-in person on a screen they cannot get past.
|
|
107
|
+
`entrypointAlias` is `protocols.screen.alias` — `continueLogin`'s `LoginStep.entrypoint` is where
|
|
108
|
+
the dispatcher sends the browser while the step is pending.
|
|
109
|
+
|
|
110
|
+
`termsRecorder` and `landingSync` are `LoginLandingHook`s, run once per freshly landed token by
|
|
111
|
+
`landAfterLogin` (see `@owlmeans/client-auth`'s `./login` skill for the bound-and-swallow contract
|
|
112
|
+
they share with every other landing hook):
|
|
113
|
+
|
|
114
|
+
- **`termsRecorder`** (priority 100, runs first) — if `resolveTerms(loginTermsConfig)` is required
|
|
115
|
+
and `termsAccepted(resolved)` (the LOCAL `localStorage` marker `@owlmeans/client-auth`'s sign-in
|
|
116
|
+
screen already set before the flow was allowed to start), posts `client.recordTerms({ documents,
|
|
117
|
+
notices, version, locale })`. A config with terms disabled, or a person who has not yet locally
|
|
118
|
+
accepted, records nothing.
|
|
119
|
+
- **`landingSync`** (priority 90) — reconciles every `client.bridges()` against the freshly loaded
|
|
120
|
+
status: a device with cookie decisions but no saved account decisions SEEDS the account
|
|
121
|
+
(`client.save({ source: 'cookie' })`); an account with decisions but a bare device SEEDS the
|
|
122
|
+
device (`bridge.write`); both present (agreeing or not), or neither present, and NOTHING is
|
|
123
|
+
overwritten — the safer of two wrong guesses is to leave two already-made choices alone rather
|
|
124
|
+
than silently pick one.
|
|
125
|
+
|
|
126
|
+
`MarketingConsentScreen`'s own "Save and continue"/"Continue without saving" buttons call
|
|
127
|
+
`useContinueLogin()({ after: MARKETING_CONSENT_LOGIN_STEP })` directly — that is `continueLogin`,
|
|
128
|
+
NOT `landAfterLogin`, so clicking through this step never re-runs `termsRecorder`/`landingSync`
|
|
129
|
+
(those only fire once, on the token that just landed, before this step's screen was ever reached).
|
|
130
|
+
|
|
131
|
+
## The cookie-consent bridge
|
|
132
|
+
|
|
133
|
+
`cookieConsentBridge(opts?: { storageOptions?: ConsentOptions })` is a `MarketingConsentBridge`
|
|
134
|
+
over `@owlmeans/consent`'s singleton `consentStore`, for every `MarketingConsentDefinition` whose
|
|
135
|
+
`cookieCategory` names a `@owlmeans/consent` category (`trackers.analytics` -> `CONSENT_ANALYTICS`,
|
|
136
|
+
`trackers.advertising` -> `CONSENT_MARKETING`). `consentStore.save` REPLACES the stored record
|
|
137
|
+
wholesale (no merge of its own), so `write` always spreads the CURRENT record first and always
|
|
138
|
+
re-asserts the essential category before patching in only the categories this bridge owns.
|
|
139
|
+
`opts.storageOptions` is read ONLY to find which category is "essential" (a host that renamed it
|
|
140
|
+
via its own `consentStore.init(storageOptions)` elsewhere is still honored); this bridge never
|
|
141
|
+
calls `consentStore.init` itself — it has no opinion about whether a cookie dialog exists at all.
|
|
142
|
+
|
|
143
|
+
A `suppress` flag guards re-entrancy: `write` sets it before calling `consentStore.save` and clears
|
|
144
|
+
it in a `finally`, and `subscribe`'s own listener skips while it is set — otherwise a save made
|
|
145
|
+
THROUGH this bridge would immediately hand the same change back to whatever reacts to `subscribe`,
|
|
146
|
+
which could write again and loop. An update made OUTSIDE the bridge (the cookie dialog's own UI)
|
|
147
|
+
still reaches every subscriber normally.
|
|
148
|
+
|
|
149
|
+
## The screen and the settings card
|
|
150
|
+
|
|
151
|
+
`MarketingConsentScreen` — full page, no application chrome, same `100dvh` centered-card shape as
|
|
152
|
+
`@owlmeans/web-oauth`'s consent screen. **Renders when framed too** — unlike that screen (a
|
|
153
|
+
token-mint decision a clickjacking overlay could steal), saving a marketing preference is not one,
|
|
154
|
+
so there is deliberately no framed-refusal branch. Nothing checked by default is a fully valid
|
|
155
|
+
save; there is no minimum-selection validation anywhere.
|
|
156
|
+
|
|
157
|
+
`MarketingConsentPreferences({ translate?, className?, onSaved? })` renders the SAME group/item/
|
|
158
|
+
select-all body (`ConsentFields`, internal — not exported) without the full-page chrome, for a
|
|
159
|
+
host's own settings card. `MarketingConsentClientService.preferences()` names the entrypoint alias
|
|
160
|
+
a footer "Privacy choices" link should point at; that screen is expected to render THIS component,
|
|
161
|
+
not `MarketingConsentScreen`. No `useContinueLogin` here at all — nothing navigates on save, and
|
|
162
|
+
there is no skip: a settings card is always revisitable, so failing to save just leaves the error
|
|
163
|
+
showing and the draft in place.
|
|
164
|
+
|
|
165
|
+
`useMarketingConsent({ source })` is the shared headless model: `source: 'sign-in'` from the screen,
|
|
166
|
+
`'settings'` (the default) from preferences — posted on every `save()` call, on EVERY item's
|
|
167
|
+
current draft value (never a diff; the server upserts by key). Groups are ordered
|
|
168
|
+
`communications`/`data`/`trackers` (`MC_GROUP_*`), any other group appended after in first-seen
|
|
169
|
+
order. `allIndeterminate` is applied to the select-all checkbox's `.indeterminate` DOM property via
|
|
170
|
+
a `ref` in a `useEffect` — a native checkbox has no ARIA `mixed` prop, only that imperative one.
|
|
171
|
+
|
|
172
|
+
## `data-marketing-consent-*` contract
|
|
173
|
+
|
|
174
|
+
`data-marketing-consent` (screen root), `data-marketing-consent-all`,
|
|
175
|
+
`data-marketing-consent-item="<key>"`, `data-marketing-consent-updated="<key>"`,
|
|
176
|
+
`data-marketing-consent-save`, `data-marketing-consent-skip` (screen only — preferences has none),
|
|
177
|
+
`data-marketing-consent-error` (screen; preferences uses a bare `role="alert"`, no fixed id),
|
|
178
|
+
`data-marketing-consent-preferences-save`. `-all`/`-item`/`-updated` are the SAME attributes in
|
|
179
|
+
both the screen and `MarketingConsentPreferences` — the two are never mounted on one page at once.
|
|
180
|
+
Do not rename them.
|
|
181
|
+
|
|
182
|
+
## The `@source` line — a consumer rule
|
|
183
|
+
|
|
184
|
+
Same trap as `@owlmeans/web-oauth`/`@owlmeans/web-consent`: Tailwind's scanner reads the CSS root
|
|
185
|
+
plus `@source` directives and excludes `node_modules`, so a class used only inside this package
|
|
186
|
+
never reaches the app's stylesheet and the screen/card render unstyled with nothing in the
|
|
187
|
+
consumer's own sources to blame. Every consuming app adds:
|
|
188
|
+
|
|
189
|
+
```css
|
|
190
|
+
@source "../../../node_modules/@owlmeans/web-marketing-consent/src";
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Point at the shipped `src`, **never** `build` — under a linked workspace the `node_modules` entry
|
|
194
|
+
is a symlink into this monorepo, where every package's `build` directory is ignored by `.gitignore`,
|
|
195
|
+
so a `build` source scans zero files and reports nothing; the failure is silent. Primitives
|
|
196
|
+
(`button`, `card`, `label`) are private under `src/@/` and imported only by relative specifiers —
|
|
197
|
+
checkbox rows are a bare native `<input type="checkbox">`, the same choice `@owlmeans/client-auth`'s
|
|
198
|
+
own `LoginTerms` makes for a sign-in screen (no checkbox primitive, and its Radix peer, is vendored
|
|
199
|
+
just to render one). `tests/package-boundary.spec.ts` fails on any `from '@/…'` in `src/`.
|
|
200
|
+
|
|
201
|
+
## Testing
|
|
202
|
+
|
|
203
|
+
Category D (`/testing-ui`) — `bun test ./tests` drives a real chromium against a real app, with
|
|
204
|
+
only the server absent, the same harness shape as `@owlmeans/web-oauth`'s:
|
|
205
|
+
|
|
206
|
+
- **Harness** (`tests/context.ts` boots Vite; `tests/harness/mount.tsx` is the app) — the real
|
|
207
|
+
`@owlmeans/web-client` context, `appendMarketingConsent(context, { protocols })` (so the step and
|
|
208
|
+
both landing hooks are really registered, even though no spec drives them directly), the
|
|
209
|
+
marketing-consent API bound with `bindAll` under a backend parent on the harness's own origin,
|
|
210
|
+
the screen's own entrypoint, a bare `home` screen, and a `/prefs` screen that renders
|
|
211
|
+
`MarketingConsentPreferences` for the preferences specs. `tests/helpers.ts`'s `open(path, {
|
|
212
|
+
stubs, signedIn, lng })` stubs `status`/`save`/`terms` via `page.route` on
|
|
213
|
+
`/\/api\/.*marketing-consent\//`; `statusView(overrides?)` builds a `MarketingConsentStatusView`
|
|
214
|
+
from `STANDARD_MARKETING_CONSENTS` (every standard key is `opt-in`, so the un-overridden default
|
|
215
|
+
IS "nothing checked").
|
|
216
|
+
- A signed-in visit is `?bearer=<makeBearer(USER)>`, written to IndexedDB (`idb-keyval`) BEFORE
|
|
217
|
+
`createRoot(...).render(...)` — **wait for a rendered marker** (`#home`, `[data-marketing-consent-save]`)
|
|
218
|
+
before `page.evaluate`-ing anything: `domcontentloaded` fires as soon as the module script
|
|
219
|
+
STARTS running, not once its own top-level `await`s (writing that bearer record) have settled: a
|
|
220
|
+
bare `page.evaluate` right after `goto` races the write and reads "signed out".
|
|
221
|
+
- **What is covered:** `screen.spec.ts` (nothing checked by default, select-all/indeterminate, the
|
|
222
|
+
`updated` badge, save posting every current draft value then continuing the flow to `/`, a failed
|
|
223
|
+
save showing error+skip with skip still moving the flow on, a framed mount rendering normally);
|
|
224
|
+
`preferences.spec.ts` (same body, no navigation on save, `onSaved` firing, no skip link exists);
|
|
225
|
+
`bridge.spec.ts` (non-browser — a fake `localStorage`/`document`/`window`, the same fixture
|
|
226
|
+
`@owlmeans/consent`'s own suite uses; read/write/the re-entrancy guard); `i18n.spec.ts`
|
|
227
|
+
(non-browser — 8-locale parity plus a literal-key scan of `t('screen.…'/'preferences.…')` calls
|
|
228
|
+
across the three component files); `service.spec.ts` (browser — drives the REAL registered
|
|
229
|
+
`MarketingConsentClientService` via `window.__mc`, since faking `assertCtx`/`context.entrypoint`
|
|
230
|
+
by hand is not worth it: signed-out never calls the API, a healthy server round-trips, a failing
|
|
231
|
+
server fails open on every method); `package-boundary.spec.ts` (no consumer `@/` aliases in
|
|
232
|
+
`src/`).
|
|
233
|
+
|
|
234
|
+
## Related
|
|
235
|
+
|
|
236
|
+
- `@owlmeans/marketing-consent` — the catalogue, `consentStatus`'s decision table, the protocol
|
|
237
|
+
tree (read its skill first; this package repeats nothing it already documents)
|
|
238
|
+
- `@owlmeans/server-marketing-consent` — the service and handlers this package's `status`/`save`/
|
|
239
|
+
`terms` calls reach
|
|
240
|
+
- `@owlmeans/client-auth` (`./login`) — `LoginStep`/`LoginLandingHook`, `landAfterLogin`/
|
|
241
|
+
`continueLogin`/`useContinueLogin`, `resolveTerms`/`termsAccepted` — the seam this package plugs
|
|
242
|
+
into; read its skill for the bound-and-swallow contract every step/hook shares
|
|
243
|
+
- `@owlmeans/consent` — `consentStore`, `ConsentRecord`, the categories this bridge patches
|
|
244
|
+
- `@owlmeans/web-oauth` — the structural precedent this package copies: headless-hook/thin-component
|
|
245
|
+
split, vendored `src/@/` primitives, the `@source` line, the Vite+Playwright test harness shape
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type VariantProps } from 'class-variance-authority';
|
|
3
|
+
declare const buttonVariants: (props?: ({
|
|
4
|
+
variant?: "default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | null | undefined;
|
|
5
|
+
size?: "default" | "icon" | "lg" | "sm" | null | undefined;
|
|
6
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
|
+
declare function Button({ className, variant, size, asChild, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
|
|
8
|
+
asChild?: boolean;
|
|
9
|
+
}): React.JSX.Element;
|
|
10
|
+
export { Button, buttonVariants };
|
|
11
|
+
//# sourceMappingURL=button.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../../../src/@/components/ui/button.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIjE,QAAA,MAAM,cAAc;;;8EA6BnB,CAAA;AAED,iBAAS,MAAM,CAAC,EACd,SAAS,EACT,OAAO,EACP,IAAI,EACJ,OAAe,EACf,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,GAC/B,YAAY,CAAC,OAAO,cAAc,CAAC,GAAG;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,qBAUF;AAED,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
3
|
+
import { cva } from 'class-variance-authority';
|
|
4
|
+
import { cn } from '../../lib/utils.js';
|
|
5
|
+
const buttonVariants = cva("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer", {
|
|
6
|
+
variants: {
|
|
7
|
+
variant: {
|
|
8
|
+
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
|
9
|
+
destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
10
|
+
outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
11
|
+
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
|
12
|
+
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
13
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
14
|
+
},
|
|
15
|
+
size: {
|
|
16
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
17
|
+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
18
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
19
|
+
icon: "size-9",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: "default",
|
|
24
|
+
size: "default",
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
function Button({ className, variant, size, asChild = false, ...props }) {
|
|
28
|
+
const Comp = asChild ? Slot : "button";
|
|
29
|
+
return (_jsx(Comp, { "data-slot": "button", className: cn(buttonVariants({ variant, size, className })), ...props }));
|
|
30
|
+
}
|
|
31
|
+
export { Button, buttonVariants };
|
|
32
|
+
//# sourceMappingURL=button.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"button.js","sourceRoot":"","sources":["../../../../src/@/components/ui/button.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC3C,OAAO,EAAE,GAAG,EAAqB,MAAM,0BAA0B,CAAA;AAEjE,OAAO,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAA;AAEvC,MAAM,cAAc,GAAG,GAAG,CACxB,4cAA4c,EAC5c;IACE,QAAQ,EAAE;QACR,OAAO,EAAE;YACP,OAAO,EACL,kEAAkE;YACpE,WAAW,EACT,6JAA6J;YAC/J,OAAO,EACL,uIAAuI;YACzI,SAAS,EACP,wEAAwE;YAC1E,KAAK,EACH,sEAAsE;YACxE,IAAI,EAAE,iDAAiD;SACxD;QACD,IAAI,EAAE;YACJ,OAAO,EAAE,+BAA+B;YACxC,EAAE,EAAE,+CAA+C;YACnD,EAAE,EAAE,sCAAsC;YAC1C,IAAI,EAAE,QAAQ;SACf;KACF;IACD,eAAe,EAAE;QACf,OAAO,EAAE,SAAS;QAClB,IAAI,EAAE,SAAS;KAChB;CACF,CACF,CAAA;AAED,SAAS,MAAM,CAAC,EACd,SAAS,EACT,OAAO,EACP,IAAI,EACJ,OAAO,GAAG,KAAK,EACf,GAAG,KAAK,EAIP;IACD,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAA;IAEtC,OAAO,CACL,KAAC,IAAI,iBACO,QAAQ,EAClB,SAAS,EAAE,EAAE,CAAC,cAAc,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,KACvD,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
declare function Card({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
3
|
+
declare function CardHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
4
|
+
declare function CardTitle({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
5
|
+
declare function CardDescription({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
6
|
+
declare function CardAction({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
7
|
+
declare function CardContent({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
8
|
+
declare function CardFooter({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
9
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent, };
|
|
10
|
+
//# sourceMappingURL=card.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"card.d.ts","sourceRoot":"","sources":["../../../../src/@/components/ui/card.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,iBAAS,IAAI,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAWjE;AAED,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAWvE;AAED,iBAAS,SAAS,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQtE;AAED,iBAAS,eAAe,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQ5E;AAED,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAWvE;AAED,iBAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQxE;AAED,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQvE;AAED,OAAO,EACL,IAAI,EACJ,UAAU,EACV,UAAU,EACV,SAAS,EACT,UAAU,EACV,eAAe,EACf,WAAW,GACZ,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from '../../lib/utils.js';
|
|
3
|
+
function Card({ className, ...props }) {
|
|
4
|
+
return (_jsx("div", { "data-slot": "card", className: cn("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", className), ...props }));
|
|
5
|
+
}
|
|
6
|
+
function CardHeader({ className, ...props }) {
|
|
7
|
+
return (_jsx("div", { "data-slot": "card-header", className: cn("@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", className), ...props }));
|
|
8
|
+
}
|
|
9
|
+
function CardTitle({ className, ...props }) {
|
|
10
|
+
return (_jsx("div", { "data-slot": "card-title", className: cn("leading-none font-semibold", className), ...props }));
|
|
11
|
+
}
|
|
12
|
+
function CardDescription({ className, ...props }) {
|
|
13
|
+
return (_jsx("div", { "data-slot": "card-description", className: cn("text-muted-foreground text-sm", className), ...props }));
|
|
14
|
+
}
|
|
15
|
+
function CardAction({ className, ...props }) {
|
|
16
|
+
return (_jsx("div", { "data-slot": "card-action", className: cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className), ...props }));
|
|
17
|
+
}
|
|
18
|
+
function CardContent({ className, ...props }) {
|
|
19
|
+
return (_jsx("div", { "data-slot": "card-content", className: cn("px-6", className), ...props }));
|
|
20
|
+
}
|
|
21
|
+
function CardFooter({ className, ...props }) {
|
|
22
|
+
return (_jsx("div", { "data-slot": "card-footer", className: cn("flex items-center px-6 [.border-t]:pt-6", className), ...props }));
|
|
23
|
+
}
|
|
24
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent, };
|
|
25
|
+
//# sourceMappingURL=card.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"card.js","sourceRoot":"","sources":["../../../../src/@/components/ui/card.tsx"],"names":[],"mappings":";AAGA,OAAO,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAA;AAEvC,SAAS,IAAI,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IAChE,OAAO,CACL,2BACY,MAAM,EAChB,SAAS,EAAE,EAAE,CACX,mFAAmF,EACnF,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IACtE,OAAO,CACL,2BACY,aAAa,EACvB,SAAS,EAAE,EAAE,CACX,4JAA4J,EAC5J,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,SAAS,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IACrE,OAAO,CACL,2BACY,YAAY,EACtB,SAAS,EAAE,EAAE,CAAC,4BAA4B,EAAE,SAAS,CAAC,KAClD,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,eAAe,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IAC3E,OAAO,CACL,2BACY,kBAAkB,EAC5B,SAAS,EAAE,EAAE,CAAC,+BAA+B,EAAE,SAAS,CAAC,KACrD,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IACtE,OAAO,CACL,2BACY,aAAa,EACvB,SAAS,EAAE,EAAE,CACX,gEAAgE,EAChE,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IACvE,OAAO,CACL,2BACY,cAAc,EACxB,SAAS,EAAE,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,KAC5B,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IACtE,OAAO,CACL,2BACY,aAAa,EACvB,SAAS,EAAE,EAAE,CAAC,yCAAyC,EAAE,SAAS,CAAC,KAC/D,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,OAAO,EACL,IAAI,EACJ,UAAU,EACV,UAAU,EACV,SAAS,EACT,UAAU,EACV,eAAe,EACf,WAAW,GACZ,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"label.d.ts","sourceRoot":"","sources":["../../../../src/@/components/ui/label.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,cAAc,MAAM,uBAAuB,CAAA;AAIvD,iBAAS,KAAK,CAAC,EACb,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,cAAc,CAAC,IAAI,CAAC,qBAWlD;AAED,OAAO,EAAE,KAAK,EAAE,CAAA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as LabelPrimitive from '@radix-ui/react-label';
|
|
3
|
+
import { cn } from '../../lib/utils.js';
|
|
4
|
+
function Label({ className, ...props }) {
|
|
5
|
+
return (_jsx(LabelPrimitive.Root, { "data-slot": "label", className: cn("flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", className), ...props }));
|
|
6
|
+
}
|
|
7
|
+
export { Label };
|
|
8
|
+
//# sourceMappingURL=label.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"label.js","sourceRoot":"","sources":["../../../../src/@/components/ui/label.tsx"],"names":[],"mappings":";AAEA,OAAO,KAAK,cAAc,MAAM,uBAAuB,CAAA;AAEvD,OAAO,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAA;AAEvC,SAAS,KAAK,CAAC,EACb,SAAS,EACT,GAAG,KAAK,EACyC;IACjD,OAAO,CACL,KAAC,cAAc,CAAC,IAAI,iBACR,OAAO,EACjB,SAAS,EAAE,EAAE,CACX,qNAAqN,EACrN,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,OAAO,EAAE,KAAK,EAAE,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/@/lib/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAA;AAG5C,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,UAEzC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../src/@/lib/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,MAAM,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAExC,MAAM,UAAU,EAAE,CAAC,GAAG,MAAoB;IACxC,OAAO,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;AAC9B,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ClientConfig, ClientContext } from '@owlmeans/client-context';
|
|
2
|
+
import type { MarketingConsentBridge, MarketingConsentConfig, MarketingConsentEntrypoints } from '@owlmeans/marketing-consent';
|
|
3
|
+
export interface MarketingConsentAppendOptions {
|
|
4
|
+
/** From `makeMarketingConsentProtocols(...)` — the app builds this once and shares it between
|
|
5
|
+
* this call and its own server-side `serveMarketingConsentEntrypoints`/`bindAll` wiring. */
|
|
6
|
+
protocols: MarketingConsentEntrypoints;
|
|
7
|
+
/**
|
|
8
|
+
* Accepted for API symmetry with the server side (`appendMarketingConsentService({ config })`),
|
|
9
|
+
* but not read here: `resolveMarketingConsents` only ever runs where the catalogue is actually
|
|
10
|
+
* enumerated — the server. This package's own UI reads the ALREADY-RESOLVED definitions back off
|
|
11
|
+
* `MarketingConsentStatusItem.definition` and never re-resolves the catalogue client-side.
|
|
12
|
+
*/
|
|
13
|
+
config?: MarketingConsentConfig;
|
|
14
|
+
/** Register the post-sign-in step. Default `true`. */
|
|
15
|
+
step?: boolean;
|
|
16
|
+
/** Record a fresh terms acceptance on landing. Default `true`. */
|
|
17
|
+
terms?: boolean;
|
|
18
|
+
/** Default `[cookieConsentBridge()]`. */
|
|
19
|
+
bridges?: MarketingConsentBridge[];
|
|
20
|
+
/** Entrypoint alias of a host's own "Privacy choices" settings screen, for
|
|
21
|
+
* `MarketingConsentClientService.preferences()`. */
|
|
22
|
+
preferences?: string;
|
|
23
|
+
locale?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Wire the marketing-consent client into a web app: the client service, the post-sign-in step
|
|
27
|
+
* (pending while anything is unanswered) and the two landing hooks (terms acceptance, then
|
|
28
|
+
* cookie/account reconciliation) — the whole of what an application calls beyond registering
|
|
29
|
+
* `marketingConsentEntrypoints(protocols)` in its own entrypoint list.
|
|
30
|
+
*/
|
|
31
|
+
export declare const appendMarketingConsent: <C extends ClientConfig, T extends ClientContext<C>>(context: T, opts: MarketingConsentAppendOptions) => T;
|
|
32
|
+
//# sourceMappingURL=append.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"append.d.ts","sourceRoot":"","sources":["../src/append.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAE3E,OAAO,KAAK,EACV,sBAAsB,EAAE,sBAAsB,EAAE,2BAA2B,EAC5E,MAAM,6BAA6B,CAAA;AAQpC,MAAM,WAAW,6BAA6B;IAC5C;gGAC4F;IAC5F,SAAS,EAAE,2BAA2B,CAAA;IACtC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,sBAAsB,CAAA;IAC/B,sDAAsD;IACtD,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,kEAAkE;IAClE,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,yCAAyC;IACzC,OAAO,CAAC,EAAE,sBAAsB,EAAE,CAAA;IAClC;wDACoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GAAI,CAAC,SAAS,YAAY,EAAE,CAAC,SAAS,aAAa,CAAC,CAAC,CAAC,WAC9E,CAAC,QAAQ,6BAA6B,KAC9C,CAiBF,CAAA"}
|
package/build/append.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ensureLoginService } from '@owlmeans/client-auth/login';
|
|
2
|
+
import { cookieConsentBridge } from './bridge.js';
|
|
3
|
+
import { MARKETING_CONSENT_CLIENT_SERVICE } from './consts.js';
|
|
4
|
+
import { landingSync, termsRecorder } from './landing.js';
|
|
5
|
+
import { appendMarketingConsentClient } from './service.js';
|
|
6
|
+
import { marketingConsentStep } from './step.js';
|
|
7
|
+
/**
|
|
8
|
+
* Wire the marketing-consent client into a web app: the client service, the post-sign-in step
|
|
9
|
+
* (pending while anything is unanswered) and the two landing hooks (terms acceptance, then
|
|
10
|
+
* cookie/account reconciliation) — the whole of what an application calls beyond registering
|
|
11
|
+
* `marketingConsentEntrypoints(protocols)` in its own entrypoint list.
|
|
12
|
+
*/
|
|
13
|
+
export const appendMarketingConsent = (context, opts) => {
|
|
14
|
+
const bridges = opts.bridges ?? [cookieConsentBridge()];
|
|
15
|
+
appendMarketingConsentClient(context, opts.protocols, { bridges, preferences: opts.preferences });
|
|
16
|
+
const client = context.service(MARKETING_CONSENT_CLIENT_SERVICE);
|
|
17
|
+
const login = ensureLoginService(context);
|
|
18
|
+
if (opts.step !== false) {
|
|
19
|
+
login.registerStep(marketingConsentStep(client, opts.protocols.screen.alias));
|
|
20
|
+
}
|
|
21
|
+
if (opts.terms !== false) {
|
|
22
|
+
const terms = context.cfg.security?.auth?.login?.terms;
|
|
23
|
+
login.onLanded(termsRecorder(terms, client, opts.locale));
|
|
24
|
+
}
|
|
25
|
+
login.onLanded(landingSync(client));
|
|
26
|
+
return context;
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=append.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"append.js","sourceRoot":"","sources":["../src/append.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAA;AAIhE,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,EAAE,gCAAgC,EAAE,MAAM,aAAa,CAAA;AAC9D,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,EAAE,4BAA4B,EAAE,MAAM,cAAc,CAAA;AAE3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAyBhD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CACpC,OAAU,EAAE,IAAmC,EAC5C,EAAE;IACL,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,mBAAmB,EAAE,CAAC,CAAA;IAEvD,4BAA4B,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAA;IACjG,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAgC,gCAAgC,CAAC,CAAA;IAE/F,MAAM,KAAK,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAA;IACzC,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QACxB,KAAK,CAAC,YAAY,CAAC,oBAAoB,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;IAC/E,CAAC;IACD,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAkC,OAAO,CAAC,GAAoB,CAAC,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,CAAA;QACtG,KAAK,CAAC,QAAQ,CAAC,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;IAC3D,CAAC;IACD,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAA;IAEnC,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ConsentOptions } from '@owlmeans/consent';
|
|
2
|
+
import type { MarketingConsentBridge } from '@owlmeans/marketing-consent';
|
|
3
|
+
export interface MakeCookieConsentBridgeOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The SAME options the app passes to `consentStore.init(...)` elsewhere (e.g. via
|
|
6
|
+
* `@owlmeans/web-consent`'s dialog mount) — read here only to find which category key is the
|
|
7
|
+
* required "essential" one, so `write` re-asserts the right key even when a host renamed it.
|
|
8
|
+
* This bridge never calls `consentStore.init` itself; a marketing-consent screen has no opinion
|
|
9
|
+
* about whether a cookie dialog exists at all.
|
|
10
|
+
*/
|
|
11
|
+
storageOptions?: ConsentOptions;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The seam between a person's saved marketing-consent DECISIONS (server-held, per account) and
|
|
15
|
+
* this browser's cookie-consent RECORD (`@owlmeans/consent`'s `consentStore`, per device) — for
|
|
16
|
+
* every definition whose `cookieCategory` names a `@owlmeans/consent` category
|
|
17
|
+
* (`trackers.analytics` -> `CONSENT_ANALYTICS`, `trackers.advertising` -> `CONSENT_MARKETING`).
|
|
18
|
+
*
|
|
19
|
+
* `consentStore.save` REPLACES the stored record wholesale — `store.ts`'s `save` calls
|
|
20
|
+
* `writeConsent(record, options)` with exactly what it is given, no merge of its own — so `write`
|
|
21
|
+
* always spreads the CURRENT record first and always re-asserts the essential category, before
|
|
22
|
+
* patching in only the categories THIS bridge owns.
|
|
23
|
+
*/
|
|
24
|
+
export declare const cookieConsentBridge: (opts?: MakeCookieConsentBridgeOptions) => MarketingConsentBridge;
|
|
25
|
+
//# sourceMappingURL=bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAiB,MAAM,mBAAmB,CAAA;AACtE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AAEzE,MAAM,WAAW,8BAA8B;IAC7C;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,cAAc,CAAA;CAChC;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,UAAW,8BAA8B,KAAG,sBA8D3E,CAAA"}
|
package/build/bridge.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { CONSENT_ESSENTIAL, DEFAULT_CONSENT_CATEGORIES, consentStore } from '@owlmeans/consent';
|
|
2
|
+
/**
|
|
3
|
+
* The seam between a person's saved marketing-consent DECISIONS (server-held, per account) and
|
|
4
|
+
* this browser's cookie-consent RECORD (`@owlmeans/consent`'s `consentStore`, per device) — for
|
|
5
|
+
* every definition whose `cookieCategory` names a `@owlmeans/consent` category
|
|
6
|
+
* (`trackers.analytics` -> `CONSENT_ANALYTICS`, `trackers.advertising` -> `CONSENT_MARKETING`).
|
|
7
|
+
*
|
|
8
|
+
* `consentStore.save` REPLACES the stored record wholesale — `store.ts`'s `save` calls
|
|
9
|
+
* `writeConsent(record, options)` with exactly what it is given, no merge of its own — so `write`
|
|
10
|
+
* always spreads the CURRENT record first and always re-asserts the essential category, before
|
|
11
|
+
* patching in only the categories THIS bridge owns.
|
|
12
|
+
*/
|
|
13
|
+
export const cookieConsentBridge = (opts) => {
|
|
14
|
+
const categories = opts?.storageOptions?.categories ?? DEFAULT_CONSENT_CATEGORIES;
|
|
15
|
+
const essentialKey = categories.find(category => category.required === true)?.key ?? CONSENT_ESSENTIAL;
|
|
16
|
+
// Re-entrancy guard: `write` calling `consentStore.save` publishes to every subscriber of the
|
|
17
|
+
// store, including the one `subscribe` below registers — without this, saving a marketing
|
|
18
|
+
// decision through this bridge would immediately hand the SAME change back to a caller that
|
|
19
|
+
// reacts to it by writing again (a naive two-way binding), looping the two writes forever.
|
|
20
|
+
let suppress = false;
|
|
21
|
+
return {
|
|
22
|
+
alias: 'cookie-consent',
|
|
23
|
+
read: defs => {
|
|
24
|
+
const record = consentStore.get().record;
|
|
25
|
+
if (record == null) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const out = {};
|
|
29
|
+
for (const definition of defs) {
|
|
30
|
+
if (definition.cookieCategory != null) {
|
|
31
|
+
out[definition.key] = record[definition.cookieCategory] === true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
},
|
|
36
|
+
write: (decisions, defs) => {
|
|
37
|
+
const current = consentStore.get().record ?? {};
|
|
38
|
+
const patch = {};
|
|
39
|
+
for (const definition of defs) {
|
|
40
|
+
if (definition.cookieCategory != null && decisions[definition.key] != null) {
|
|
41
|
+
patch[definition.cookieCategory] = decisions[definition.key];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (Object.keys(patch).length === 0) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
suppress = true;
|
|
48
|
+
try {
|
|
49
|
+
consentStore.save({ ...current, [essentialKey]: true, ...patch });
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
suppress = false;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
subscribe: (listener, defs) => consentStore.subscribe(state => {
|
|
56
|
+
if (suppress || state.record == null) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const out = {};
|
|
60
|
+
for (const definition of defs) {
|
|
61
|
+
if (definition.cookieCategory != null) {
|
|
62
|
+
out[definition.key] = state.record[definition.cookieCategory] === true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
listener(out);
|
|
66
|
+
}),
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,0BAA0B,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAe/F;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,IAAqC,EAA0B,EAAE;IACnG,MAAM,UAAU,GAAG,IAAI,EAAE,cAAc,EAAE,UAAU,IAAI,0BAA0B,CAAA;IACjF,MAAM,YAAY,GAAG,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,KAAK,IAAI,CAAC,EAAE,GAAG,IAAI,iBAAiB,CAAA;IAEtG,8FAA8F;IAC9F,0FAA0F;IAC1F,4FAA4F;IAC5F,2FAA2F;IAC3F,IAAI,QAAQ,GAAG,KAAK,CAAA;IAEpB,OAAO;QACL,KAAK,EAAE,gBAAgB;QAEvB,IAAI,EAAE,IAAI,CAAC,EAAE;YACX,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,EAAE,CAAC,MAAM,CAAA;YACxC,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;gBACnB,OAAO,IAAI,CAAA;YACb,CAAC;YAED,MAAM,GAAG,GAA4B,EAAE,CAAA;YACvC,KAAK,MAAM,UAAU,IAAI,IAAI,EAAE,CAAC;gBAC9B,IAAI,UAAU,CAAC,cAAc,IAAI,IAAI,EAAE,CAAC;oBACtC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,cAAc,CAAC,KAAK,IAAI,CAAA;gBAClE,CAAC;YACH,CAAC;YAED,OAAO,GAAG,CAAA;QACZ,CAAC;QAED,KAAK,EAAE,CAAC,SAAS,EAAE,IAAI,EAAE,EAAE;YACzB,MAAM,OAAO,GAAkB,YAAY,CAAC,GAAG,EAAE,CAAC,MAAM,IAAI,EAAE,CAAA;YAC9D,MAAM,KAAK,GAA2B,EAAE,CAAA;YACxC,KAAK,MAAM,UAAU,IAAI,IAAI,EAAE,CAAC;gBAC9B,IAAI,UAAU,CAAC,cAAc,IAAI,IAAI,IAAI,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;oBAC3E,KAAK,CAAC,UAAU,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC,CAAA;gBAC9D,CAAC;YACH,CAAC;YACD,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACpC,OAAM;YACR,CAAC;YAED,QAAQ,GAAG,IAAI,CAAA;YACf,IAAI,CAAC;gBACH,YAAY,CAAC,IAAI,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,GAAG,KAAK,EAAE,CAAC,CAAA;YACnE,CAAC;oBAAS,CAAC;gBACT,QAAQ,GAAG,KAAK,CAAA;YAClB,CAAC;QACH,CAAC;QAED,SAAS,EAAE,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE;YAC5D,IAAI,QAAQ,IAAI,KAAK,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC;gBACrC,OAAM;YACR,CAAC;YACD,MAAM,GAAG,GAA4B,EAAE,CAAA;YACvC,KAAK,MAAM,UAAU,IAAI,IAAI,EAAE,CAAC;gBAC9B,IAAI,UAAU,CAAC,cAAc,IAAI,IAAI,EAAE,CAAC;oBACtC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,cAAc,CAAC,KAAK,IAAI,CAAA;gBACxE,CAAC;YACH,CAAC;YACD,QAAQ,CAAC,GAAG,CAAC,CAAA;QACf,CAAC,CAAC;KACH,CAAA;AACH,CAAC,CAAA"}
|