@aglyn/plugins-email 1.0.0-beta.196 → 1.0.0-beta.198

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.
Files changed (50) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/consent-group-access.d.ts +33 -0
  3. package/src/lib/components/consent-group-access.js +73 -0
  4. package/src/lib/components/consent-group-access.js.map +1 -0
  5. package/src/lib/components/consent-group-change-progress.d.ts +55 -0
  6. package/src/lib/components/consent-group-change-progress.js +282 -0
  7. package/src/lib/components/consent-group-change-progress.js.map +1 -0
  8. package/src/lib/components/consent-group-confirmation-card.d.ts +6 -16
  9. package/src/lib/components/consent-group-confirmation-card.js +7 -50
  10. package/src/lib/components/consent-group-confirmation-card.js.map +1 -1
  11. package/src/lib/components/consent-group-dialog.d.ts +53 -0
  12. package/src/lib/components/consent-group-dialog.js +495 -0
  13. package/src/lib/components/consent-group-dialog.js.map +1 -0
  14. package/src/lib/components/consent-group-editing.d.ts +127 -0
  15. package/src/lib/components/consent-group-editing.js +225 -0
  16. package/src/lib/components/consent-group-editing.js.map +1 -0
  17. package/src/lib/components/consent-group-review.d.ts +62 -0
  18. package/src/lib/components/consent-group-review.js +353 -0
  19. package/src/lib/components/consent-group-review.js.map +1 -0
  20. package/src/lib/components/consent-group-site-picker.d.ts +35 -0
  21. package/src/lib/components/consent-group-site-picker.js +190 -0
  22. package/src/lib/components/consent-group-site-picker.js.map +1 -0
  23. package/src/lib/components/consent-groups-api.d.ts +158 -0
  24. package/src/lib/components/consent-groups-api.js +250 -0
  25. package/src/lib/components/consent-groups-api.js.map +1 -0
  26. package/src/lib/components/consent-groups-card.d.ts +47 -0
  27. package/src/lib/components/consent-groups-card.js +351 -0
  28. package/src/lib/components/consent-groups-card.js.map +1 -0
  29. package/src/lib/components/email-screens-card.js +1 -1
  30. package/src/lib/components/email-screens-card.js.map +1 -1
  31. package/src/lib/components/emails-console-page.d.ts +1 -1
  32. package/src/lib/components/emails-console-page.js +62 -27
  33. package/src/lib/components/emails-console-page.js.map +1 -1
  34. package/src/lib/components/emails-console-sections.d.ts +1 -1
  35. package/src/lib/components/emails-console-sections.js +14 -1
  36. package/src/lib/components/emails-console-sections.js.map +1 -1
  37. package/src/lib/components/org-email-templates-card.js +1 -1
  38. package/src/lib/components/org-email-templates-card.js.map +1 -1
  39. package/src/lib/components/site-consent-group-card.d.ts +23 -0
  40. package/src/lib/components/site-consent-group-card.js +80 -0
  41. package/src/lib/components/site-consent-group-card.js.map +1 -0
  42. package/src/lib/components/suppressions-card.js +28 -7
  43. package/src/lib/components/suppressions-card.js.map +1 -1
  44. package/src/lib/server-console.js +4 -2
  45. package/src/lib/server-console.js.map +1 -1
  46. package/src/lib/server-list-gate.d.ts +9 -0
  47. package/src/lib/server-list-gate.js +23 -9
  48. package/src/lib/server-list-gate.js.map +1 -1
  49. package/src/lib/server-list-import.js +5 -2
  50. package/src/lib/server-list-import.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-email",
3
- "version": "1.0.0-beta.196",
3
+ "version": "1.0.0-beta.198",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,22 +25,22 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.196",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.196",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.196",
31
- "@aglyn/shared-data-types": "1.0.0-beta.196",
32
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.196",
33
- "@aglyn/shared-ui-jsx": "1.0.0-beta.196",
34
- "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.196",
35
- "@aglyn/shared-ui-next": "1.0.0-beta.196",
36
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.196",
37
- "@aglyn/shared-ui-theme": "1.0.0-beta.196",
38
- "@aglyn/shared-util-email": "1.0.0-beta.196",
39
- "@aglyn/shared-util-http": "1.0.0-beta.196",
40
- "@aglyn/shared-util-timestamp": "1.0.0-beta.196",
41
- "@aglyn/shared-util-tools": "1.0.0-beta.196",
42
- "@aglyn/tenant-data-admin": "1.0.0-beta.196",
43
- "@aglyn/tenant-feature-instance": "1.0.0-beta.196",
28
+ "@aglyn/aglyn": "1.0.0-beta.198",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.198",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.198",
31
+ "@aglyn/shared-data-types": "1.0.0-beta.198",
32
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.198",
33
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.198",
34
+ "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.198",
35
+ "@aglyn/shared-ui-next": "1.0.0-beta.198",
36
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.198",
37
+ "@aglyn/shared-ui-theme": "1.0.0-beta.198",
38
+ "@aglyn/shared-util-email": "1.0.0-beta.198",
39
+ "@aglyn/shared-util-http": "1.0.0-beta.198",
40
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.198",
41
+ "@aglyn/shared-util-tools": "1.0.0-beta.198",
42
+ "@aglyn/tenant-data-admin": "1.0.0-beta.198",
43
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.198",
44
44
  "@swc/helpers": "0.5.23"
45
45
  },
46
46
  "peerDependencies": {
@@ -0,0 +1,33 @@
1
+ import { type OrgPermission } from '@aglyn/aglyn';
2
+ /**
3
+ * What a member needs to change anything about the org's consent groups: the
4
+ * declaration itself (`/api/orgs/consent-groups`) or the switch over how its
5
+ * sites treat each other's confirmations (`/api/orgs/settings`). Both routes
6
+ * ask for the org-settings permission and for the permission that opens this
7
+ * console, so both cards ask the same question.
8
+ */
9
+ export declare const CONSENT_GROUP_REQUIRED_PERMISSIONS: readonly OrgPermission[];
10
+ /** Whether the signed-in member may change consent groups, once known. */
11
+ export interface ConsentGroupAccess {
12
+ /** The label of the first permission they lack, or `null` when they hold both. */
13
+ missing: string | null;
14
+ /** False until their membership (and custom role, if any) has been read. */
15
+ ready: boolean;
16
+ }
17
+ /**
18
+ * Which permission the signed-in member lacks to change consent groups — its
19
+ * label, or `null` when they hold both — and whether that is known yet.
20
+ *
21
+ * Read off the member's own membership document, and their custom role when
22
+ * they hold one, both of which the rules let a member read. The shell's
23
+ * permission map carries the legacy keys and the plugin-declared ones, not
24
+ * these two, and the CRM's org settings cards answer the same question the
25
+ * same way. `ready` separates "no" from "not yet", so a control disables with
26
+ * a reason instead of hiding until the read lands. A membership that cannot
27
+ * be read resolves no permission at all; the route decides either way.
28
+ *
29
+ * Handed no org, it opens no read and stays not ready — which is how a card
30
+ * handed the answer by its section avoids a second listen on the same
31
+ * membership.
32
+ */
33
+ export declare function useConsentGroupAccess(orgId: string | undefined): ConsentGroupAccess;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 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 { orgPermissionLabel, resolveOrgPermissions } from "@aglyn/aglyn";
18
+ import { useFirestore, useFirestoreDoc, useUser } from "@aglyn/tenant-feature-instance";
19
+ import { doc } from "firebase/firestore";
20
+ /**
21
+ * What a member needs to change anything about the org's consent groups: the
22
+ * declaration itself (`/api/orgs/consent-groups`) or the switch over how its
23
+ * sites treat each other's confirmations (`/api/orgs/settings`). Both routes
24
+ * ask for the org-settings permission and for the permission that opens this
25
+ * console, so both cards ask the same question.
26
+ */ export const CONSENT_GROUP_REQUIRED_PERMISSIONS = [
27
+ 'org.settings',
28
+ 'data.manage'
29
+ ];
30
+ /**
31
+ * Which permission the signed-in member lacks to change consent groups — its
32
+ * label, or `null` when they hold both — and whether that is known yet.
33
+ *
34
+ * Read off the member's own membership document, and their custom role when
35
+ * they hold one, both of which the rules let a member read. The shell's
36
+ * permission map carries the legacy keys and the plugin-declared ones, not
37
+ * these two, and the CRM's org settings cards answer the same question the
38
+ * same way. `ready` separates "no" from "not yet", so a control disables with
39
+ * a reason instead of hiding until the read lands. A membership that cannot
40
+ * be read resolves no permission at all; the route decides either way.
41
+ *
42
+ * Handed no org, it opens no read and stays not ready — which is how a card
43
+ * handed the answer by its section avoids a second listen on the same
44
+ * membership.
45
+ */ export function useConsentGroupAccess(orgId) {
46
+ var _ref, _roleRead_data;
47
+ const firestore = useFirestore();
48
+ const { data: user } = useUser();
49
+ const uid = (_ref = user == null ? void 0 : user.uid) != null ? _ref : '';
50
+ const memberRead = useFirestoreDoc(()=>orgId && uid ? doc(firestore, 'orgs', orgId, 'members', uid) : null, [
51
+ firestore,
52
+ orgId,
53
+ uid
54
+ ]);
55
+ const member = memberRead.status === 'success' ? memberRead.data : undefined;
56
+ const roleId = typeof (member == null ? void 0 : member.roleId) === 'string' ? member.roleId : '';
57
+ const roleRead = useFirestoreDoc(()=>orgId && roleId ? doc(firestore, 'orgs', orgId, 'roles', roleId) : null, [
58
+ firestore,
59
+ orgId,
60
+ roleId
61
+ ]);
62
+ // A reference that is never built stays `loading`, so a member with no
63
+ // custom role has nothing left to wait for.
64
+ const ready = Boolean(orgId && uid) && memberRead.status !== 'loading' && (!roleId || roleRead.status !== 'loading');
65
+ const granted = resolveOrgPermissions(member != null ? member : null, roleId && roleRead.status === 'success' ? (_roleRead_data = roleRead.data) != null ? _roleRead_data : null : null);
66
+ const lacking = CONSENT_GROUP_REQUIRED_PERMISSIONS.find((key)=>granted[key] !== true);
67
+ return {
68
+ missing: lacking ? orgPermissionLabel(lacking) : null,
69
+ ready
70
+ };
71
+ }
72
+
73
+ //# sourceMappingURL=consent-group-access.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/consent-group-access.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type AglynOrgCustomRole,\n type OrgPermission,\n orgPermissionLabel,\n resolveOrgPermissions,\n} from '@aglyn/aglyn'\nimport {\n useFirestore,\n useFirestoreDoc,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport { doc } from 'firebase/firestore'\n\n/**\n * What a member needs to change anything about the org's consent groups: the\n * declaration itself (`/api/orgs/consent-groups`) or the switch over how its\n * sites treat each other's confirmations (`/api/orgs/settings`). Both routes\n * ask for the org-settings permission and for the permission that opens this\n * console, so both cards ask the same question.\n */\nexport const CONSENT_GROUP_REQUIRED_PERMISSIONS: readonly OrgPermission[] = [\n 'org.settings',\n 'data.manage',\n]\n\n/** A membership document, as the permission resolver reads one. */\ntype MemberRecord = NonNullable<Parameters<typeof resolveOrgPermissions>[0]>\n\n/** Whether the signed-in member may change consent groups, once known. */\nexport interface ConsentGroupAccess {\n /** The label of the first permission they lack, or `null` when they hold both. */\n missing: string | null\n /** False until their membership (and custom role, if any) has been read. */\n ready: boolean\n}\n\n/**\n * Which permission the signed-in member lacks to change consent groups — its\n * label, or `null` when they hold both — and whether that is known yet.\n *\n * Read off the member's own membership document, and their custom role when\n * they hold one, both of which the rules let a member read. The shell's\n * permission map carries the legacy keys and the plugin-declared ones, not\n * these two, and the CRM's org settings cards answer the same question the\n * same way. `ready` separates \"no\" from \"not yet\", so a control disables with\n * a reason instead of hiding until the read lands. A membership that cannot\n * be read resolves no permission at all; the route decides either way.\n *\n * Handed no org, it opens no read and stays not ready — which is how a card\n * handed the answer by its section avoids a second listen on the same\n * membership.\n */\nexport function useConsentGroupAccess(\n orgId: string | undefined,\n): ConsentGroupAccess {\n const firestore = useFirestore()\n const { data: user } = useUser()\n const uid = user?.uid ?? ''\n const memberRead = useFirestoreDoc<MemberRecord>(\n () => (orgId && uid ? doc(firestore, 'orgs', orgId, 'members', uid) : null),\n [firestore, orgId, uid],\n )\n const member = memberRead.status === 'success' ? memberRead.data : undefined\n const roleId = typeof member?.roleId === 'string' ? member.roleId : ''\n const roleRead = useFirestoreDoc<AglynOrgCustomRole>(\n () => (orgId && roleId ? doc(firestore, 'orgs', orgId, 'roles', roleId) : null),\n [firestore, orgId, roleId],\n )\n // A reference that is never built stays `loading`, so a member with no\n // custom role has nothing left to wait for.\n const ready =\n Boolean(orgId && uid) &&\n memberRead.status !== 'loading' &&\n (!roleId || roleRead.status !== 'loading')\n const granted = resolveOrgPermissions(\n member ?? null,\n roleId && roleRead.status === 'success' ? (roleRead.data ?? null) : null,\n )\n const lacking = CONSENT_GROUP_REQUIRED_PERMISSIONS.find(\n (key) => granted[key] !== true,\n )\n return { missing: lacking ? orgPermissionLabel(lacking) : null, ready }\n}\n"],"names":["orgPermissionLabel","resolveOrgPermissions","useFirestore","useFirestoreDoc","useUser","doc","CONSENT_GROUP_REQUIRED_PERMISSIONS","useConsentGroupAccess","orgId","roleRead","firestore","data","user","uid","memberRead","member","status","undefined","roleId","ready","Boolean","granted","lacking","find","key","missing"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SAGEA,kBAAkB,EAClBC,qBAAqB,QAChB,eAAc;AACrB,SACEC,YAAY,EACZC,eAAe,EACfC,OAAO,QACF,iCAAgC;AACvC,SAASC,GAAG,QAAQ,qBAAoB;AAExC;;;;;;CAMC,GACD,OAAO,MAAMC,qCAA+D;IAC1E;IACA;CACD,CAAA;AAaD;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASC,sBACdC,KAAyB;cAuBoBC;IArB7C,MAAMC,YAAYR;IAClB,MAAM,EAAES,MAAMC,IAAI,EAAE,GAAGR;IACvB,MAAMS,cAAMD,wBAAAA,KAAMC,GAAG,mBAAI;IACzB,MAAMC,aAAaX,gBACjB,IAAOK,SAASK,MAAMR,IAAIK,WAAW,QAAQF,OAAO,WAAWK,OAAO,MACtE;QAACH;QAAWF;QAAOK;KAAI;IAEzB,MAAME,SAASD,WAAWE,MAAM,KAAK,YAAYF,WAAWH,IAAI,GAAGM;IACnE,MAAMC,SAAS,QAAOH,0BAAAA,OAAQG,MAAM,MAAK,WAAWH,OAAOG,MAAM,GAAG;IACpE,MAAMT,WAAWN,gBACf,IAAOK,SAASU,SAASb,IAAIK,WAAW,QAAQF,OAAO,SAASU,UAAU,MAC1E;QAACR;QAAWF;QAAOU;KAAO;IAE5B,uEAAuE;IACvE,4CAA4C;IAC5C,MAAMC,QACJC,QAAQZ,SAASK,QACjBC,WAAWE,MAAM,KAAK,aACrB,CAAA,CAACE,UAAUT,SAASO,MAAM,KAAK,SAAQ;IAC1C,MAAMK,UAAUpB,sBACdc,iBAAAA,SAAU,MACVG,UAAUT,SAASO,MAAM,KAAK,aAAaP,iBAAAA,SAASE,IAAI,YAAbF,iBAAiB,OAAQ;IAEtE,MAAMa,UAAUhB,mCAAmCiB,IAAI,CACrD,CAACC,MAAQH,OAAO,CAACG,IAAI,KAAK;IAE5B,OAAO;QAAEC,SAASH,UAAUtB,mBAAmBsB,WAAW;QAAMH;IAAM;AACxE"}
@@ -0,0 +1,55 @@
1
+ import { type ConsentGroupChangeMarker, type ConsentGroupChangePhase } from './consent-groups-api';
2
+ /**
3
+ * The least time between two `continue` calls from one page. A call that
4
+ * returns at once — the job is waiting on its sweep, or another runner holds
5
+ * it — must not become a loop that asks again the instant it is answered.
6
+ */
7
+ export declare const CONSENT_GROUP_CONTINUE_GAP_MS = 3000;
8
+ /** What each phase is called while it runs, and what it means for the reader. */
9
+ export declare const CONSENT_GROUP_PHASE_TEXT: Readonly<Record<ConsentGroupChangePhase, {
10
+ title: string;
11
+ body: string;
12
+ }>>;
13
+ export interface ConsentGroupChangeProgressProps {
14
+ orgId: string;
15
+ changeId: string;
16
+ /** The org's marker for this change, when the org document carries it yet. */
17
+ marker: ConsentGroupChangeMarker | null;
18
+ /**
19
+ * The reader may drive and stop the change. Anyone else sees where it is,
20
+ * and opens no read of the job for it.
21
+ */
22
+ canDrive: boolean;
23
+ /** The change finished, or was stopped before it took effect. */
24
+ onFinished: (outcome: 'done' | 'canceled') => void;
25
+ }
26
+ /**
27
+ * A consent group change while it runs (AGL-3320).
28
+ *
29
+ * ## It finishes on its own; an open page only makes it faster
30
+ *
31
+ * The route works in slices of under a minute and a cron resumes it every
32
+ * quarter hour, so closing the page never strands a change. While this is
33
+ * mounted for somebody who may drive it, it asks for the next slice as soon
34
+ * as nobody else holds the job — the job document's lease says when — so a
35
+ * change the admin is watching finishes in minutes rather than quarter hours.
36
+ *
37
+ * ## Only the first phase can be stopped
38
+ *
39
+ * Before the flip nothing an admin would notice has changed: refusals were
40
+ * copied, which never lets anybody be mailed who could not be before, and
41
+ * the old declaration still holds. So **Stop before it takes effect** is
42
+ * offered exactly then. After the flip the new declaration is what every
43
+ * send reads, and stopping would leave the CRM's records half moved.
44
+ *
45
+ * ## A stall is shown, not hidden
46
+ *
47
+ * After five failures in a row the job stops retrying itself on every call
48
+ * and records why. The banner shows that reason and **Retry now**; the cron
49
+ * keeps trying either way.
50
+ */
51
+ export declare function ConsentGroupChangeProgress(props: ConsentGroupChangeProgressProps): import("react").JSX.Element;
52
+ export declare namespace ConsentGroupChangeProgress {
53
+ var displayName: string;
54
+ }
55
+ export default ConsentGroupChangeProgress;
@@ -0,0 +1,282 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
+ import { useConfirmationContext } from "@aglyn/shared-ui-jsx";
19
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
20
+ import { useFirestore, useFirestoreDoc, useUser } from "@aglyn/tenant-feature-instance";
21
+ import { Alert, AlertTitle, Button, LinearProgress, Stack, Typography } from "@mui/material";
22
+ import { doc } from "firebase/firestore";
23
+ import { useCallback, useEffect, useRef, useState } from "react";
24
+ import { CONSENT_GROUP_CHANGES_COLLECTION, CONSENT_GROUP_SWEEP_DELAY_MS, postConsentGroups, readConsentGroupChangeJob } from "./consent-groups-api.js";
25
+ /**
26
+ * The least time between two `continue` calls from one page. A call that
27
+ * returns at once — the job is waiting on its sweep, or another runner holds
28
+ * it — must not become a loop that asks again the instant it is answered.
29
+ */ export const CONSENT_GROUP_CONTINUE_GAP_MS = 3000;
30
+ /** What each phase is called while it runs, and what it means for the reader. */ export const CONSENT_GROUP_PHASE_TEXT = {
31
+ carry: {
32
+ title: 'Copying opt-outs — nothing has changed yet',
33
+ body: 'Opt-outs are being copied onto every site that will stop sharing them, ' + 'so nobody who opted out can be emailed after the change. Until this ' + 'finishes, your consent groups work exactly as they did.'
34
+ },
35
+ rehome: {
36
+ title: 'In effect — combining CRM records',
37
+ body: 'Every send already follows your new consent groups. The CRM records ' + 'these sites keep are being combined or copied, so some may show under ' + 'their old group for a little while.'
38
+ },
39
+ sweep: {
40
+ title: 'Finishing up',
41
+ body: 'A last pass for anything written while the change ran. You can make ' + 'another change once this finishes.'
42
+ }
43
+ };
44
+ /**
45
+ * A consent group change while it runs (AGL-3320).
46
+ *
47
+ * ## It finishes on its own; an open page only makes it faster
48
+ *
49
+ * The route works in slices of under a minute and a cron resumes it every
50
+ * quarter hour, so closing the page never strands a change. While this is
51
+ * mounted for somebody who may drive it, it asks for the next slice as soon
52
+ * as nobody else holds the job — the job document's lease says when — so a
53
+ * change the admin is watching finishes in minutes rather than quarter hours.
54
+ *
55
+ * ## Only the first phase can be stopped
56
+ *
57
+ * Before the flip nothing an admin would notice has changed: refusals were
58
+ * copied, which never lets anybody be mailed who could not be before, and
59
+ * the old declaration still holds. So **Stop before it takes effect** is
60
+ * offered exactly then. After the flip the new declaration is what every
61
+ * send reads, and stopping would leave the CRM's records half moved.
62
+ *
63
+ * ## A stall is shown, not hidden
64
+ *
65
+ * After five failures in a row the job stops retrying itself on every call
66
+ * and records why. The banner shows that reason and **Retry now**; the cron
67
+ * keeps trying either way.
68
+ */ export function ConsentGroupChangeProgress(props) {
69
+ var _ref, _ref1, _ref2, _job_lastError;
70
+ var _answer_progress, _answer_progress1;
71
+ const { orgId, changeId, marker, canDrive, onFinished } = props;
72
+ const firestore = useFirestore();
73
+ const { data: user } = useUser();
74
+ const { enqueueSnackbar } = useSnackbar();
75
+ const { confirm } = useConfirmationContext();
76
+ const jobRead = useFirestoreDoc(()=>canDrive && orgId && changeId ? doc(firestore, 'orgs', orgId, CONSENT_GROUP_CHANGES_COLLECTION, changeId) : null, [
77
+ firestore,
78
+ orgId,
79
+ changeId,
80
+ canDrive
81
+ ]);
82
+ const job = readConsentGroupChangeJob(jobRead.status === 'success' ? jobRead.data : undefined);
83
+ const [answer, setAnswer] = useState(null);
84
+ const [busy, setBusy] = useState(false);
85
+ const [stopping, setStopping] = useState(false);
86
+ /** A refusal that stops this page driving until somebody retries. */ const [halted, setHalted] = useState(null);
87
+ /** Re-arms the driver after each answer. */ const [tick, setTick] = useState(0);
88
+ const lastCallAt = useRef(0);
89
+ const finished = useRef(false);
90
+ const mounted = useRef(true);
91
+ useEffect(()=>{
92
+ mounted.current = true;
93
+ return ()=>{
94
+ mounted.current = false;
95
+ };
96
+ }, []);
97
+ const markerPhase = marker && marker.changeId === changeId ? marker.phase : null;
98
+ const answerPhase = answer && [
99
+ 'carry',
100
+ 'rehome',
101
+ 'sweep'
102
+ ].includes(String(answer.phase)) ? answer.phase : null;
103
+ const jobPhase = [
104
+ 'carry',
105
+ 'rehome',
106
+ 'sweep'
107
+ ].includes(String(job.phase)) ? job.phase : null;
108
+ const phase = (_ref = (_ref1 = markerPhase != null ? markerPhase : answerPhase) != null ? _ref1 : jobPhase) != null ? _ref : 'carry';
109
+ const stalled = job.stalled || (answer == null ? void 0 : (_answer_progress = answer.progress) == null ? void 0 : _answer_progress.stalled) === true;
110
+ const lastError = (_ref2 = (_job_lastError = job.lastError) != null ? _job_lastError : answer == null ? void 0 : (_answer_progress1 = answer.progress) == null ? void 0 : _answer_progress1.lastError) != null ? _ref2 : null;
111
+ const finish = useCallback((outcome)=>{
112
+ if (finished.current) return;
113
+ finished.current = true;
114
+ onFinished(outcome);
115
+ }, [
116
+ onFinished
117
+ ]);
118
+ // The job can finish without this page: the cron, or another tab.
119
+ useEffect(()=>{
120
+ if (job.finished) finish(job.phase === 'canceled' ? 'canceled' : 'done');
121
+ }, [
122
+ job.finished,
123
+ job.phase,
124
+ finish
125
+ ]);
126
+ const drive = useCallback(async ()=>{
127
+ if (!user || finished.current) return;
128
+ lastCallAt.current = Date.now();
129
+ setBusy(true);
130
+ const result = await postConsentGroups(user, {
131
+ orgId,
132
+ action: 'continue',
133
+ changeId
134
+ });
135
+ if (!mounted.current) return;
136
+ setBusy(false);
137
+ if (result.ok) {
138
+ setAnswer(result.body);
139
+ if (result.body.done === true) return void finish('done');
140
+ setTick((value)=>value + 1);
141
+ return;
142
+ }
143
+ setHalted(result.failure.message);
144
+ }, [
145
+ user,
146
+ orgId,
147
+ changeId,
148
+ finish
149
+ ]);
150
+ /*
151
+ * The driver. It waits out the least gap between calls, any lease another
152
+ * runner holds, and the delay before the sweep may run — then asks once.
153
+ * Each answer re-arms it through `tick`; a lease renewed under it re-arms
154
+ * it through the job document.
155
+ */ useEffect(()=>{
156
+ if (!canDrive || !user || finished.current) return;
157
+ if (busy || stopping || stalled || halted) return;
158
+ if (jobRead.status === 'loading') return;
159
+ const now = Date.now();
160
+ let at = Math.max(now, lastCallAt.current + CONSENT_GROUP_CONTINUE_GAP_MS);
161
+ if (job.leaseUntilMs != null && job.leaseUntilMs > now) {
162
+ at = Math.max(at, job.leaseUntilMs + 1000);
163
+ }
164
+ if (phase === 'sweep' && (marker == null ? void 0 : marker.declaredAtMs)) {
165
+ at = Math.max(at, marker.declaredAtMs + CONSENT_GROUP_SWEEP_DELAY_MS);
166
+ }
167
+ const timer = setTimeout(()=>void drive(), at - now);
168
+ return ()=>clearTimeout(timer);
169
+ }, [
170
+ canDrive,
171
+ user,
172
+ busy,
173
+ stopping,
174
+ stalled,
175
+ halted,
176
+ jobRead.status,
177
+ job.leaseUntilMs,
178
+ phase,
179
+ marker == null ? void 0 : marker.declaredAtMs,
180
+ drive,
181
+ tick
182
+ ]);
183
+ const retry = ()=>{
184
+ setHalted(null);
185
+ lastCallAt.current = 0;
186
+ void drive();
187
+ };
188
+ const stop = async ()=>{
189
+ if (!user || stopping) return;
190
+ const accepted = await confirm({
191
+ title: 'Stop this change?',
192
+ description: 'Nothing has taken effect yet, so your consent groups stay exactly as ' + 'they are. Opt-outs already copied stay in place — a copied opt-out ' + 'never lets anyone be emailed who couldn’t be before.',
193
+ confirmationText: 'Stop the change',
194
+ cancellationText: 'Keep going'
195
+ })// `confirm` resolves with no value and REJECTS on cancel (AGL-950).
196
+ .then(()=>true).catch(()=>false);
197
+ if (!accepted) return;
198
+ setStopping(true);
199
+ const result = await postConsentGroups(user, {
200
+ orgId,
201
+ action: 'cancel',
202
+ changeId
203
+ });
204
+ if (!mounted.current) return;
205
+ setStopping(false);
206
+ if (result.ok) {
207
+ enqueueSnackbar('Stopped. Your consent groups are unchanged.', {
208
+ variant: 'success',
209
+ persist: false
210
+ });
211
+ return void finish('canceled');
212
+ }
213
+ enqueueSnackbar(result.failure.message, {
214
+ variant: result.failure.kind === 'took-effect' ? 'info' : 'error',
215
+ allowDuplicate: true
216
+ });
217
+ };
218
+ const text = CONSENT_GROUP_PHASE_TEXT[phase];
219
+ return /*#__PURE__*/ _jsxs(Alert, {
220
+ severity: stalled || halted ? 'error' : 'info',
221
+ "data-testid": "consent-group-change-progress",
222
+ sx: {
223
+ '& .MuiAlert-message': {
224
+ width: '100%'
225
+ }
226
+ },
227
+ children: [
228
+ /*#__PURE__*/ _jsx(AlertTitle, {
229
+ children: text.title
230
+ }),
231
+ /*#__PURE__*/ _jsxs(Stack, {
232
+ spacing: 1,
233
+ children: [
234
+ /*#__PURE__*/ _jsx(Typography, {
235
+ variant: "body2",
236
+ children: text.body
237
+ }),
238
+ stalled ? /*#__PURE__*/ _jsx(Typography, {
239
+ variant: "body2",
240
+ children: `This change stopped after repeated errors${lastError ? `: ${lastError}` : '.'} It keeps trying on its own every 15 minutes, or you can retry now.`
241
+ }) : null,
242
+ halted ? /*#__PURE__*/ _jsx(Typography, {
243
+ variant: "body2",
244
+ children: halted
245
+ }) : null,
246
+ !canDrive ? /*#__PURE__*/ _jsx(Typography, {
247
+ variant: "body2",
248
+ color: "text.secondary",
249
+ children: 'It finishes on its own. Only members who can change consent groups can stop it.'
250
+ }) : null,
251
+ !stalled && !halted ? /*#__PURE__*/ _jsx(LinearProgress, {
252
+ "aria-label": "Consent group change in progress"
253
+ }) : null,
254
+ canDrive && (stalled || halted || phase === 'carry') ? /*#__PURE__*/ _jsxs(Stack, {
255
+ direction: "row",
256
+ spacing: 1,
257
+ children: [
258
+ stalled || halted ? /*#__PURE__*/ _jsx(Button, {
259
+ size: "small",
260
+ variant: "contained",
261
+ onClick: retry,
262
+ disabled: busy,
263
+ children: 'Retry now'
264
+ }) : null,
265
+ phase === 'carry' ? /*#__PURE__*/ _jsx(Button, {
266
+ size: "small",
267
+ color: "inherit",
268
+ onClick: ()=>void stop(),
269
+ disabled: stopping,
270
+ children: 'Stop before it takes effect'
271
+ }) : null
272
+ ]
273
+ }) : null
274
+ ]
275
+ })
276
+ ]
277
+ });
278
+ }
279
+ ConsentGroupChangeProgress.displayName = 'ConsentGroupChangeProgress';
280
+ export default ConsentGroupChangeProgress;
281
+
282
+ //# sourceMappingURL=consent-group-change-progress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/consent-group-change-progress.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreDoc,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n AlertTitle,\n Button,\n LinearProgress,\n Stack,\n Typography,\n} from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport {\n CONSENT_GROUP_CHANGES_COLLECTION,\n CONSENT_GROUP_SWEEP_DELAY_MS,\n postConsentGroups,\n readConsentGroupChangeJob,\n type ConsentGroupChangeMarker,\n type ConsentGroupChangePhase,\n type ConsentGroupsProgressResponse,\n} from './consent-groups-api'\n\n/**\n * The least time between two `continue` calls from one page. A call that\n * returns at once — the job is waiting on its sweep, or another runner holds\n * it — must not become a loop that asks again the instant it is answered.\n */\nexport const CONSENT_GROUP_CONTINUE_GAP_MS = 3_000\n\n/** What each phase is called while it runs, and what it means for the reader. */\nexport const CONSENT_GROUP_PHASE_TEXT: Readonly<\n Record<ConsentGroupChangePhase, { title: string; body: string }>\n> = {\n carry: {\n title: 'Copying opt-outs — nothing has changed yet',\n body:\n 'Opt-outs are being copied onto every site that will stop sharing them, ' +\n 'so nobody who opted out can be emailed after the change. Until this ' +\n 'finishes, your consent groups work exactly as they did.',\n },\n rehome: {\n title: 'In effect — combining CRM records',\n body:\n 'Every send already follows your new consent groups. The CRM records ' +\n 'these sites keep are being combined or copied, so some may show under ' +\n 'their old group for a little while.',\n },\n sweep: {\n title: 'Finishing up',\n body:\n 'A last pass for anything written while the change ran. You can make ' +\n 'another change once this finishes.',\n },\n}\n\nexport interface ConsentGroupChangeProgressProps {\n orgId: string\n changeId: string\n /** The org's marker for this change, when the org document carries it yet. */\n marker: ConsentGroupChangeMarker | null\n /**\n * The reader may drive and stop the change. Anyone else sees where it is,\n * and opens no read of the job for it.\n */\n canDrive: boolean\n /** The change finished, or was stopped before it took effect. */\n onFinished: (outcome: 'done' | 'canceled') => void\n}\n\n/**\n * A consent group change while it runs (AGL-3320).\n *\n * ## It finishes on its own; an open page only makes it faster\n *\n * The route works in slices of under a minute and a cron resumes it every\n * quarter hour, so closing the page never strands a change. While this is\n * mounted for somebody who may drive it, it asks for the next slice as soon\n * as nobody else holds the job — the job document's lease says when — so a\n * change the admin is watching finishes in minutes rather than quarter hours.\n *\n * ## Only the first phase can be stopped\n *\n * Before the flip nothing an admin would notice has changed: refusals were\n * copied, which never lets anybody be mailed who could not be before, and\n * the old declaration still holds. So **Stop before it takes effect** is\n * offered exactly then. After the flip the new declaration is what every\n * send reads, and stopping would leave the CRM's records half moved.\n *\n * ## A stall is shown, not hidden\n *\n * After five failures in a row the job stops retrying itself on every call\n * and records why. The banner shows that reason and **Retry now**; the cron\n * keeps trying either way.\n */\nexport function ConsentGroupChangeProgress(props: ConsentGroupChangeProgressProps) {\n const { orgId, changeId, marker, canDrive, onFinished } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n\n const jobRead = useFirestoreDoc<Record<string, unknown>>(\n () =>\n canDrive && orgId && changeId\n ? doc(firestore, 'orgs', orgId, CONSENT_GROUP_CHANGES_COLLECTION, changeId)\n : null,\n [firestore, orgId, changeId, canDrive],\n )\n const job = readConsentGroupChangeJob(\n jobRead.status === 'success' ? jobRead.data : undefined,\n )\n\n const [answer, setAnswer] = useState<ConsentGroupsProgressResponse | null>(null)\n const [busy, setBusy] = useState(false)\n const [stopping, setStopping] = useState(false)\n /** A refusal that stops this page driving until somebody retries. */\n const [halted, setHalted] = useState<string | null>(null)\n /** Re-arms the driver after each answer. */\n const [tick, setTick] = useState(0)\n const lastCallAt = useRef(0)\n const finished = useRef(false)\n const mounted = useRef(true)\n useEffect(() => {\n mounted.current = true\n return () => {\n mounted.current = false\n }\n }, [])\n\n const markerPhase =\n marker && marker.changeId === changeId ? marker.phase : null\n const answerPhase =\n answer && ['carry', 'rehome', 'sweep'].includes(String(answer.phase))\n ? (answer.phase as ConsentGroupChangePhase)\n : null\n const jobPhase = ['carry', 'rehome', 'sweep'].includes(String(job.phase))\n ? (job.phase as ConsentGroupChangePhase)\n : null\n const phase: ConsentGroupChangePhase =\n markerPhase ?? answerPhase ?? jobPhase ?? 'carry'\n const stalled = job.stalled || answer?.progress?.stalled === true\n const lastError = job.lastError ?? answer?.progress?.lastError ?? null\n\n const finish = useCallback(\n (outcome: 'done' | 'canceled') => {\n if (finished.current) return\n finished.current = true\n onFinished(outcome)\n },\n [onFinished],\n )\n\n // The job can finish without this page: the cron, or another tab.\n useEffect(() => {\n if (job.finished) finish(job.phase === 'canceled' ? 'canceled' : 'done')\n }, [job.finished, job.phase, finish])\n\n const drive = useCallback(async () => {\n if (!user || finished.current) return\n lastCallAt.current = Date.now()\n setBusy(true)\n const result = await postConsentGroups<ConsentGroupsProgressResponse>(user, {\n orgId,\n action: 'continue',\n changeId,\n })\n if (!mounted.current) return\n setBusy(false)\n if (result.ok) {\n setAnswer(result.body)\n if (result.body.done === true) return void finish('done')\n setTick((value) => value + 1)\n return\n }\n setHalted(result.failure.message)\n }, [user, orgId, changeId, finish])\n\n /*\n * The driver. It waits out the least gap between calls, any lease another\n * runner holds, and the delay before the sweep may run — then asks once.\n * Each answer re-arms it through `tick`; a lease renewed under it re-arms\n * it through the job document.\n */\n useEffect(() => {\n if (!canDrive || !user || finished.current) return\n if (busy || stopping || stalled || halted) return\n if (jobRead.status === 'loading') return\n const now = Date.now()\n let at = Math.max(now, lastCallAt.current + CONSENT_GROUP_CONTINUE_GAP_MS)\n if (job.leaseUntilMs != null && job.leaseUntilMs > now) {\n at = Math.max(at, job.leaseUntilMs + 1_000)\n }\n if (phase === 'sweep' && marker?.declaredAtMs) {\n at = Math.max(at, marker.declaredAtMs + CONSENT_GROUP_SWEEP_DELAY_MS)\n }\n const timer = setTimeout(() => void drive(), at - now)\n return () => clearTimeout(timer)\n }, [\n canDrive,\n user,\n busy,\n stopping,\n stalled,\n halted,\n jobRead.status,\n job.leaseUntilMs,\n phase,\n marker?.declaredAtMs,\n drive,\n tick,\n ])\n\n const retry = () => {\n setHalted(null)\n lastCallAt.current = 0\n void drive()\n }\n\n const stop = async () => {\n if (!user || stopping) return\n const accepted = await confirm({\n title: 'Stop this change?',\n description:\n 'Nothing has taken effect yet, so your consent groups stay exactly as ' +\n 'they are. Opt-outs already copied stay in place — a copied opt-out ' +\n 'never lets anyone be emailed who couldn’t be before.',\n confirmationText: 'Stop the change',\n cancellationText: 'Keep going',\n })\n // `confirm` resolves with no value and REJECTS on cancel (AGL-950).\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n setStopping(true)\n const result = await postConsentGroups(user, {\n orgId,\n action: 'cancel',\n changeId,\n })\n if (!mounted.current) return\n setStopping(false)\n if (result.ok) {\n enqueueSnackbar('Stopped. Your consent groups are unchanged.', {\n variant: 'success',\n persist: false,\n })\n return void finish('canceled')\n }\n enqueueSnackbar(result.failure.message, {\n variant: result.failure.kind === 'took-effect' ? 'info' : 'error',\n allowDuplicate: true,\n })\n }\n\n const text = CONSENT_GROUP_PHASE_TEXT[phase]\n return (\n <Alert\n severity={stalled || halted ? 'error' : 'info'}\n data-testid=\"consent-group-change-progress\"\n sx={{ '& .MuiAlert-message': { width: '100%' } }}\n >\n <AlertTitle>{text.title}</AlertTitle>\n <Stack spacing={1}>\n <Typography variant=\"body2\">{text.body}</Typography>\n {stalled ? (\n <Typography variant=\"body2\">\n {`This change stopped after repeated errors${\n lastError ? `: ${lastError}` : '.'\n } It keeps trying on its own every 15 minutes, or you can retry now.`}\n </Typography>\n ) : null}\n {halted ? <Typography variant=\"body2\">{halted}</Typography> : null}\n {!canDrive ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'It finishes on its own. Only members who can change consent groups can stop it.'}\n </Typography>\n ) : null}\n {!stalled && !halted ? (\n <LinearProgress aria-label=\"Consent group change in progress\" />\n ) : null}\n {canDrive && (stalled || halted || phase === 'carry') ? (\n <Stack direction=\"row\" spacing={1}>\n {stalled || halted ? (\n <Button size=\"small\" variant=\"contained\" onClick={retry} disabled={busy}>\n {'Retry now'}\n </Button>\n ) : null}\n {phase === 'carry' ? (\n <Button\n size=\"small\"\n color=\"inherit\"\n onClick={() => void stop()}\n disabled={stopping}\n >\n {'Stop before it takes effect'}\n </Button>\n ) : null}\n </Stack>\n ) : null}\n </Stack>\n </Alert>\n )\n}\nConsentGroupChangeProgress.displayName = 'ConsentGroupChangeProgress'\n\nexport default ConsentGroupChangeProgress\n"],"names":["useConfirmationContext","useSnackbar","useFirestore","useFirestoreDoc","useUser","Alert","AlertTitle","Button","LinearProgress","Stack","Typography","doc","useCallback","useEffect","useRef","useState","CONSENT_GROUP_CHANGES_COLLECTION","CONSENT_GROUP_SWEEP_DELAY_MS","postConsentGroups","readConsentGroupChangeJob","CONSENT_GROUP_CONTINUE_GAP_MS","CONSENT_GROUP_PHASE_TEXT","carry","title","body","rehome","sweep","ConsentGroupChangeProgress","props","markerPhase","job","answer","orgId","changeId","marker","canDrive","onFinished","firestore","data","user","enqueueSnackbar","confirm","jobRead","status","undefined","setAnswer","busy","setBusy","stopping","setStopping","halted","setHalted","tick","setTick","lastCallAt","finished","mounted","current","phase","answerPhase","includes","String","jobPhase","stalled","progress","lastError","finish","outcome","drive","Date","now","result","action","ok","done","value","failure","message","at","Math","max","leaseUntilMs","declaredAtMs","timer","setTimeout","clearTimeout","retry","stop","accepted","description","confirmationText","cancellationText","then","catch","variant","persist","kind","allowDuplicate","text","severity","data-testid","sx","width","spacing","color","aria-label","direction","size","onClick","disabled","displayName"],"mappings":"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"}
@@ -1,22 +1,12 @@
1
- /**
2
- * Which permission the signed-in member lacks for this switch — its label, or
3
- * `null` when they hold both — and whether that is known yet.
4
- *
5
- * Read off the member's own membership document, and their custom role when
6
- * they hold one, both of which the rules let a member read. The shell's
7
- * permission map carries the legacy keys and the plugin-declared ones, not
8
- * these two, and the CRM's org settings cards answer the same question the
9
- * same way. `ready` separates "no" from "not yet", so the switch disables
10
- * with a reason instead of hiding until the read lands. A membership that
11
- * cannot be read resolves no permission at all; the route decides either way.
12
- */
13
- export declare function useConsentGroupConfirmationAccess(orgId: string | undefined): {
14
- missing: string | null;
15
- ready: boolean;
16
- };
1
+ import { type ConsentGroupAccess } from './consent-group-access';
17
2
  export interface ConsentGroupConfirmationCardProps {
18
3
  /** The org document the shell passes, kept live by its listener. */
19
4
  org?: Record<string, unknown> | null;
5
+ /**
6
+ * Whether the reader may move the switch, when the section already asked.
7
+ * Absent, the card reads the reader's membership itself.
8
+ */
9
+ access?: ConsentGroupAccess;
20
10
  }
21
11
  /**
22
12
  * "Wait for confirmation across a consent group" — the org's one switch over