@aglyn/plugins-marketing 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +56 -0
- package/src/index.d.ts +19 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/announcement-bar-card.component.d.ts +20 -0
- package/src/lib/components/announcement-bar-card.component.js +292 -0
- package/src/lib/components/announcement-bar-card.component.js.map +1 -0
- package/src/lib/components/campaign-composer.d.ts +84 -0
- package/src/lib/components/campaign-composer.js +1643 -0
- package/src/lib/components/campaign-composer.js.map +1 -0
- package/src/lib/components/campaign-conversions-card.d.ts +22 -0
- package/src/lib/components/campaign-conversions-card.js +642 -0
- package/src/lib/components/campaign-conversions-card.js.map +1 -0
- package/src/lib/components/campaign-detail-card.d.ts +55 -0
- package/src/lib/components/campaign-detail-card.js +771 -0
- package/src/lib/components/campaign-detail-card.js.map +1 -0
- package/src/lib/components/campaign-edit-drawer.d.ts +60 -0
- package/src/lib/components/campaign-edit-drawer.js +252 -0
- package/src/lib/components/campaign-edit-drawer.js.map +1 -0
- package/src/lib/components/campaign-email-zones.d.ts +111 -0
- package/src/lib/components/campaign-email-zones.js +54 -0
- package/src/lib/components/campaign-email-zones.js.map +1 -0
- package/src/lib/components/campaign-glance-card.component.d.ts +18 -0
- package/src/lib/components/campaign-glance-card.component.js +151 -0
- package/src/lib/components/campaign-glance-card.component.js.map +1 -0
- package/src/lib/components/campaign-members-section.d.ts +87 -0
- package/src/lib/components/campaign-members-section.js +499 -0
- package/src/lib/components/campaign-members-section.js.map +1 -0
- package/src/lib/components/campaign-reach-sections.d.ts +137 -0
- package/src/lib/components/campaign-reach-sections.js +694 -0
- package/src/lib/components/campaign-reach-sections.js.map +1 -0
- package/src/lib/components/campaign-report-card.d.ts +91 -0
- package/src/lib/components/campaign-report-card.js +667 -0
- package/src/lib/components/campaign-report-card.js.map +1 -0
- package/src/lib/components/campaign-test-send-drawer.d.ts +45 -0
- package/src/lib/components/campaign-test-send-drawer.js +238 -0
- package/src/lib/components/campaign-test-send-drawer.js.map +1 -0
- package/src/lib/components/campaigns-card.d.ts +31 -0
- package/src/lib/components/campaigns-card.js +667 -0
- package/src/lib/components/campaigns-card.js.map +1 -0
- package/src/lib/components/conversion-attribution.component.d.ts +62 -0
- package/src/lib/components/conversion-attribution.component.js +127 -0
- package/src/lib/components/conversion-attribution.component.js.map +1 -0
- package/src/lib/components/email-compose-card.d.ts +12 -0
- package/src/lib/components/email-compose-card.js +157 -0
- package/src/lib/components/email-compose-card.js.map +1 -0
- package/src/lib/components/email-detail.d.ts +40 -0
- package/src/lib/components/email-detail.js +1180 -0
- package/src/lib/components/email-detail.js.map +1 -0
- package/src/lib/components/email-edit-drawer.d.ts +54 -0
- package/src/lib/components/email-edit-drawer.js +172 -0
- package/src/lib/components/email-edit-drawer.js.map +1 -0
- package/src/lib/components/email-messages-widget.d.ts +22 -0
- package/src/lib/components/email-messages-widget.js +49 -0
- package/src/lib/components/email-messages-widget.js.map +1 -0
- package/src/lib/components/email-recipients-card.d.ts +50 -0
- package/src/lib/components/email-recipients-card.js +338 -0
- package/src/lib/components/email-recipients-card.js.map +1 -0
- package/src/lib/components/emails-list-card.d.ts +36 -0
- package/src/lib/components/emails-list-card.js +544 -0
- package/src/lib/components/emails-list-card.js.map +1 -0
- package/src/lib/components/experiment-zones.d.ts +78 -0
- package/src/lib/components/experiment-zones.js +65 -0
- package/src/lib/components/experiment-zones.js.map +1 -0
- package/src/lib/components/host-experiments-card.component.d.ts +17 -0
- package/src/lib/components/host-experiments-card.component.js +887 -0
- package/src/lib/components/host-experiments-card.component.js.map +1 -0
- package/src/lib/components/host-marketing-summary-card.component.d.ts +15 -0
- package/src/lib/components/host-marketing-summary-card.component.js +161 -0
- package/src/lib/components/host-marketing-summary-card.component.js.map +1 -0
- package/src/lib/components/host-overlays-card.component.d.ts +19 -0
- package/src/lib/components/host-overlays-card.component.js +783 -0
- package/src/lib/components/host-overlays-card.component.js.map +1 -0
- package/src/lib/components/marketing-console-page.d.ts +28 -0
- package/src/lib/components/marketing-console-page.js +166 -0
- package/src/lib/components/marketing-console-page.js.map +1 -0
- package/src/lib/components/marketing-console-sections.d.ts +34 -0
- package/src/lib/components/marketing-console-sections.js +77 -0
- package/src/lib/components/marketing-console-sections.js.map +1 -0
- package/src/lib/components/overlay-stats-row.component.d.ts +15 -0
- package/src/lib/components/overlay-stats-row.component.js +78 -0
- package/src/lib/components/overlay-stats-row.component.js.map +1 -0
- package/src/lib/components/popup-card.component.d.ts +18 -0
- package/src/lib/components/popup-card.component.js +462 -0
- package/src/lib/components/popup-card.component.js.map +1 -0
- package/src/lib/components/record-attribution-widget.d.ts +35 -0
- package/src/lib/components/record-attribution-widget.js +33 -0
- package/src/lib/components/record-attribution-widget.js.map +1 -0
- package/src/lib/components/site-runtime.d.ts +9 -0
- package/src/lib/components/site-runtime.js +1081 -0
- package/src/lib/components/site-runtime.js.map +1 -0
- package/src/lib/components/use-campaign-send-api.d.ts +35 -0
- package/src/lib/components/use-campaign-send-api.js +99 -0
- package/src/lib/components/use-campaign-send-api.js.map +1 -0
- package/src/lib/components/use-campaign-topic-options.d.ts +20 -0
- package/src/lib/components/use-campaign-topic-options.js +44 -0
- package/src/lib/components/use-campaign-topic-options.js.map +1 -0
- package/src/lib/components/use-emails-hub-path.d.ts +39 -0
- package/src/lib/components/use-emails-hub-path.js +70 -0
- package/src/lib/components/use-emails-hub-path.js.map +1 -0
- package/src/lib/components/use-marketing-hub-path.d.ts +21 -0
- package/src/lib/components/use-marketing-hub-path.js +50 -0
- package/src/lib/components/use-marketing-hub-path.js.map +1 -0
- package/src/lib/components/use-overlay-copy-editor.d.ts +79 -0
- package/src/lib/components/use-overlay-copy-editor.js +110 -0
- package/src/lib/components/use-overlay-copy-editor.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +9 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/model/campaign-membership-figures.d.ts +109 -0
- package/src/lib/model/campaign-membership-figures.js +113 -0
- package/src/lib/model/campaign-membership-figures.js.map +1 -0
- package/src/lib/model/compile-client-automations.d.ts +65 -0
- package/src/lib/model/compile-client-automations.js +95 -0
- package/src/lib/model/compile-client-automations.js.map +1 -0
- package/src/lib/model/experiments.d.ts +130 -0
- package/src/lib/model/experiments.js +186 -0
- package/src/lib/model/experiments.js.map +1 -0
- package/src/lib/model/index.d.ts +26 -0
- package/src/lib/model/index.js +26 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/marketing-record-routes.d.ts +6 -0
- package/src/lib/model/marketing-record-routes.js +46 -0
- package/src/lib/model/marketing-record-routes.js.map +1 -0
- package/src/lib/model/overlay-copy.d.ts +62 -0
- package/src/lib/model/overlay-copy.js +78 -0
- package/src/lib/model/overlay-copy.js.map +1 -0
- package/src/lib/model/overlays.d.ts +113 -0
- package/src/lib/model/overlays.js +93 -0
- package/src/lib/model/overlays.js.map +1 -0
- package/src/lib/model/site-contract.d.ts +100 -0
- package/src/lib/model/site-contract.js +23 -0
- package/src/lib/model/site-contract.js.map +1 -0
- package/src/lib/plugin.d.ts +27 -0
- package/src/lib/plugin.js +168 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/preview-props.d.ts +31 -0
- package/src/lib/preview-props.js +54 -0
- package/src/lib/preview-props.js.map +1 -0
- package/src/lib/server/campaign-manage.d.ts +62 -0
- package/src/lib/server/campaign-manage.js +829 -0
- package/src/lib/server/campaign-manage.js.map +1 -0
- package/src/lib/server/campaign-process-scheduled.d.ts +26 -0
- package/src/lib/server/campaign-process-scheduled.js +198 -0
- package/src/lib/server/campaign-process-scheduled.js.map +1 -0
- package/src/lib/server/campaign-recipients.d.ts +55 -0
- package/src/lib/server/campaign-recipients.js +219 -0
- package/src/lib/server/campaign-recipients.js.map +1 -0
- package/src/lib/server/campaign-send.d.ts +487 -0
- package/src/lib/server/campaign-send.js +3157 -0
- package/src/lib/server/campaign-send.js.map +1 -0
- package/src/lib/server/email-campaign-reach.d.ts +174 -0
- package/src/lib/server/email-campaign-reach.js +254 -0
- package/src/lib/server/email-campaign-reach.js.map +1 -0
- package/src/lib/server/email-events.d.ts +27 -0
- package/src/lib/server/email-events.js +714 -0
- package/src/lib/server/email-events.js.map +1 -0
- package/src/lib/server/get-client-automations.d.ts +60 -0
- package/src/lib/server/get-client-automations.js +87 -0
- package/src/lib/server/get-client-automations.js.map +1 -0
- package/src/lib/server/get-overlays.d.ts +28 -0
- package/src/lib/server/get-overlays.js +65 -0
- package/src/lib/server/get-overlays.js.map +1 -0
- package/src/lib/server/get-screen-experiments.d.ts +42 -0
- package/src/lib/server/get-screen-experiments.js +82 -0
- package/src/lib/server/get-screen-experiments.js.map +1 -0
- package/src/lib/server/lists-materialize.d.ts +18 -0
- package/src/lib/server/lists-materialize.js +130 -0
- package/src/lib/server/lists-materialize.js.map +1 -0
- package/src/lib/server/marketing-figures.d.ts +37 -0
- package/src/lib/server/marketing-figures.js +248 -0
- package/src/lib/server/marketing-figures.js.map +1 -0
- package/src/lib/server/site-page-enricher.d.ts +26 -0
- package/src/lib/server/site-page-enricher.js +210 -0
- package/src/lib/server/site-page-enricher.js.map +1 -0
- package/src/lib/server.d.ts +20 -0
- package/src/lib/server.js +152 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/site.d.ts +22 -0
- package/src/lib/site.js +48 -0
- package/src/lib/site.js.map +1 -0
|
@@ -0,0 +1,1081 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2022 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
|
+
import * as Aglyn from "@aglyn/aglyn";
|
|
20
|
+
import { sendAnalyticsBeacon } from "@aglyn/aglyn/app-utils/analytics-beacon";
|
|
21
|
+
import { trackAuthoredEvent, trackEvent } from "@aglyn/aglyn/app-utils/analytics-events";
|
|
22
|
+
import { campaignTouchField } from "@aglyn/aglyn/app-utils/campaign-touch";
|
|
23
|
+
import { useEffect, useRef, useState } from "react";
|
|
24
|
+
import * as MarketingModel from "../model/index.js";
|
|
25
|
+
/**
|
|
26
|
+
* Overlay metrics beacon (AGL-200): fire-and-forget, never blocks UX.
|
|
27
|
+
* With an `overlayId` (marketing-hub docs, AGL-271) the collector also
|
|
28
|
+
* increments that overlay's own stats counters.
|
|
29
|
+
*/ function sendOverlayBeacon(hostId, overlay, overlayId) {
|
|
30
|
+
if (!hostId) return;
|
|
31
|
+
// Best-effort, and refused outright from a non-production build or a browser
|
|
32
|
+
// marked ours — the same gate the pageview beacon passes. The GA mirror
|
|
33
|
+
// below is NOT gated here: `trackEvent` carries the taxonomy's own
|
|
34
|
+
// environment check and stamps `traffic_type` rather than dropping the hit.
|
|
35
|
+
sendAnalyticsBeacon(_extends({
|
|
36
|
+
hostId,
|
|
37
|
+
overlay
|
|
38
|
+
}, overlayId ? {
|
|
39
|
+
overlayId
|
|
40
|
+
} : {}));
|
|
41
|
+
// GA mirror (wave v8): sites with Analytics configured see overlay
|
|
42
|
+
// engagement in their own property; no-op without gtag.
|
|
43
|
+
//
|
|
44
|
+
// Through the taxonomy since AGL-1591 rather than raw gtag. `trackEvent`
|
|
45
|
+
// already swallows everything and never throws, so the try/catch this used
|
|
46
|
+
// to need is gone rather than kept as decoration.
|
|
47
|
+
trackEvent('aglyn_overlay', _extends({
|
|
48
|
+
overlay_action: overlay
|
|
49
|
+
}, overlayId ? {
|
|
50
|
+
overlay_id: overlayId
|
|
51
|
+
} : {}));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Site-wide announcement bar (AGL-195). Dismissal is per content-hash in
|
|
55
|
+
* localStorage (no cookies): editing the text re-shows the bar for
|
|
56
|
+
* everyone who hid the old one.
|
|
57
|
+
*/ /**
|
|
58
|
+
* Experiments runner (AGL-253): deterministic per-visitor assignment for
|
|
59
|
+
* the screen's experiments. The first active experiment wins (one per
|
|
60
|
+
* screen); its variant tree — composed server-side per variant version —
|
|
61
|
+
* swaps in via canvas.setNodes on hydration. Exposures beacon on
|
|
62
|
+
* assignment; the conversion listener matches the experiment's goal and
|
|
63
|
+
* beacons once per pageview.
|
|
64
|
+
*/ function ExperimentsRunner(props) {
|
|
65
|
+
var _experiments_;
|
|
66
|
+
const { hostId, experiments, consentHost } = props;
|
|
67
|
+
useEffect(()=>{
|
|
68
|
+
var _ref;
|
|
69
|
+
var _experiment_goal;
|
|
70
|
+
if (!hostId || !experiments.length) return undefined;
|
|
71
|
+
const experiment = experiments[0];
|
|
72
|
+
// Visitor-id persistence honors the consent gate (AGL-1498): without an
|
|
73
|
+
// analytics yes on a consent-gated site, the id lives in sessionStorage —
|
|
74
|
+
// variants stay stable within the visit, and the indefinite identifier
|
|
75
|
+
// is not kept. The id never leaves the browser either way (the beacons
|
|
76
|
+
// below carry no visitor id), which is why this is a persistence
|
|
77
|
+
// downgrade rather than a kill switch on experiments.
|
|
78
|
+
const persistence = Aglyn.resolveVisitorIdPersistence(consentHost, Aglyn.readStoredVisitorConsent(hostId));
|
|
79
|
+
let visitorId;
|
|
80
|
+
try {
|
|
81
|
+
var _ref1, _ref2, _storage_getItem;
|
|
82
|
+
const storage = persistence === 'local' ? localStorage : sessionStorage;
|
|
83
|
+
// Read across BOTH stores so the assignment survives a consent move:
|
|
84
|
+
// a session id promoted to local (consent granted mid-visit) and a
|
|
85
|
+
// legacy local id migrating down to session both keep their variant.
|
|
86
|
+
visitorId = (_ref1 = (_ref2 = (_storage_getItem = storage.getItem(Aglyn.VISITOR_ID_STORAGE_KEY)) != null ? _storage_getItem : sessionStorage.getItem(Aglyn.VISITOR_ID_STORAGE_KEY)) != null ? _ref2 : localStorage.getItem(Aglyn.VISITOR_ID_STORAGE_KEY)) != null ? _ref1 : '';
|
|
87
|
+
if (!visitorId) {
|
|
88
|
+
visitorId = `v-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
|
|
89
|
+
}
|
|
90
|
+
storage.setItem(Aglyn.VISITOR_ID_STORAGE_KEY, visitorId);
|
|
91
|
+
// A pre-consent-tool id may still sit in localStorage from before the
|
|
92
|
+
// gate existed; while un-consented it must not persist.
|
|
93
|
+
if (persistence === 'session') {
|
|
94
|
+
localStorage.removeItem(Aglyn.VISITOR_ID_STORAGE_KEY);
|
|
95
|
+
}
|
|
96
|
+
} catch (unused) {
|
|
97
|
+
return undefined // No storage (privacy mode) — serve the default.
|
|
98
|
+
;
|
|
99
|
+
}
|
|
100
|
+
const variant = MarketingModel.assignExperimentVariant(// Wire-typed props (AGL-418); the plugin narrows its own enum.
|
|
101
|
+
_extends({
|
|
102
|
+
status: experiment.status,
|
|
103
|
+
variants: experiment.variants
|
|
104
|
+
}, experiment.winnerVariantId ? {
|
|
105
|
+
winnerVariantId: experiment.winnerVariantId
|
|
106
|
+
} : {}, experiment.endAtMs ? {
|
|
107
|
+
endAtMs: experiment.endAtMs
|
|
108
|
+
} : {}), experiment.id, visitorId);
|
|
109
|
+
if (!variant) return undefined;
|
|
110
|
+
const payload = experiment.payloads[variant.id];
|
|
111
|
+
if (payload) Aglyn.canvas.setNodes(payload);
|
|
112
|
+
const beacon = (kind)=>{
|
|
113
|
+
void fetch('/api/experiments/track', {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: {
|
|
116
|
+
'Content-Type': 'application/json'
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
hostId,
|
|
120
|
+
experimentId: experiment.id,
|
|
121
|
+
variantId: variant.id,
|
|
122
|
+
kind
|
|
123
|
+
}),
|
|
124
|
+
keepalive: true
|
|
125
|
+
}).catch(()=>undefined);
|
|
126
|
+
// GA mirror (wave v8): exposure/conversion events land in the
|
|
127
|
+
// site's own Analytics property when configured. Through the taxonomy
|
|
128
|
+
// since AGL-1591 — which also reserves the name against an authored
|
|
129
|
+
// `trackGaEvent` step, so nothing hand-written can add itself to the
|
|
130
|
+
// counts that decide which variant wins.
|
|
131
|
+
trackEvent('aglyn_experiment', {
|
|
132
|
+
experiment_id: experiment.id,
|
|
133
|
+
variant_id: variant.id,
|
|
134
|
+
experiment_action: kind
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
// Finished experiments serve the winner without counting stats.
|
|
138
|
+
if (experiment.status !== 'running') return undefined;
|
|
139
|
+
beacon('exposure');
|
|
140
|
+
// Goal listener — client-observable goals only; server-side goals
|
|
141
|
+
// (bookings etc.) count through their own pipelines in a follow-up.
|
|
142
|
+
const cleanups = [];
|
|
143
|
+
let converted = false;
|
|
144
|
+
const convert = ()=>{
|
|
145
|
+
if (converted) return;
|
|
146
|
+
converted = true;
|
|
147
|
+
beacon('conversion');
|
|
148
|
+
};
|
|
149
|
+
const goal = (_ref = (_experiment_goal = experiment.goal) == null ? void 0 : _experiment_goal.event) != null ? _ref : 'formSubmission';
|
|
150
|
+
if (goal === 'formSubmission') {
|
|
151
|
+
const onSubmit = ()=>convert();
|
|
152
|
+
document.addEventListener('submit', onSubmit, true);
|
|
153
|
+
cleanups.push(()=>document.removeEventListener('submit', onSubmit, true));
|
|
154
|
+
} else if (goal === 'elementClick') {
|
|
155
|
+
var _experiment_goal_filter, _experiment_goal1;
|
|
156
|
+
const selector = ((_experiment_goal1 = experiment.goal) == null ? void 0 : (_experiment_goal_filter = _experiment_goal1.filter) == null ? void 0 : _experiment_goal_filter.trim()) || 'a, button';
|
|
157
|
+
const onClick = (event)=>{
|
|
158
|
+
var _target_closest;
|
|
159
|
+
const target = event.target;
|
|
160
|
+
if (target == null ? void 0 : (_target_closest = target.closest) == null ? void 0 : _target_closest.call(target, selector)) convert();
|
|
161
|
+
};
|
|
162
|
+
document.addEventListener('click', onClick, true);
|
|
163
|
+
cleanups.push(()=>document.removeEventListener('click', onClick, true));
|
|
164
|
+
} else if (goal === 'scrollDepth') {
|
|
165
|
+
const onScroll = ()=>{
|
|
166
|
+
const doc = document.documentElement;
|
|
167
|
+
const max = doc.scrollHeight - window.innerHeight;
|
|
168
|
+
if (max <= 0 || window.scrollY / max * 100 >= 50) {
|
|
169
|
+
convert();
|
|
170
|
+
window.removeEventListener('scroll', onScroll);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
window.addEventListener('scroll', onScroll, {
|
|
174
|
+
passive: true
|
|
175
|
+
});
|
|
176
|
+
cleanups.push(()=>window.removeEventListener('scroll', onScroll));
|
|
177
|
+
} else if (goal === 'timeOnPage') {
|
|
178
|
+
const timer = setTimeout(convert, 30000);
|
|
179
|
+
cleanups.push(()=>clearTimeout(timer));
|
|
180
|
+
}
|
|
181
|
+
return ()=>{
|
|
182
|
+
for (const cleanup of cleanups)cleanup();
|
|
183
|
+
};
|
|
184
|
+
// Experiments are static per pageview (ISR props).
|
|
185
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
186
|
+
}, [
|
|
187
|
+
hostId,
|
|
188
|
+
experiments.length && ((_experiments_ = experiments[0]) == null ? void 0 : _experiments_.id)
|
|
189
|
+
]);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Site-event automations engine (AGL-256/257): arms the page's triggers
|
|
194
|
+
* (scroll depth, element visibility/clicks, exit intent, dwell time),
|
|
195
|
+
* runs each fired action's CLIENT steps in the browser, and dispatches
|
|
196
|
+
* actions with server steps to /api/events/dispatch. Every trigger fires
|
|
197
|
+
* at most once per pageview.
|
|
198
|
+
*/ function AutomationsEngine(props) {
|
|
199
|
+
const { hostId, automations, screens, onShowOverlay } = props;
|
|
200
|
+
const showOverlayRef = useRef(onShowOverlay);
|
|
201
|
+
showOverlayRef.current = onShowOverlay;
|
|
202
|
+
useEffect(()=>{
|
|
203
|
+
if (!hostId || !automations.length) return undefined;
|
|
204
|
+
const fired = new Set();
|
|
205
|
+
const cleanups = [];
|
|
206
|
+
const showToast = (message, severity)=>{
|
|
207
|
+
const toast = document.createElement('div');
|
|
208
|
+
toast.textContent = message;
|
|
209
|
+
toast.setAttribute('role', 'status');
|
|
210
|
+
Object.assign(toast.style, {
|
|
211
|
+
position: 'fixed',
|
|
212
|
+
bottom: '16px',
|
|
213
|
+
left: '50%',
|
|
214
|
+
transform: 'translateX(-50%)',
|
|
215
|
+
zIndex: '2147483000',
|
|
216
|
+
padding: '10px 16px',
|
|
217
|
+
borderRadius: '8px',
|
|
218
|
+
fontSize: '14px',
|
|
219
|
+
fontFamily: 'system-ui, sans-serif',
|
|
220
|
+
color: '#fff',
|
|
221
|
+
backgroundColor: severity === 'error' ? '#c62828' : severity === 'warning' ? '#e65100' : severity === 'success' ? '#2e7d32' : 'rgba(0, 0, 0, 0.85)',
|
|
222
|
+
maxWidth: '90vw'
|
|
223
|
+
});
|
|
224
|
+
document.body.appendChild(toast);
|
|
225
|
+
setTimeout(()=>toast.remove(), 6000);
|
|
226
|
+
};
|
|
227
|
+
const runClientSteps = (automation)=>{
|
|
228
|
+
for (const step of automation.steps){
|
|
229
|
+
try {
|
|
230
|
+
if (step.type === 'addClass' || step.type === 'removeClass' || step.type === 'toggleClass') {
|
|
231
|
+
// Expand the raw-id selector to also match the layout-composed
|
|
232
|
+
// live id (AGL-573); non-leaf selectors are unchanged.
|
|
233
|
+
const targets = document.querySelectorAll(Aglyn.expandLeafSelector(step.selector));
|
|
234
|
+
targets.forEach((element)=>{
|
|
235
|
+
if (step.type === 'addClass') {
|
|
236
|
+
element.classList.add(step.className);
|
|
237
|
+
} else if (step.type === 'removeClass') {
|
|
238
|
+
element.classList.remove(step.className);
|
|
239
|
+
} else {
|
|
240
|
+
element.classList.toggle(step.className);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
} else if (step.type === 'setAttribute' || step.type === 'removeAttribute') {
|
|
244
|
+
/*
|
|
245
|
+
The semantics half of a hand-built disclosure (AGL-2546).
|
|
246
|
+
|
|
247
|
+
Deliberately in THIS loop rather than behind a guard of its own:
|
|
248
|
+
it then inherits exactly the gating the visibility steps have,
|
|
249
|
+
including which surfaces mount the runtime at all. A separate
|
|
250
|
+
path reaching `document.querySelector` directly is how this
|
|
251
|
+
would come to run somewhere `showElement` does not.
|
|
252
|
+
|
|
253
|
+
The name is re-checked here and not only in the editor. The
|
|
254
|
+
editor is a convenience; a step can reach this runtime from a
|
|
255
|
+
document written before the allowlist existed, or by any route
|
|
256
|
+
that does not pass through the console UI.
|
|
257
|
+
|
|
258
|
+
A rejected name is a NO-OP, not a throw — the `catch` around
|
|
259
|
+
this loop would otherwise let one bad attribute name silently
|
|
260
|
+
drop every step after it in the same automation.
|
|
261
|
+
*/ if (Aglyn.isInteractionAttributeAllowed(step.name)) {
|
|
262
|
+
const name = step.name.trim().toLowerCase();
|
|
263
|
+
const targets = document.querySelectorAll(Aglyn.expandLeafSelector(step.selector));
|
|
264
|
+
targets.forEach((element)=>{
|
|
265
|
+
if (step.type === 'setAttribute') {
|
|
266
|
+
var _step_value;
|
|
267
|
+
element.setAttribute(name, (_step_value = step.value) != null ? _step_value : '');
|
|
268
|
+
} else {
|
|
269
|
+
element.removeAttribute(name);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
} else if (step.type === 'scrollTo') {
|
|
274
|
+
// One target, the first one on screen (AGL-2867). A target that
|
|
275
|
+
// is missing, deleted or hidden makes the step a no-op, and the
|
|
276
|
+
// steps after it in this automation still run.
|
|
277
|
+
Aglyn.runScrollToStep(Aglyn.expandLeafSelector(step.selector), {
|
|
278
|
+
behavior: step.behavior,
|
|
279
|
+
offsetPx: step.offsetPx
|
|
280
|
+
});
|
|
281
|
+
} else if (step.type === 'playVideo') {
|
|
282
|
+
// The Video element's own poster press, sent to the element: the
|
|
283
|
+
// player loads only now, exactly as it would for a click on the
|
|
284
|
+
// poster, and under the same consent rules.
|
|
285
|
+
Aglyn.runPlayVideoStep(Aglyn.expandLeafSelector(step.selector));
|
|
286
|
+
} else if (step.type === 'stickyNav') {
|
|
287
|
+
var _step_selector;
|
|
288
|
+
const target = document.querySelector(Aglyn.expandLeafSelector(((_step_selector = step.selector) == null ? void 0 : _step_selector.trim()) || 'header, nav'));
|
|
289
|
+
if (target instanceof HTMLElement) {
|
|
290
|
+
target.style.position = 'sticky';
|
|
291
|
+
target.style.top = '0';
|
|
292
|
+
target.style.zIndex = '1100';
|
|
293
|
+
}
|
|
294
|
+
} else if (step.type === 'showHtml') {
|
|
295
|
+
const container = document.createElement('div');
|
|
296
|
+
// Sanitize (AGL-504): unlike runJs, showHtml is NOT dropped by the
|
|
297
|
+
// JS entitlement gate, so raw innerHTML would let any automations-
|
|
298
|
+
// tier author execute event-handler attributes (<img onerror>).
|
|
299
|
+
//
|
|
300
|
+
// ONE policy for the surface (AGL-2486). AGL-1901 moved rich text
|
|
301
|
+
// and Custom HTML onto `sanitizeAuthorHtml`; this call was the
|
|
302
|
+
// last raw DOMPurify left on a published page, and two sanitizers
|
|
303
|
+
// with different rules on one surface is worse than the weaker of
|
|
304
|
+
// them — a reviewer reads the strict one and assumes it covers
|
|
305
|
+
// both. It did not: DOMPurify filters NO CSS, so every `style`
|
|
306
|
+
// attribute arriving here reached the visitor unexamined while
|
|
307
|
+
// the Custom HTML block beside it went through `sanitizeAuthorCss`.
|
|
308
|
+
//
|
|
309
|
+
// Nothing this used to refuse is now allowed. `sanitizeAuthorHtml`
|
|
310
|
+
// is a strict SUBSET of the DOMPurify config it replaces, and that
|
|
311
|
+
// is measured rather than asserted: `rich-text-hydration.spec.tsx`
|
|
312
|
+
// holds this exact former config as `referenceShowHtmlPurify` and
|
|
313
|
+
// runs the shared corpus through both. So the change is only in
|
|
314
|
+
// what it ADDITIONALLY refuses:
|
|
315
|
+
//
|
|
316
|
+
// - `style` now gets the CSS rule — `http:` and unknown-scheme
|
|
317
|
+
// `url()` neutered to `about:invalid`, and `@import`,
|
|
318
|
+
// `expression()`, `behavior:` and `-moz-binding` dropping the
|
|
319
|
+
// declaration whole. Character references are decoded BEFORE
|
|
320
|
+
// the scheme is judged, which the old config did not do: it
|
|
321
|
+
// emitted `url(http://…)` as a literal `http://`.
|
|
322
|
+
// - `<form>` stops being an element. The former FORBID_TAGS here
|
|
323
|
+
// listed `script, iframe, object, embed, base` and omitted
|
|
324
|
+
// `form` — alone among this repo's author-HTML configs, both of
|
|
325
|
+
// which forbade it — so a password field on the owner's live
|
|
326
|
+
// page was one paste away.
|
|
327
|
+
//
|
|
328
|
+
// What is deliberately NOT refused, on either side of the change:
|
|
329
|
+
// a third-party `https:` `url()`, and `position:fixed` / `z-index`.
|
|
330
|
+
// The first is AGL-1725's actor analysis (the site owner chooses
|
|
331
|
+
// recipients for their own visitors); the second is a capability
|
|
332
|
+
// the Styles panel already gives the same author as a first-class
|
|
333
|
+
// field, so refusing it only here would move nothing.
|
|
334
|
+
container.innerHTML = Aglyn.sanitizeAuthorHtml(step.html);
|
|
335
|
+
document.body.appendChild(container);
|
|
336
|
+
} else if (step.type === 'runJs') {
|
|
337
|
+
// Business-gated server-side (dropped from props otherwise);
|
|
338
|
+
// the site owner's own code running on their own page.
|
|
339
|
+
// eslint-disable-next-line no-new-func
|
|
340
|
+
new Function(step.code)();
|
|
341
|
+
} else if (step.type === 'redirect') {
|
|
342
|
+
// Screen targets resolve through the routing map (AGL-339) so
|
|
343
|
+
// slug renames never break the interaction; url is the manual
|
|
344
|
+
// fallback.
|
|
345
|
+
const screenSlug = step.screenId != null ? screens == null ? void 0 : screens[step.screenId] : undefined;
|
|
346
|
+
const destination = screenSlug != null ? Aglyn.screenRoutePathToUrl(screenSlug) : step.url;
|
|
347
|
+
if (destination) window.location.assign(destination);
|
|
348
|
+
} else if (step.type === 'showElement' || step.type === 'hideElement' || step.type === 'toggleElement') {
|
|
349
|
+
// Element choreography (AGL-562): class-based so "show"
|
|
350
|
+
// reveals targets the author started hidden. The tenant page
|
|
351
|
+
// ships the hidden-class rule in its SSR HTML; ensure is the
|
|
352
|
+
// fallback for other embeddings. Grace delays + Escape/
|
|
353
|
+
// outside-click dismissal ride the same step (AGL-589).
|
|
354
|
+
Aglyn.ensureElementHiddenStyle();
|
|
355
|
+
Aglyn.runElementVisibilityStep(step.type === 'showElement' ? 'show' : step.type === 'hideElement' ? 'hide' : 'toggle', // Expand so a show/hide/toggle authored against the raw
|
|
356
|
+
// canvas id still reaches the layout-composed node (AGL-573).
|
|
357
|
+
Aglyn.expandLeafSelector(step.selector), {
|
|
358
|
+
delayMs: step.delayMs,
|
|
359
|
+
dismissOn: step.dismissOn
|
|
360
|
+
});
|
|
361
|
+
} else if (step.type === 'openDrawer' || step.type === 'closeDrawer' || step.type === 'toggleDrawer') {
|
|
362
|
+
// Drawer commands (AGL-562) ride the window event bus; the
|
|
363
|
+
// muiDrawer element answers (first drawer for broadcasts).
|
|
364
|
+
Aglyn.dispatchDrawerCommand(step.type === 'openDrawer' ? 'open' : step.type === 'closeDrawer' ? 'close' : 'toggle', step.drawerNodeId || undefined);
|
|
365
|
+
} else if (step.type === 'openMenu' || step.type === 'closeMenu' || step.type === 'toggleMenu') {
|
|
366
|
+
// Menu commands (AGL-568) ride their own event bus like
|
|
367
|
+
// drawers; the nav-menu elements answer (first menu for
|
|
368
|
+
// broadcasts). A hover-enter trigger stamps the hover flag
|
|
369
|
+
// so a menu opened this way closes itself when the pointer
|
|
370
|
+
// leaves the trigger + panel surface.
|
|
371
|
+
Aglyn.dispatchMenuCommand(step.type === 'openMenu' ? 'open' : step.type === 'closeMenu' ? 'close' : 'toggle', step.menuNodeId || undefined, {
|
|
372
|
+
hover: automation.event === 'elementHoverEnter'
|
|
373
|
+
});
|
|
374
|
+
} else if (step.type === 'trackGaEvent') {
|
|
375
|
+
// AGL-1587: the name and the params are AUTHORED, which makes
|
|
376
|
+
// this the least trustworthy analytics input in the repo — a
|
|
377
|
+
// param could carry a form field's value. It goes through the
|
|
378
|
+
// shared sanitizer and the reserved-name refusal like everything
|
|
379
|
+
// else, never raw gtag.
|
|
380
|
+
trackAuthoredEvent(step.eventName, step.params);
|
|
381
|
+
} else if (step.type === 'siteAlert') {
|
|
382
|
+
var _step_message, _step_severity;
|
|
383
|
+
showToast(String((_step_message = step.message) != null ? _step_message : '').slice(0, 300), (_step_severity = step.severity) != null ? _step_severity : 'info');
|
|
384
|
+
} else if (step.type === 'showOverlay') {
|
|
385
|
+
if (step.overlayId) showOverlayRef.current(step.overlayId);
|
|
386
|
+
}
|
|
387
|
+
} catch (error) {
|
|
388
|
+
console.error('automation step failed', error);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
const fire = (automation)=>{
|
|
393
|
+
// Repeatable UI choreography (AGL-562): everyTime automations skip
|
|
394
|
+
// the once-per-pageview gate (explicit caps below still apply).
|
|
395
|
+
if (!automation.everyTime) {
|
|
396
|
+
if (fired.has(automation.id)) return;
|
|
397
|
+
fired.add(automation.id);
|
|
398
|
+
}
|
|
399
|
+
// Once per visitor (AGL-266): a persisted flag outlives the pageview.
|
|
400
|
+
if (automation.oncePerVisitor) {
|
|
401
|
+
const key = `aglyn:auto:${automation.id}`;
|
|
402
|
+
try {
|
|
403
|
+
if (localStorage.getItem(key)) return;
|
|
404
|
+
localStorage.setItem(key, '1');
|
|
405
|
+
} catch (unused) {
|
|
406
|
+
// Storage unavailable — degrade to once per pageview.
|
|
407
|
+
}
|
|
408
|
+
} else if (automation.oncePerSession) {
|
|
409
|
+
// Once per session (AGL-274): survives navigation, not the tab.
|
|
410
|
+
const key = `aglyn:auto:${automation.id}`;
|
|
411
|
+
try {
|
|
412
|
+
if (sessionStorage.getItem(key)) return;
|
|
413
|
+
sessionStorage.setItem(key, '1');
|
|
414
|
+
} catch (unused) {
|
|
415
|
+
// Storage unavailable — degrade to once per pageview.
|
|
416
|
+
}
|
|
417
|
+
} else if (automation.cooldownMinutes) {
|
|
418
|
+
// Cooldown (AGL-274): a timestamp gates re-fires for the visitor.
|
|
419
|
+
const key = `aglyn:auto:${automation.id}:at`;
|
|
420
|
+
try {
|
|
421
|
+
var _localStorage_getItem;
|
|
422
|
+
const last = Number((_localStorage_getItem = localStorage.getItem(key)) != null ? _localStorage_getItem : 0);
|
|
423
|
+
if (last && Date.now() - last < automation.cooldownMinutes * 60000) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
localStorage.setItem(key, String(Date.now()));
|
|
427
|
+
} catch (unused) {
|
|
428
|
+
// Storage unavailable — degrade to once per pageview.
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
runClientSteps(automation);
|
|
432
|
+
if (automation.hasServerSteps) {
|
|
433
|
+
void fetch('/api/events/dispatch', {
|
|
434
|
+
method: 'POST',
|
|
435
|
+
headers: {
|
|
436
|
+
'Content-Type': 'application/json'
|
|
437
|
+
},
|
|
438
|
+
body: JSON.stringify({
|
|
439
|
+
hostId,
|
|
440
|
+
actionId: automation.id,
|
|
441
|
+
event: automation.event,
|
|
442
|
+
payload: {
|
|
443
|
+
path: window.location.pathname
|
|
444
|
+
}
|
|
445
|
+
}),
|
|
446
|
+
keepalive: true
|
|
447
|
+
}).catch(()=>undefined);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
/**
|
|
451
|
+
* Elements that arrive AFTER this effect binds (AGL-2512).
|
|
452
|
+
*
|
|
453
|
+
* `elementVisible` and `scrollToElement` are the only two triggers that
|
|
454
|
+
* resolve their targets by querying the DOM — every other one listens on
|
|
455
|
+
* `document` and so needs no element to exist yet. A single query at bind
|
|
456
|
+
* time made them dead on the two pages where the tree lands late: a
|
|
457
|
+
* deferred lazy panel patched back in on interaction (AGL-1285), and a
|
|
458
|
+
* gated screen's tree swapped in once the visitor is past the gate
|
|
459
|
+
* (AGL-2510).
|
|
460
|
+
*
|
|
461
|
+
* One MutationObserver for all of them, not one each, and it stops the
|
|
462
|
+
* moment nothing is left watching — these triggers fire once per pageview
|
|
463
|
+
* by construction, so a page whose targets have all been seen pays
|
|
464
|
+
* nothing for the rest of the visit. A page with no such automation never
|
|
465
|
+
* starts one at all.
|
|
466
|
+
*/ const pendingObservers = [];
|
|
467
|
+
let domObserver = null;
|
|
468
|
+
const rescan = ()=>{
|
|
469
|
+
for (const attach of [
|
|
470
|
+
...pendingObservers
|
|
471
|
+
])attach();
|
|
472
|
+
if (!pendingObservers.length) {
|
|
473
|
+
domObserver == null ? void 0 : domObserver.disconnect();
|
|
474
|
+
domObserver = null;
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
const watchLateElements = (attach)=>{
|
|
478
|
+
pendingObservers.push(attach);
|
|
479
|
+
if (domObserver || typeof MutationObserver === 'undefined') return;
|
|
480
|
+
domObserver = new MutationObserver(rescan);
|
|
481
|
+
domObserver.observe(document.body, {
|
|
482
|
+
childList: true,
|
|
483
|
+
subtree: true
|
|
484
|
+
});
|
|
485
|
+
};
|
|
486
|
+
const stopWatching = (attach)=>{
|
|
487
|
+
const index = pendingObservers.indexOf(attach);
|
|
488
|
+
if (index >= 0) pendingObservers.splice(index, 1);
|
|
489
|
+
if (!pendingObservers.length) {
|
|
490
|
+
domObserver == null ? void 0 : domObserver.disconnect();
|
|
491
|
+
domObserver = null;
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
cleanups.push(()=>{
|
|
495
|
+
pendingObservers.length = 0;
|
|
496
|
+
domObserver == null ? void 0 : domObserver.disconnect();
|
|
497
|
+
domObserver = null;
|
|
498
|
+
});
|
|
499
|
+
for (const automation of automations){
|
|
500
|
+
const { event, threshold } = automation;
|
|
501
|
+
// Composition namespaces a node's live `data-aglyn` id, but the
|
|
502
|
+
// interaction builder records the RAW canvas id in the selector.
|
|
503
|
+
// Expand it so the trigger still matches the live element — layouts
|
|
504
|
+
// (AGL-573) and reusable-component grafts (AGL-1229), the latter being
|
|
505
|
+
// why every interaction authored inside the Site nav bound to nothing.
|
|
506
|
+
// Hand-typed CSS selectors pass through untouched.
|
|
507
|
+
const selector = automation.selector ? Aglyn.expandLeafSelector(automation.selector) : automation.selector;
|
|
508
|
+
if (event === 'pageVisit') {
|
|
509
|
+
fire(automation);
|
|
510
|
+
} else if (event === 'timeOnPage') {
|
|
511
|
+
const timer = setTimeout(()=>fire(automation), Math.max(1, threshold != null ? threshold : 5) * 1000);
|
|
512
|
+
cleanups.push(()=>clearTimeout(timer));
|
|
513
|
+
} else if (event === 'scrollDepth') {
|
|
514
|
+
const onScroll = ()=>{
|
|
515
|
+
const doc = document.documentElement;
|
|
516
|
+
const max = doc.scrollHeight - window.innerHeight;
|
|
517
|
+
const percent = max > 0 ? window.scrollY / max * 100 : 100;
|
|
518
|
+
if (percent >= Math.min(100, Math.max(1, threshold != null ? threshold : 50))) {
|
|
519
|
+
fire(automation);
|
|
520
|
+
window.removeEventListener('scroll', onScroll);
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
window.addEventListener('scroll', onScroll, {
|
|
524
|
+
passive: true
|
|
525
|
+
});
|
|
526
|
+
cleanups.push(()=>window.removeEventListener('scroll', onScroll));
|
|
527
|
+
} else if (event === 'elementVisible' || event === 'scrollToElement') {
|
|
528
|
+
if (!selector) continue;
|
|
529
|
+
let fired = false;
|
|
530
|
+
const observer = new IntersectionObserver((entries)=>{
|
|
531
|
+
if (fired || !entries.some((entry)=>entry.isIntersecting)) return;
|
|
532
|
+
fired = true;
|
|
533
|
+
fire(automation);
|
|
534
|
+
observer.disconnect();
|
|
535
|
+
stopWatching(attachTargets);
|
|
536
|
+
});
|
|
537
|
+
// Re-entrant: called again on every DOM change until the trigger
|
|
538
|
+
// fires, and observing an element twice is a no-op, so a rescan costs
|
|
539
|
+
// one `querySelectorAll` and nothing else.
|
|
540
|
+
const attachTargets = ()=>{
|
|
541
|
+
if (fired) return stopWatching(attachTargets);
|
|
542
|
+
document.querySelectorAll(selector).forEach((target)=>observer.observe(target));
|
|
543
|
+
};
|
|
544
|
+
attachTargets();
|
|
545
|
+
watchLateElements(attachTargets);
|
|
546
|
+
cleanups.push(()=>observer.disconnect());
|
|
547
|
+
} else if (event === 'elementClick') {
|
|
548
|
+
if (!selector) continue;
|
|
549
|
+
const onClick = (mouseEvent)=>{
|
|
550
|
+
var _target_closest;
|
|
551
|
+
const target = mouseEvent.target;
|
|
552
|
+
if (target == null ? void 0 : (_target_closest = target.closest) == null ? void 0 : _target_closest.call(target, selector)) fire(automation);
|
|
553
|
+
};
|
|
554
|
+
document.addEventListener('click', onClick, true);
|
|
555
|
+
cleanups.push(()=>document.removeEventListener('click', onClick, true));
|
|
556
|
+
} else if (event === 'elementHoverEnter' || event === 'elementHoverLeave') {
|
|
557
|
+
// Hover choreography (AGL-562): delegated mouseover/mouseout with
|
|
558
|
+
// enter/leave semantics — moves WITHIN the matched element (its
|
|
559
|
+
// own children) never re-fire.
|
|
560
|
+
if (!selector) continue;
|
|
561
|
+
const onHover = (mouseEvent)=>{
|
|
562
|
+
var _target_closest;
|
|
563
|
+
const target = mouseEvent.target;
|
|
564
|
+
const match = target == null ? void 0 : (_target_closest = target.closest) == null ? void 0 : _target_closest.call(target, selector);
|
|
565
|
+
if (!match) return;
|
|
566
|
+
const related = mouseEvent.relatedTarget;
|
|
567
|
+
if (related && match.contains(related)) return;
|
|
568
|
+
fire(automation);
|
|
569
|
+
};
|
|
570
|
+
const type = event === 'elementHoverEnter' ? 'mouseover' : 'mouseout';
|
|
571
|
+
document.addEventListener(type, onHover, true);
|
|
572
|
+
cleanups.push(()=>document.removeEventListener(type, onHover, true));
|
|
573
|
+
} else if (event === 'exitIntent') {
|
|
574
|
+
const onLeave = (mouseEvent)=>{
|
|
575
|
+
if (mouseEvent.clientY <= 0) fire(automation);
|
|
576
|
+
};
|
|
577
|
+
document.addEventListener('mouseleave', onLeave);
|
|
578
|
+
cleanups.push(()=>document.removeEventListener('mouseleave', onLeave));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return ()=>{
|
|
582
|
+
for (const cleanup of cleanups)cleanup();
|
|
583
|
+
};
|
|
584
|
+
// Automations are static per pageview (ISR props).
|
|
585
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
586
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
587
|
+
}, [
|
|
588
|
+
hostId,
|
|
589
|
+
screens,
|
|
590
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
591
|
+
JSON.stringify(automations.map((automation)=>automation.id))
|
|
592
|
+
]);
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
function AnnouncementBar(props) {
|
|
596
|
+
const { bar, hostId } = props;
|
|
597
|
+
const storageKey = `aglyn-abar-${bar.contentHash}`;
|
|
598
|
+
const [hidden, setHidden] = useState(true);
|
|
599
|
+
useEffect(()=>{
|
|
600
|
+
try {
|
|
601
|
+
setHidden(Boolean(window.localStorage.getItem(storageKey)));
|
|
602
|
+
} catch (unused) {
|
|
603
|
+
setHidden(false);
|
|
604
|
+
}
|
|
605
|
+
}, [
|
|
606
|
+
storageKey
|
|
607
|
+
]);
|
|
608
|
+
// Impression (AGL-271): once per pageview, only when actually shown.
|
|
609
|
+
useEffect(()=>{
|
|
610
|
+
if (!hidden) sendOverlayBeacon(hostId, 'barImpression', bar.overlayId);
|
|
611
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
612
|
+
}, [
|
|
613
|
+
hidden
|
|
614
|
+
]);
|
|
615
|
+
if (hidden) return null;
|
|
616
|
+
const style = {
|
|
617
|
+
position: 'relative',
|
|
618
|
+
zIndex: 1300,
|
|
619
|
+
display: 'flex',
|
|
620
|
+
alignItems: 'center',
|
|
621
|
+
justifyContent: 'center',
|
|
622
|
+
gap: 8,
|
|
623
|
+
padding: '8px 40px 8px 16px',
|
|
624
|
+
fontSize: 14,
|
|
625
|
+
fontFamily: 'system-ui, sans-serif',
|
|
626
|
+
textAlign: 'center',
|
|
627
|
+
color: bar.textColor || '#fff',
|
|
628
|
+
backgroundColor: bar.backgroundColor || '#111827'
|
|
629
|
+
};
|
|
630
|
+
const text = bar.href ? /*#__PURE__*/ _jsx("a", {
|
|
631
|
+
href: bar.href,
|
|
632
|
+
onClick: ()=>sendOverlayBeacon(hostId, 'barClick', bar.overlayId),
|
|
633
|
+
style: {
|
|
634
|
+
color: 'inherit',
|
|
635
|
+
textDecoration: 'underline'
|
|
636
|
+
},
|
|
637
|
+
children: bar.text
|
|
638
|
+
}) : /*#__PURE__*/ _jsx("span", {
|
|
639
|
+
children: bar.text
|
|
640
|
+
});
|
|
641
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
642
|
+
role: "region",
|
|
643
|
+
"aria-label": "Announcement",
|
|
644
|
+
style: style,
|
|
645
|
+
children: [
|
|
646
|
+
text,
|
|
647
|
+
bar.dismissible ? /*#__PURE__*/ _jsx("button", {
|
|
648
|
+
type: "button",
|
|
649
|
+
"aria-label": "Dismiss announcement",
|
|
650
|
+
onClick: ()=>{
|
|
651
|
+
try {
|
|
652
|
+
window.localStorage.setItem(storageKey, '1');
|
|
653
|
+
} catch (unused) {
|
|
654
|
+
// Storage may be unavailable (private mode); hide anyway.
|
|
655
|
+
}
|
|
656
|
+
sendOverlayBeacon(hostId, 'barDismiss', bar.overlayId);
|
|
657
|
+
setHidden(true);
|
|
658
|
+
},
|
|
659
|
+
style: {
|
|
660
|
+
position: 'absolute',
|
|
661
|
+
right: 8,
|
|
662
|
+
top: '50%',
|
|
663
|
+
transform: 'translateY(-50%)',
|
|
664
|
+
border: 'none',
|
|
665
|
+
background: 'transparent',
|
|
666
|
+
color: 'inherit',
|
|
667
|
+
fontSize: 16,
|
|
668
|
+
lineHeight: 1,
|
|
669
|
+
cursor: 'pointer',
|
|
670
|
+
padding: 6
|
|
671
|
+
},
|
|
672
|
+
children: '✕'
|
|
673
|
+
}) : null
|
|
674
|
+
]
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Promotional popup (AGL-196). Client owns: schedule-window re-check (the
|
|
679
|
+
* page is ISR-cached), trigger timing (delay/scroll/exit), and frequency
|
|
680
|
+
* capping via a per-content-hash localStorage stamp (no cookies). ESC and
|
|
681
|
+
* backdrop close; reduced-motion preferences skip the entrance ease.
|
|
682
|
+
*/ function PopupOverlay(props) {
|
|
683
|
+
var _popup_imageAlt;
|
|
684
|
+
const { popup, hostId } = props;
|
|
685
|
+
const storageKey = `aglyn-popup-${popup.contentHash}`;
|
|
686
|
+
const [open, setOpen] = useState(false);
|
|
687
|
+
// Email capture (AGL-200): submits into the forms pipeline (inbox +
|
|
688
|
+
// contacts); success suppresses the popup for ~10 years.
|
|
689
|
+
const [email, setEmail] = useState('');
|
|
690
|
+
// The honeypot's value, held in state so whatever a bot typed into the
|
|
691
|
+
// hidden input below is what gets POSTed (AGL-1665). This used to be a
|
|
692
|
+
// hardcoded `website: ''` in the request body: the route drops a
|
|
693
|
+
// submission only when the field is NON-empty, so a constant empty string
|
|
694
|
+
// never failed the check — it removed it. There was no field on the page
|
|
695
|
+
// to fill and no value a script could set, which made the popup the one
|
|
696
|
+
// form surface on the site with no honeypot at all.
|
|
697
|
+
const [website, setWebsite] = useState('');
|
|
698
|
+
const [submitted, setSubmitted] = useState(false);
|
|
699
|
+
useEffect(()=>{
|
|
700
|
+
if (open) sendOverlayBeacon(hostId, 'popupImpression', popup.overlayId);
|
|
701
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
702
|
+
}, [
|
|
703
|
+
open,
|
|
704
|
+
hostId
|
|
705
|
+
]);
|
|
706
|
+
useEffect(()=>{
|
|
707
|
+
const now = Date.now();
|
|
708
|
+
if (popup.startAtMs && now < popup.startAtMs) return;
|
|
709
|
+
if (popup.endAtMs && now > popup.endAtMs) return;
|
|
710
|
+
try {
|
|
711
|
+
var _window_localStorage_getItem;
|
|
712
|
+
// The decision itself is a pure function in the model (AGL-2174) —
|
|
713
|
+
// which store to read is the whole feature, and it has to agree with
|
|
714
|
+
// what `close` writes.
|
|
715
|
+
const suppressed = MarketingModel.popupSuppressed(popup, MarketingModel.popupCapStore(popup) === 'session' ? {
|
|
716
|
+
sessionFlag: window.sessionStorage.getItem(storageKey)
|
|
717
|
+
} : {
|
|
718
|
+
lastShownMs: Number((_window_localStorage_getItem = window.localStorage.getItem(storageKey)) != null ? _window_localStorage_getItem : 0)
|
|
719
|
+
}, now);
|
|
720
|
+
if (suppressed) return;
|
|
721
|
+
} catch (unused) {
|
|
722
|
+
// Storage unavailable: show at most per-pageview.
|
|
723
|
+
}
|
|
724
|
+
let cleanup;
|
|
725
|
+
if (popup.trigger === 'scroll') {
|
|
726
|
+
const threshold = Math.min(100, Math.max(0, popup.triggerValue)) / 100;
|
|
727
|
+
const onScroll = ()=>{
|
|
728
|
+
const height = document.documentElement.scrollHeight - window.innerHeight;
|
|
729
|
+
if (height <= 0 || window.scrollY / height >= threshold) {
|
|
730
|
+
setOpen(true);
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
window.addEventListener('scroll', onScroll, {
|
|
734
|
+
passive: true
|
|
735
|
+
});
|
|
736
|
+
cleanup = ()=>window.removeEventListener('scroll', onScroll);
|
|
737
|
+
} else if (popup.trigger === 'exit') {
|
|
738
|
+
const onLeave = (event)=>{
|
|
739
|
+
if (event.clientY <= 0) setOpen(true);
|
|
740
|
+
};
|
|
741
|
+
document.addEventListener('mouseout', onLeave);
|
|
742
|
+
cleanup = ()=>document.removeEventListener('mouseout', onLeave);
|
|
743
|
+
} else {
|
|
744
|
+
const timer = window.setTimeout(()=>setOpen(true), Math.max(0, popup.triggerValue) * 1000);
|
|
745
|
+
cleanup = ()=>window.clearTimeout(timer);
|
|
746
|
+
}
|
|
747
|
+
return cleanup;
|
|
748
|
+
}, [
|
|
749
|
+
popup,
|
|
750
|
+
storageKey
|
|
751
|
+
]);
|
|
752
|
+
const close = (kind = 'popupDismiss')=>{
|
|
753
|
+
try {
|
|
754
|
+
// Written to whichever store the cap READS (AGL-2174). Stamping
|
|
755
|
+
// `localStorage` for a session-capped popup would leave a timestamp
|
|
756
|
+
// nothing reads, outliving the cap it was meant to record.
|
|
757
|
+
if (MarketingModel.popupCapStore(popup) === 'session') {
|
|
758
|
+
window.sessionStorage.setItem(storageKey, '1');
|
|
759
|
+
} else {
|
|
760
|
+
window.localStorage.setItem(storageKey, String(Date.now()));
|
|
761
|
+
}
|
|
762
|
+
} catch (unused) {
|
|
763
|
+
// Private mode etc. — closing still works for this pageview.
|
|
764
|
+
}
|
|
765
|
+
sendOverlayBeacon(hostId, kind, popup.overlayId);
|
|
766
|
+
setOpen(false);
|
|
767
|
+
};
|
|
768
|
+
const handleEmailSubmit = async ()=>{
|
|
769
|
+
const value = email.trim();
|
|
770
|
+
if (!value || submitted) return;
|
|
771
|
+
try {
|
|
772
|
+
const response = await fetch('/api/forms/submit', {
|
|
773
|
+
method: 'POST',
|
|
774
|
+
headers: {
|
|
775
|
+
'Content-Type': 'application/json'
|
|
776
|
+
},
|
|
777
|
+
body: JSON.stringify(_extends({
|
|
778
|
+
hostId,
|
|
779
|
+
// The door this capture comes through (AGL-3029): the popup is a
|
|
780
|
+
// Marketing element, so a site that switched Forms off still
|
|
781
|
+
// collects the address it shows the popup for.
|
|
782
|
+
door: 'popup',
|
|
783
|
+
formName: 'Popup',
|
|
784
|
+
fields: {
|
|
785
|
+
email: value
|
|
786
|
+
},
|
|
787
|
+
path: window.location.pathname,
|
|
788
|
+
// Submitted AS-IS (AGL-1665) — the point of a honeypot is that the
|
|
789
|
+
// client never judges it. `libs/plugins/mui/.../form.tsx` reads its
|
|
790
|
+
// own hidden `website` input off the FormData and forwards it the
|
|
791
|
+
// same way; `/api/forms/submit` is the single place that decides.
|
|
792
|
+
website
|
|
793
|
+
}, campaignTouchField()))
|
|
794
|
+
});
|
|
795
|
+
// The popup's email capture is a lead, and it reported none: an
|
|
796
|
+
// announcement popup is often a site's highest-converting capture, and
|
|
797
|
+
// the merchant saw the impression and the dismiss (`aglyn_overlay`) with
|
|
798
|
+
// nothing in between. The generic form block has sent `generate_lead`
|
|
799
|
+
// since AGL-1561; this door was simply never wired to it.
|
|
800
|
+
//
|
|
801
|
+
// Gated on `response.ok`, unlike the visitor-facing thank-you below.
|
|
802
|
+
// The two disagree deliberately: a spam-refused or rate-limited capture
|
|
803
|
+
// is still shown the thank-you — telling a real visitor to try again
|
|
804
|
+
// after a honeypot verdict would leak the honeypot — but it is NOT a
|
|
805
|
+
// lead, and counting it would inflate the only number the merchant uses
|
|
806
|
+
// to judge whether the popup is worth interrupting people with.
|
|
807
|
+
//
|
|
808
|
+
// `form_name` is the block's own name, matching the `formName` on the
|
|
809
|
+
// wire; the address the visitor just typed never leaves this function.
|
|
810
|
+
if (response.ok) {
|
|
811
|
+
trackEvent('generate_lead', {
|
|
812
|
+
form_name: 'Popup',
|
|
813
|
+
form_location: window.location.pathname
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
} catch (unused) {
|
|
817
|
+
// Best-effort — still thank the visitor.
|
|
818
|
+
}
|
|
819
|
+
setSubmitted(true);
|
|
820
|
+
try {
|
|
821
|
+
// Far-future stamp: a successful capture never re-prompts.
|
|
822
|
+
window.localStorage.setItem(storageKey, String(Date.now() + 315360000000));
|
|
823
|
+
} catch (unused) {
|
|
824
|
+
// Ignore.
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
useEffect(()=>{
|
|
828
|
+
if (!open) return;
|
|
829
|
+
const onKey = (event)=>{
|
|
830
|
+
if (event.key === 'Escape') close();
|
|
831
|
+
};
|
|
832
|
+
document.addEventListener('keydown', onKey);
|
|
833
|
+
return ()=>document.removeEventListener('keydown', onKey);
|
|
834
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
835
|
+
}, [
|
|
836
|
+
open
|
|
837
|
+
]);
|
|
838
|
+
if (!open) return null;
|
|
839
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
840
|
+
onClick: ()=>close(),
|
|
841
|
+
style: {
|
|
842
|
+
position: 'fixed',
|
|
843
|
+
inset: 0,
|
|
844
|
+
zIndex: 2147483200,
|
|
845
|
+
display: 'flex',
|
|
846
|
+
alignItems: 'center',
|
|
847
|
+
justifyContent: 'center',
|
|
848
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
849
|
+
padding: 16
|
|
850
|
+
},
|
|
851
|
+
children: /*#__PURE__*/ _jsxs("div", {
|
|
852
|
+
role: "dialog",
|
|
853
|
+
"aria-modal": "true",
|
|
854
|
+
"aria-label": popup.headline || 'Announcement',
|
|
855
|
+
onClick: (event)=>event.stopPropagation(),
|
|
856
|
+
style: {
|
|
857
|
+
maxWidth: 420,
|
|
858
|
+
width: '100%',
|
|
859
|
+
background: '#fff',
|
|
860
|
+
color: '#111827',
|
|
861
|
+
borderRadius: 12,
|
|
862
|
+
overflow: 'hidden',
|
|
863
|
+
boxShadow: '0 24px 64px rgba(0, 0, 0, 0.35)',
|
|
864
|
+
fontFamily: 'system-ui, sans-serif',
|
|
865
|
+
position: 'relative'
|
|
866
|
+
},
|
|
867
|
+
children: [
|
|
868
|
+
/*#__PURE__*/ _jsx("button", {
|
|
869
|
+
type: "button",
|
|
870
|
+
"aria-label": "Close",
|
|
871
|
+
onClick: ()=>close(),
|
|
872
|
+
style: {
|
|
873
|
+
position: 'absolute',
|
|
874
|
+
top: 8,
|
|
875
|
+
right: 8,
|
|
876
|
+
border: 'none',
|
|
877
|
+
background: 'rgba(255, 255, 255, 0.85)',
|
|
878
|
+
borderRadius: '50%',
|
|
879
|
+
width: 28,
|
|
880
|
+
height: 28,
|
|
881
|
+
cursor: 'pointer',
|
|
882
|
+
fontSize: 14,
|
|
883
|
+
lineHeight: 1
|
|
884
|
+
},
|
|
885
|
+
children: '✕'
|
|
886
|
+
}),
|
|
887
|
+
popup.imageUrl && !Aglyn.isRefusedAuthorImageSrc(popup.imageUrl) ? // eslint-disable-next-line @next/next/no-img-element
|
|
888
|
+
/*#__PURE__*/ _jsx("img", {
|
|
889
|
+
src: popup.imageUrl,
|
|
890
|
+
// AGL-1896. This was a hardcoded `alt=""` with no field behind
|
|
891
|
+
// it. Still empty when the author left it empty — a decorative
|
|
892
|
+
// banner beside its own headline is the case `alt=""` exists for
|
|
893
|
+
// — but it is now their choice rather than ours, and picking the
|
|
894
|
+
// image from the library fills it in from the asset.
|
|
895
|
+
alt: (_popup_imageAlt = popup.imageAlt) != null ? _popup_imageAlt : '',
|
|
896
|
+
style: {
|
|
897
|
+
width: '100%',
|
|
898
|
+
maxHeight: 200,
|
|
899
|
+
objectFit: 'cover',
|
|
900
|
+
display: 'block'
|
|
901
|
+
}
|
|
902
|
+
}) : null,
|
|
903
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
904
|
+
style: {
|
|
905
|
+
padding: 24,
|
|
906
|
+
textAlign: 'center'
|
|
907
|
+
},
|
|
908
|
+
children: [
|
|
909
|
+
popup.headline ? /*#__PURE__*/ _jsx("h2", {
|
|
910
|
+
style: {
|
|
911
|
+
margin: '0 0 8px',
|
|
912
|
+
fontSize: 20
|
|
913
|
+
},
|
|
914
|
+
children: popup.headline
|
|
915
|
+
}) : null,
|
|
916
|
+
/*#__PURE__*/ _jsx("p", {
|
|
917
|
+
style: {
|
|
918
|
+
margin: 0,
|
|
919
|
+
fontSize: 14,
|
|
920
|
+
whiteSpace: 'pre-wrap'
|
|
921
|
+
},
|
|
922
|
+
children: popup.body
|
|
923
|
+
}),
|
|
924
|
+
popup.collectEmail ? /*#__PURE__*/ _jsx("div", {
|
|
925
|
+
style: {
|
|
926
|
+
marginTop: 16
|
|
927
|
+
},
|
|
928
|
+
children: submitted ? /*#__PURE__*/ _jsx("p", {
|
|
929
|
+
style: {
|
|
930
|
+
margin: 0,
|
|
931
|
+
fontSize: 14,
|
|
932
|
+
fontWeight: 600
|
|
933
|
+
},
|
|
934
|
+
children: 'Thanks — you are on the list!'
|
|
935
|
+
}) : /*#__PURE__*/ _jsxs("form", {
|
|
936
|
+
onSubmit: (event)=>{
|
|
937
|
+
event.preventDefault();
|
|
938
|
+
void handleEmailSubmit();
|
|
939
|
+
},
|
|
940
|
+
style: {
|
|
941
|
+
display: 'flex',
|
|
942
|
+
gap: 8,
|
|
943
|
+
justifyContent: 'center'
|
|
944
|
+
},
|
|
945
|
+
children: [
|
|
946
|
+
/*#__PURE__*/ _jsx("input", {
|
|
947
|
+
type: "email",
|
|
948
|
+
required: true,
|
|
949
|
+
placeholder: "you@example.com",
|
|
950
|
+
// Same nameless-input defect as the newsletter block
|
|
951
|
+
// (AGL-2392). A popup is modal-ish and arrives unbidden,
|
|
952
|
+
// so a screen-reader user meets this field with no
|
|
953
|
+
// surrounding context at all.
|
|
954
|
+
"aria-label": "Email address",
|
|
955
|
+
value: email,
|
|
956
|
+
onChange: (event)=>setEmail(event.target.value),
|
|
957
|
+
style: {
|
|
958
|
+
flex: 1,
|
|
959
|
+
maxWidth: 240,
|
|
960
|
+
padding: '8px 12px',
|
|
961
|
+
borderRadius: 8,
|
|
962
|
+
border: '1px solid #d1d5db',
|
|
963
|
+
fontSize: 14
|
|
964
|
+
}
|
|
965
|
+
}),
|
|
966
|
+
/*#__PURE__*/ _jsx("input", {
|
|
967
|
+
type: "text",
|
|
968
|
+
name: "website",
|
|
969
|
+
tabIndex: -1,
|
|
970
|
+
autoComplete: "off",
|
|
971
|
+
"aria-hidden": "true",
|
|
972
|
+
value: website,
|
|
973
|
+
onChange: (event)=>setWebsite(event.target.value),
|
|
974
|
+
style: {
|
|
975
|
+
position: 'absolute',
|
|
976
|
+
left: '-5000px',
|
|
977
|
+
height: 0,
|
|
978
|
+
width: 0
|
|
979
|
+
}
|
|
980
|
+
}),
|
|
981
|
+
/*#__PURE__*/ _jsx("button", {
|
|
982
|
+
type: "submit",
|
|
983
|
+
style: {
|
|
984
|
+
padding: '8px 16px',
|
|
985
|
+
borderRadius: 8,
|
|
986
|
+
border: 'none',
|
|
987
|
+
background: '#111827',
|
|
988
|
+
color: '#fff',
|
|
989
|
+
fontSize: 14,
|
|
990
|
+
cursor: 'pointer'
|
|
991
|
+
},
|
|
992
|
+
children: 'Sign up'
|
|
993
|
+
})
|
|
994
|
+
]
|
|
995
|
+
})
|
|
996
|
+
}) : null,
|
|
997
|
+
popup.ctaHref && popup.ctaLabel ? /*#__PURE__*/ _jsx("a", {
|
|
998
|
+
href: popup.ctaHref,
|
|
999
|
+
onClick: ()=>close('popupClick'),
|
|
1000
|
+
style: {
|
|
1001
|
+
display: 'inline-block',
|
|
1002
|
+
marginTop: 16,
|
|
1003
|
+
padding: '10px 24px',
|
|
1004
|
+
borderRadius: 8,
|
|
1005
|
+
background: '#111827',
|
|
1006
|
+
color: '#fff',
|
|
1007
|
+
textDecoration: 'none',
|
|
1008
|
+
fontSize: 14
|
|
1009
|
+
},
|
|
1010
|
+
children: popup.ctaLabel
|
|
1011
|
+
}) : null
|
|
1012
|
+
]
|
|
1013
|
+
})
|
|
1014
|
+
]
|
|
1015
|
+
})
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* The marketing plugin's site runtime (AGL-419), relocated verbatim from
|
|
1020
|
+
* the tenant catch-all: announcement bar + popup rendering (AGL-195/200),
|
|
1021
|
+
* the experiments runner (AGL-253), the site-event automations engine
|
|
1022
|
+
* (AGL-256/257), and automation-triggered overlays. Reads back exactly the
|
|
1023
|
+
* page-props slices its server enricher wrote (site-contract shapes).
|
|
1024
|
+
*/ export function MarketingSiteRuntime({ hostId, screens, page }) {
|
|
1025
|
+
var _page_experiments, _page_clientAutomations, _automationOverlay_popup_frequencyDays;
|
|
1026
|
+
var _page_data, _automationOverlay_bar, _automationOverlay_popup;
|
|
1027
|
+
const [automationOverlay, setAutomationOverlay] = useState(null);
|
|
1028
|
+
const announcementBar = page['announcementBar'];
|
|
1029
|
+
const popup = page['popup'];
|
|
1030
|
+
const experiments = (_page_experiments = page['experiments']) != null ? _page_experiments : [];
|
|
1031
|
+
const clientAutomations = (_page_clientAutomations = page['clientAutomations']) != null ? _page_clientAutomations : [];
|
|
1032
|
+
const automationOverlays = page['automationOverlays'];
|
|
1033
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
1034
|
+
children: [
|
|
1035
|
+
announcementBar ? /*#__PURE__*/ _jsx(AnnouncementBar, {
|
|
1036
|
+
bar: announcementBar,
|
|
1037
|
+
hostId: hostId
|
|
1038
|
+
}) : null,
|
|
1039
|
+
popup ? /*#__PURE__*/ _jsx(PopupOverlay, {
|
|
1040
|
+
popup: popup,
|
|
1041
|
+
hostId: hostId
|
|
1042
|
+
}) : null,
|
|
1043
|
+
experiments.length ? /*#__PURE__*/ _jsx(ExperimentsRunner, {
|
|
1044
|
+
hostId: hostId,
|
|
1045
|
+
experiments: experiments,
|
|
1046
|
+
consentHost: (_page_data = page['data']) == null ? void 0 : _page_data.host
|
|
1047
|
+
}) : null,
|
|
1048
|
+
clientAutomations.length ? /*#__PURE__*/ _jsx(AutomationsEngine, {
|
|
1049
|
+
hostId: hostId,
|
|
1050
|
+
automations: clientAutomations,
|
|
1051
|
+
screens: screens,
|
|
1052
|
+
onShowOverlay: (overlayId)=>{
|
|
1053
|
+
const overlay = automationOverlays == null ? void 0 : automationOverlays[overlayId];
|
|
1054
|
+
if (overlay) setAutomationOverlay(_extends({
|
|
1055
|
+
id: overlayId
|
|
1056
|
+
}, overlay));
|
|
1057
|
+
}
|
|
1058
|
+
}) : null,
|
|
1059
|
+
(automationOverlay == null ? void 0 : automationOverlay.kind) === 'bar' && ((_automationOverlay_bar = automationOverlay.bar) == null ? void 0 : _automationOverlay_bar.text) ? /*#__PURE__*/ _jsx(AnnouncementBar, {
|
|
1060
|
+
bar: _extends({}, automationOverlay.bar, {
|
|
1061
|
+
text: String(automationOverlay.bar.text),
|
|
1062
|
+
dismissible: automationOverlay.bar.dismissible !== false,
|
|
1063
|
+
contentHash: `automation-${automationOverlay.id}`
|
|
1064
|
+
}),
|
|
1065
|
+
hostId: hostId
|
|
1066
|
+
}) : null,
|
|
1067
|
+
(automationOverlay == null ? void 0 : automationOverlay.kind) === 'popup' && ((_automationOverlay_popup = automationOverlay.popup) == null ? void 0 : _automationOverlay_popup.body) ? /*#__PURE__*/ _jsx(PopupOverlay, {
|
|
1068
|
+
popup: _extends({}, automationOverlay.popup, {
|
|
1069
|
+
body: String(automationOverlay.popup.body),
|
|
1070
|
+
trigger: 'delay',
|
|
1071
|
+
triggerValue: 0,
|
|
1072
|
+
frequencyDays: Math.max(1, Number((_automationOverlay_popup_frequencyDays = automationOverlay.popup.frequencyDays) != null ? _automationOverlay_popup_frequencyDays : 1)),
|
|
1073
|
+
contentHash: `automation-${automationOverlay.id}`
|
|
1074
|
+
}),
|
|
1075
|
+
hostId: hostId
|
|
1076
|
+
}) : null
|
|
1077
|
+
]
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
//# sourceMappingURL=site-runtime.js.map
|