@aglyn/plugins-outreach 1.0.0-beta.197 → 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 (63) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/compliance-section.d.ts +3 -1
  3. package/src/lib/components/compliance-section.js +110 -45
  4. package/src/lib/components/compliance-section.js.map +1 -1
  5. package/src/lib/components/enroll-dialog.d.ts +7 -1
  6. package/src/lib/components/enroll-dialog.js +47 -3
  7. package/src/lib/components/enroll-dialog.js.map +1 -1
  8. package/src/lib/components/outreach-ui.js +1 -0
  9. package/src/lib/components/outreach-ui.js.map +1 -1
  10. package/src/lib/components/sequence-editor.js +25 -24
  11. package/src/lib/components/sequence-editor.js.map +1 -1
  12. package/src/lib/engine/delivery-status.d.ts +8 -0
  13. package/src/lib/engine/delivery-status.js +10 -5
  14. package/src/lib/engine/delivery-status.js.map +1 -1
  15. package/src/lib/engine/enrollment-state.d.ts +2 -0
  16. package/src/lib/engine/enrollment-state.js +2 -2
  17. package/src/lib/engine/enrollment-state.js.map +1 -1
  18. package/src/lib/engine/gates.d.ts +28 -1
  19. package/src/lib/engine/gates.js +28 -1
  20. package/src/lib/engine/gates.js.map +1 -1
  21. package/src/lib/engine/mail-gateway.d.ts +97 -0
  22. package/src/lib/engine/mail-gateway.js +226 -0
  23. package/src/lib/engine/mail-gateway.js.map +1 -0
  24. package/src/lib/engine/mailbox-health.d.ts +3 -3
  25. package/src/lib/engine/mailbox-health.js +17 -11
  26. package/src/lib/engine/mailbox-health.js.map +1 -1
  27. package/src/lib/engine/thread-classification.d.ts +2 -0
  28. package/src/lib/engine/thread-classification.js +2 -1
  29. package/src/lib/engine/thread-classification.js.map +1 -1
  30. package/src/lib/enrollment/enroll-people.d.ts +6 -1
  31. package/src/lib/enrollment/enroll-people.js +18 -4
  32. package/src/lib/enrollment/enroll-people.js.map +1 -1
  33. package/src/lib/enrollment/gate-lookups.d.ts +7 -0
  34. package/src/lib/enrollment/gate-lookups.js +11 -2
  35. package/src/lib/enrollment/gate-lookups.js.map +1 -1
  36. package/src/lib/model/outreach-api.d.ts +14 -0
  37. package/src/lib/model/outreach-api.js.map +1 -1
  38. package/src/lib/model/outreach.types.d.ts +79 -3
  39. package/src/lib/model/outreach.types.js +21 -2
  40. package/src/lib/model/outreach.types.js.map +1 -1
  41. package/src/lib/routes/enroll-routes.js +23 -4
  42. package/src/lib/routes/enroll-routes.js.map +1 -1
  43. package/src/lib/routes/enrollment-routes.js +15 -1
  44. package/src/lib/routes/enrollment-routes.js.map +1 -1
  45. package/src/lib/routes/register-routes.js +3 -0
  46. package/src/lib/routes/register-routes.js.map +1 -1
  47. package/src/lib/routes/route-deps.d.ts +6 -0
  48. package/src/lib/routes/route-deps.js.map +1 -1
  49. package/src/lib/runtime/gateway-ledger.d.ts +46 -0
  50. package/src/lib/runtime/gateway-ledger.js +113 -0
  51. package/src/lib/runtime/gateway-ledger.js.map +1 -0
  52. package/src/lib/runtime/platform-runtime-deps.js +3 -0
  53. package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
  54. package/src/lib/runtime/runtime-deps.d.ts +6 -0
  55. package/src/lib/runtime/runtime-deps.js.map +1 -1
  56. package/src/lib/runtime/send-job.js +48 -0
  57. package/src/lib/runtime/send-job.js.map +1 -1
  58. package/src/lib/runtime/sync-job.d.ts +2 -0
  59. package/src/lib/runtime/sync-job.js +40 -3
  60. package/src/lib/runtime/sync-job.js.map +1 -1
  61. package/src/lib/storage/domain-intel-store.d.ts +107 -0
  62. package/src/lib/storage/domain-intel-store.js +320 -0
  63. package/src/lib/storage/domain-intel-store.js.map +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-outreach",
3
- "version": "1.0.0-beta.197",
3
+ "version": "1.0.0-beta.198",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,19 +25,19 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.197",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.197",
30
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.197",
31
- "@aglyn/shared-ui-jsx": "1.0.0-beta.197",
32
- "@aglyn/shared-ui-next": "1.0.0-beta.197",
33
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.197",
34
- "@aglyn/shared-util-email": "1.0.0-beta.197",
35
- "@aglyn/shared-util-http": "1.0.0-beta.197",
36
- "@aglyn/shared-util-timestamp": "1.0.0-beta.197",
37
- "@aglyn/shared-util-tools": "1.0.0-beta.197",
38
- "@aglyn/tenant-data-admin": "1.0.0-beta.197",
39
- "@aglyn/tenant-feature-instance": "1.0.0-beta.197",
40
- "@aglyn/tenant-runtime": "1.0.0-beta.197",
28
+ "@aglyn/aglyn": "1.0.0-beta.198",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.198",
30
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.198",
31
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.198",
32
+ "@aglyn/shared-ui-next": "1.0.0-beta.198",
33
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.198",
34
+ "@aglyn/shared-util-email": "1.0.0-beta.198",
35
+ "@aglyn/shared-util-http": "1.0.0-beta.198",
36
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.198",
37
+ "@aglyn/shared-util-tools": "1.0.0-beta.198",
38
+ "@aglyn/tenant-data-admin": "1.0.0-beta.198",
39
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.198",
40
+ "@aglyn/tenant-runtime": "1.0.0-beta.198",
41
41
  "@swc/helpers": "0.5.23"
42
42
  },
43
43
  "peerDependencies": {
@@ -7,7 +7,9 @@ export declare const OUTREACH_POSTAL_ADDRESS_HELP: string;
7
7
  /**
8
8
  * Sequences → Compliance (AGL-2980): who every email says sent it, the
9
9
  * countries a sequence may send to at all, and — below the settings, with
10
- * no Save of its own — the domains no sequence emails (AGL-3244).
10
+ * no Save of its own — the domains no sequence emails (AGL-3244). Each
11
+ * settings card saves its own fields from its header, the console's place
12
+ * for a card's actions (AGL-3333).
11
13
  *
12
14
  * The footer is previewed from what is typed, by the engine's own
13
15
  * `composeOutreachFooter`, so the page shows the lines every email will end
@@ -21,7 +21,7 @@ import { pluginDocsHelp } from "@aglyn/aglyn";
21
21
  import { CardDisplay } from "@aglyn/shared-ui-jsx";
22
22
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
23
23
  import { Alert, Autocomplete, Button, Chip, Paper, Stack, TextField, Typography } from "@mui/material";
24
- import { useEffect, useMemo, useState } from "react";
24
+ import { useEffect, useMemo, useRef, useState } from "react";
25
25
  import { composeOutreachFooter } from "../engine/compose.js";
26
26
  import { outreachComplianceSettingsEqual, outreachCountryLabel, outreachCountryOptions, OUTREACH_BRAND_NAME_MAX, OUTREACH_LEGAL_NAME_MAX, OUTREACH_POSTAL_ADDRESS_MAX, validateOutreachComplianceSettings } from "../model/compliance-settings.js";
27
27
  import { OutreachDoNotContactDomainsCard } from "./do-not-contact-domains.js";
@@ -37,10 +37,41 @@ const EMPTY = {
37
37
  'US'
38
38
  ]
39
39
  };
40
+ /**
41
+ * The page's cards and the settings each one owns. Every card saves only its
42
+ * own fields onto what is stored, so saving the countries never commits a
43
+ * half-edited sender identity, and the other way round.
44
+ */ const CARDS = {
45
+ identity: {
46
+ fields: [
47
+ 'legalName',
48
+ 'brandName',
49
+ 'postalAddress'
50
+ ],
51
+ saved: 'Sender identity saved.'
52
+ },
53
+ countries: {
54
+ fields: [
55
+ 'allowedCountries'
56
+ ],
57
+ saved: 'Allowed countries saved.'
58
+ }
59
+ };
60
+ function pickCard(settings, card) {
61
+ return Object.fromEntries(CARDS[card].fields.map((field)=>[
62
+ field,
63
+ settings[field]
64
+ ]));
65
+ }
66
+ function cardEqual(card, a, b) {
67
+ return outreachComplianceSettingsEqual(_extends({}, b, pickCard(a, card)), b);
68
+ }
40
69
  /**
41
70
  * Sequences → Compliance (AGL-2980): who every email says sent it, the
42
71
  * countries a sequence may send to at all, and — below the settings, with
43
- * no Save of its own — the domains no sequence emails (AGL-3244).
72
+ * no Save of its own — the domains no sequence emails (AGL-3244). Each
73
+ * settings card saves its own fields from its header, the console's place
74
+ * for a card's actions (AGL-3333).
44
75
  *
45
76
  * The footer is previewed from what is typed, by the engine's own
46
77
  * `composeOutreachFooter`, so the page shows the lines every email will end
@@ -53,12 +84,27 @@ const EMPTY = {
53
84
  const loaded = useOutreachComplianceSettings(api, orgId);
54
85
  const { enqueueSnackbar } = useSnackbar();
55
86
  const [form, setForm] = useState(EMPTY);
56
- const [saving, setSaving] = useState(false);
87
+ const [saving, setSaving] = useState(null);
57
88
  const [serverIssues, setServerIssues] = useState({});
58
89
  const countries = useMemo(()=>outreachCountryOptions(), []);
59
90
  const stored = loaded.settings;
91
+ const previousStored = useRef(null);
60
92
  useEffect(()=>{
61
- if (stored) setForm(_extends({}, stored));
93
+ if (!stored) return;
94
+ const before = previousStored.current;
95
+ previousStored.current = stored;
96
+ // A fresh read replaces every card the member has not touched; a card
97
+ // with unsaved edits keeps them, so saving one card never wipes another.
98
+ setForm((current)=>{
99
+ if (!before) return _extends({}, stored);
100
+ let next = _extends({}, stored);
101
+ for (const card of Object.keys(CARDS)){
102
+ if (!cardEqual(card, current, before)) {
103
+ next = _extends({}, next, pickCard(current, card));
104
+ }
105
+ }
106
+ return next;
107
+ });
62
108
  }, [
63
109
  stored
64
110
  ]);
@@ -68,37 +114,54 @@ const EMPTY = {
68
114
  var _issues_find;
69
115
  return (_serverIssues_field = serverIssues[field]) != null ? _serverIssues_field : (_issues_find = issues.find((issue)=>issue.field === field)) == null ? void 0 : _issues_find.message;
70
116
  };
71
- const dirty = stored ? !outreachComplianceSettingsEqual(stored, normalized) : false;
117
+ /** What saving one card would store: its own fields over the stored rest. */ const candidate = (card)=>validateOutreachComplianceSettings(stored ? _extends({}, stored, pickCard(form, card)) : form);
118
+ const cardDirty = (card)=>stored ? !outreachComplianceSettingsEqual(stored, candidate(card).settings) : false;
119
+ const cardBlocked = (card)=>candidate(card).issues.some((issue)=>CARDS[card].fields.includes(issue.field));
72
120
  const footer = composeOutreachFooter(normalized);
73
121
  const set = (field)=>(value)=>{
74
- setServerIssues({});
122
+ setServerIssues((previous)=>{
123
+ const next = _extends({}, previous);
124
+ delete next[field];
125
+ return next;
126
+ });
75
127
  setForm((previous)=>_extends({}, previous, {
76
128
  [field]: value
77
129
  }));
78
130
  };
79
- const save = async ()=>{
80
- setSaving(true);
131
+ const clearIssues = (card)=>setServerIssues((previous)=>{
132
+ const next = _extends({}, previous);
133
+ for (const field of CARDS[card].fields){
134
+ delete next[field];
135
+ }
136
+ return next;
137
+ });
138
+ const discard = (card)=>{
139
+ clearIssues(card);
140
+ if (stored) setForm((previous)=>_extends({}, previous, pickCard(stored, card)));
141
+ };
142
+ const save = async (card)=>{
143
+ setSaving(card);
81
144
  try {
82
- const answer = await api.saveSettings(normalized);
83
- setServerIssues({});
84
- setForm(_extends({}, answer.settings));
145
+ const answer = await api.saveSettings(candidate(card).settings);
146
+ clearIssues(card);
147
+ setForm((previous)=>_extends({}, previous, pickCard(answer.settings, card)));
85
148
  loaded.reload();
86
- enqueueSnackbar(answer.changed ? 'Compliance settings saved.' : 'Nothing changed.', {
149
+ enqueueSnackbar(answer.changed ? CARDS[card].saved : 'Nothing changed.', {
87
150
  variant: answer.changed ? 'success' : 'info'
88
151
  });
89
152
  } catch (error) {
90
153
  if (error instanceof OutreachRouteError && error.issues.length) {
91
- setServerIssues(Object.fromEntries(error.issues.filter((issue)=>'field' in issue).map((issue)=>[
92
- issue.field,
93
- issue.message
94
- ])));
154
+ setServerIssues((previous)=>_extends({}, previous, Object.fromEntries(error.issues.filter((issue)=>'field' in issue).map((issue)=>[
155
+ issue.field,
156
+ issue.message
157
+ ]))));
95
158
  }
96
159
  enqueueSnackbar(error.message, {
97
160
  variant: 'error',
98
161
  allowDuplicate: true
99
162
  });
100
163
  } finally{
101
- setSaving(false);
164
+ setSaving(null);
102
165
  }
103
166
  };
104
167
  if (loaded.status === 'loading' || !orgId && !stored) {
@@ -121,6 +184,29 @@ const EMPTY = {
121
184
  name: outreachCountryLabel(code)
122
185
  };
123
186
  });
187
+ const cardActions = (card)=>{
188
+ const dirty = cardDirty(card);
189
+ return /*#__PURE__*/ _jsxs(Stack, {
190
+ direction: "row",
191
+ spacing: 1,
192
+ children: [
193
+ /*#__PURE__*/ _jsx(Button, {
194
+ size: "small",
195
+ variant: "text",
196
+ disabled: !dirty || saving !== null,
197
+ onClick: ()=>discard(card),
198
+ children: "Discard changes"
199
+ }),
200
+ /*#__PURE__*/ _jsx(Button, {
201
+ size: "small",
202
+ variant: "contained",
203
+ disabled: !dirty || saving !== null || cardBlocked(card),
204
+ onClick: ()=>void save(card),
205
+ children: saving === card ? 'Saving…' : 'Save'
206
+ })
207
+ ]
208
+ });
209
+ };
124
210
  return /*#__PURE__*/ _jsxs(Stack, {
125
211
  spacing: 2,
126
212
  children: [
@@ -129,6 +215,9 @@ const EMPTY = {
129
215
  help: pluginDocsHelp('sequences', {
130
216
  anchor: '#compliance-settings'
131
217
  }),
218
+ HeaderProps: {
219
+ action: cardActions('identity')
220
+ },
132
221
  contentGutterX: true,
133
222
  contentGutterY: true,
134
223
  children: /*#__PURE__*/ _jsxs(Stack, {
@@ -215,6 +304,9 @@ const EMPTY = {
215
304
  help: pluginDocsHelp('sequences', {
216
305
  anchor: '#allowed-countries'
217
306
  }),
307
+ HeaderProps: {
308
+ action: cardActions('countries')
309
+ },
218
310
  contentGutterX: true,
219
311
  contentGutterY: true,
220
312
  children: /*#__PURE__*/ _jsxs(Stack, {
@@ -232,7 +324,7 @@ const EMPTY = {
232
324
  getOptionLabel: (option)=>option.name,
233
325
  isOptionEqualToValue: (option, value)=>option.code === value.code,
234
326
  onChange: (_event, next)=>{
235
- setServerIssues({});
327
+ clearIssues('countries');
236
328
  setForm((previous)=>_extends({}, previous, {
237
329
  allowedCountries: next.map((option)=>option.code)
238
330
  }));
@@ -257,33 +349,6 @@ const EMPTY = {
257
349
  ]
258
350
  })
259
351
  }),
260
- /*#__PURE__*/ _jsxs(Stack, {
261
- direction: {
262
- xs: 'column',
263
- sm: 'row'
264
- },
265
- spacing: 1,
266
- sx: {
267
- justifyContent: 'flex-end'
268
- },
269
- children: [
270
- /*#__PURE__*/ _jsx(Button, {
271
- variant: "text",
272
- disabled: !dirty || saving,
273
- onClick: ()=>{
274
- setServerIssues({});
275
- if (stored) setForm(_extends({}, stored));
276
- },
277
- children: "Discard changes"
278
- }),
279
- /*#__PURE__*/ _jsx(Button, {
280
- variant: "contained",
281
- disabled: !dirty || saving || issues.length > 0,
282
- onClick: ()=>void save(),
283
- children: saving ? 'Saving…' : 'Save'
284
- })
285
- ]
286
- }),
287
352
  /*#__PURE__*/ _jsx(OutreachDoNotContactDomainsCard, {
288
353
  orgId: orgId
289
354
  })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/compliance-section.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 { pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Autocomplete,\n Button,\n Chip,\n Paper,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useMemo, useState } from 'react'\nimport { composeOutreachFooter } from '../engine/compose'\nimport {\n outreachComplianceSettingsEqual,\n outreachCountryLabel,\n outreachCountryOptions,\n OUTREACH_BRAND_NAME_MAX,\n OUTREACH_LEGAL_NAME_MAX,\n OUTREACH_POSTAL_ADDRESS_MAX,\n validateOutreachComplianceSettings,\n type OutreachComplianceField,\n type OutreachCountryOption,\n} from '../model/compliance-settings'\nimport type { OutreachComplianceSettings } from '../model/outreach.types'\nimport { OutreachDoNotContactDomainsCard } from './do-not-contact-domains'\nimport { OutreachLoading, OutreachLoadProblem } from './outreach-ui'\nimport { OutreachRouteError, useOutreachApi } from './use-outreach-api'\nimport { useOutreachComplianceSettings } from './use-outreach-settings'\n\nexport interface OutreachComplianceSectionProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n}\n\n/** What CAN-SPAM accepts as the postal address, as the field says it. */\nexport const OUTREACH_POSTAL_ADDRESS_HELP =\n 'A street address, a post office box registered with the US Postal Service, or a private mailbox ' +\n 'registered with the US Postal Service. Every email prints it in its footer.'\n\nconst EMPTY: OutreachComplianceSettings = {\n legalName: '',\n brandName: '',\n postalAddress: '',\n allowedCountries: ['US'],\n}\n\n/**\n * Sequences → Compliance (AGL-2980): who every email says sent it, the\n * countries a sequence may send to at all, and — below the settings, with\n * no Save of its own — the domains no sequence emails (AGL-3244).\n *\n * The footer is previewed from what is typed, by the engine's own\n * `composeOutreachFooter`, so the page shows the lines every email will end\n * with — or, while the legal name or the address is empty, the sentence\n * that explains why nothing can be sent and no sequence activated.\n */\nexport function OutreachComplianceSection(\n props: OutreachComplianceSectionProps,\n) {\n const { orgId } = props\n const api = useOutreachApi(orgId)\n const loaded = useOutreachComplianceSettings(api, orgId)\n const { enqueueSnackbar } = useSnackbar()\n const [form, setForm] = useState<OutreachComplianceSettings>(EMPTY)\n const [saving, setSaving] = useState(false)\n const [serverIssues, setServerIssues] = useState<\n Partial<Record<OutreachComplianceField, string>>\n >({})\n const countries = useMemo(() => outreachCountryOptions(), [])\n\n const stored = loaded.settings\n useEffect(() => {\n if (stored) setForm({ ...stored })\n }, [stored])\n\n const { settings: normalized, issues } =\n validateOutreachComplianceSettings(form)\n const fieldIssue = (field: OutreachComplianceField) =>\n serverIssues[field] ??\n issues.find((issue) => issue.field === field)?.message\n const dirty = stored\n ? !outreachComplianceSettingsEqual(stored, normalized)\n : false\n const footer = composeOutreachFooter(normalized)\n const set = (field: keyof OutreachComplianceSettings) => (value: string) => {\n setServerIssues({})\n setForm((previous) => ({ ...previous, [field]: value }))\n }\n\n const save = async () => {\n setSaving(true)\n try {\n const answer = await api.saveSettings(normalized)\n setServerIssues({})\n setForm({ ...answer.settings })\n loaded.reload()\n enqueueSnackbar(\n answer.changed ? 'Compliance settings saved.' : 'Nothing changed.',\n {\n variant: answer.changed ? 'success' : 'info',\n },\n )\n } catch (error) {\n if (error instanceof OutreachRouteError && error.issues.length) {\n setServerIssues(\n Object.fromEntries(\n error.issues\n .filter(\n (\n issue,\n ): issue is {\n field: OutreachComplianceField\n message: string\n } => 'field' in issue,\n )\n .map((issue) => [issue.field, issue.message]),\n ),\n )\n }\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setSaving(false)\n }\n }\n\n if (loaded.status === 'loading' || (!orgId && !stored)) {\n return <OutreachLoading label=\"Loading compliance settings…\" />\n }\n if (loaded.status === 'error' || loaded.status === 'refused') {\n return (\n <OutreachLoadProblem\n status={loaded.status}\n what=\"compliance settings\"\n message={loaded.message}\n onRetry={loaded.reload}\n />\n )\n }\n\n const selectedCountries = form.allowedCountries.map(\n (code) =>\n countries.find((option) => option.code === code) ?? {\n code,\n name: outreachCountryLabel(code),\n },\n )\n\n return (\n <Stack spacing={2}>\n <CardDisplay\n header=\"Sender identity\"\n help={pluginDocsHelp('sequences', { anchor: '#compliance-settings' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Every email a sequence sends ends with a footer naming your organization, its postal ' +\n 'address, that the email is a sales email, and how to stop more of them. The law ' +\n 'requires these on commercial email, so every email gets the footer automatically.'}\n </Typography>\n <TextField\n label=\"Legal name\"\n value={form.legalName}\n onChange={(event) => set('legalName')(event.target.value)}\n error={Boolean(fieldIssue('legalName'))}\n helperText={\n fieldIssue('legalName') ??\n 'Your organization’s legal name, as the footer prints it.'\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_LEGAL_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n label=\"Brand name\"\n value={form.brandName}\n onChange={(event) => set('brandName')(event.target.value)}\n error={Boolean(fieldIssue('brandName'))}\n helperText={\n fieldIssue('brandName') ??\n 'The name the solicitation sentence uses. Leave empty to use the legal name.'\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_BRAND_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n label=\"Postal address\"\n value={form.postalAddress}\n onChange={(event) => set('postalAddress')(event.target.value)}\n error={Boolean(fieldIssue('postalAddress'))}\n helperText={\n fieldIssue('postalAddress') ?? OUTREACH_POSTAL_ADDRESS_HELP\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_POSTAL_ADDRESS_MAX + 40 },\n }}\n multiline\n minRows={3}\n fullWidth\n />\n {footer.footer ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">Every email ends with</Typography>\n <Paper variant=\"outlined\" sx={{ p: 1.5 }}>\n <Typography\n variant=\"body2\"\n sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}\n data-testid=\"outreach-footer-preview\"\n >\n {footer.footer}\n </Typography>\n </Paper>\n </Stack>\n ) : (\n <Alert severity=\"warning\">\n {'No sequence can be activated, and no email sent, until you add your organization’s legal ' +\n 'name and postal address.'}\n </Alert>\n )}\n </Stack>\n </CardDisplay>\n\n <CardDisplay\n header=\"Allowed countries\"\n help={pluginDocsHelp('sequences', { anchor: '#allowed-countries' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Sequences may send only to people in these countries, and every sequence only to the ones ' +\n 'it and this list both allow. The United States is the default: Canada, the United Kingdom and most ' +\n 'of the European Union require a consent basis that an email to someone who never contacted ' +\n 'you does not have, so a cold email never goes outside the United States whatever this list ' +\n 'says. Add another country only for people who came to you first.'}\n </Typography>\n <Autocomplete<OutreachCountryOption, true>\n multiple\n options={countries}\n value={selectedCountries}\n getOptionLabel={(option) => option.name}\n isOptionEqualToValue={(option, value) => option.code === value.code}\n onChange={(_event, next) => {\n setServerIssues({})\n setForm((previous) => ({\n ...previous,\n allowedCountries: next.map((option) => option.code),\n }))\n }}\n renderValue={(value, getItemProps) =>\n value.map((option, index) => {\n const { key, ...item } = getItemProps({ index })\n return (\n <Chip key={key} size=\"small\" label={option.name} {...item} />\n )\n })\n }\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Countries\"\n error={Boolean(fieldIssue('allowedCountries'))}\n helperText={fieldIssue('allowedCountries')}\n />\n )}\n />\n </Stack>\n </CardDisplay>\n\n <Stack\n direction={{ xs: 'column', sm: 'row' }}\n spacing={1}\n sx={{ justifyContent: 'flex-end' }}\n >\n <Button\n variant=\"text\"\n disabled={!dirty || saving}\n onClick={() => {\n setServerIssues({})\n if (stored) setForm({ ...stored })\n }}\n >\n Discard changes\n </Button>\n <Button\n variant=\"contained\"\n disabled={!dirty || saving || issues.length > 0}\n onClick={() => void save()}\n >\n {saving ? 'Saving…' : 'Save'}\n </Button>\n </Stack>\n\n {/* Its own list, saved as it is edited (AGL-3244): no Save above it. */}\n <OutreachDoNotContactDomainsCard orgId={orgId} />\n </Stack>\n )\n}\nOutreachComplianceSection.displayName = 'OutreachComplianceSection'\n\nexport default OutreachComplianceSection\n"],"names":["pluginDocsHelp","CardDisplay","useSnackbar","Alert","Autocomplete","Button","Chip","Paper","Stack","TextField","Typography","useEffect","useMemo","useState","composeOutreachFooter","outreachComplianceSettingsEqual","outreachCountryLabel","outreachCountryOptions","OUTREACH_BRAND_NAME_MAX","OUTREACH_LEGAL_NAME_MAX","OUTREACH_POSTAL_ADDRESS_MAX","validateOutreachComplianceSettings","OutreachDoNotContactDomainsCard","OutreachLoading","OutreachLoadProblem","OutreachRouteError","useOutreachApi","useOutreachComplianceSettings","OUTREACH_POSTAL_ADDRESS_HELP","EMPTY","legalName","brandName","postalAddress","allowedCountries","OutreachComplianceSection","props","fieldIssue","orgId","api","loaded","enqueueSnackbar","form","setForm","saving","setSaving","serverIssues","setServerIssues","countries","stored","settings","normalized","issues","field","find","issue","message","dirty","footer","set","value","previous","save","answer","saveSettings","reload","changed","variant","error","length","Object","fromEntries","filter","map","allowDuplicate","status","label","what","onRetry","selectedCountries","code","option","name","spacing","header","help","anchor","contentGutterX","contentGutterY","color","onChange","event","target","Boolean","helperText","slotProps","htmlInput","maxLength","fullWidth","multiline","minRows","sx","p","whiteSpace","wordBreak","data-testid","severity","multiple","options","getOptionLabel","isOptionEqualToValue","_event","next","renderValue","getItemProps","index","key","item","size","renderInput","params","direction","xs","sm","justifyContent","disabled","onClick","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/compliance-section.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 { pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Autocomplete,\n Button,\n Chip,\n Paper,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport { composeOutreachFooter } from '../engine/compose'\nimport {\n outreachComplianceSettingsEqual,\n outreachCountryLabel,\n outreachCountryOptions,\n OUTREACH_BRAND_NAME_MAX,\n OUTREACH_LEGAL_NAME_MAX,\n OUTREACH_POSTAL_ADDRESS_MAX,\n validateOutreachComplianceSettings,\n type OutreachComplianceField,\n type OutreachCountryOption,\n} from '../model/compliance-settings'\nimport type { OutreachComplianceSettings } from '../model/outreach.types'\nimport { OutreachDoNotContactDomainsCard } from './do-not-contact-domains'\nimport { OutreachLoading, OutreachLoadProblem } from './outreach-ui'\nimport { OutreachRouteError, useOutreachApi } from './use-outreach-api'\nimport { useOutreachComplianceSettings } from './use-outreach-settings'\n\nexport interface OutreachComplianceSectionProps {\n /** The organization the hub is mounted under; `null` until the shell knows it. */\n orgId: string | null\n}\n\n/** What CAN-SPAM accepts as the postal address, as the field says it. */\nexport const OUTREACH_POSTAL_ADDRESS_HELP =\n 'A street address, a post office box registered with the US Postal Service, or a private mailbox ' +\n 'registered with the US Postal Service. Every email prints it in its footer.'\n\nconst EMPTY: OutreachComplianceSettings = {\n legalName: '',\n brandName: '',\n postalAddress: '',\n allowedCountries: ['US'],\n}\n\ntype ComplianceCardKey = 'identity' | 'countries'\n\n/**\n * The page's cards and the settings each one owns. Every card saves only its\n * own fields onto what is stored, so saving the countries never commits a\n * half-edited sender identity, and the other way round.\n */\nconst CARDS: Record<\n ComplianceCardKey,\n { fields: readonly (keyof OutreachComplianceSettings)[]; saved: string }\n> = {\n identity: {\n fields: ['legalName', 'brandName', 'postalAddress'],\n saved: 'Sender identity saved.',\n },\n countries: {\n fields: ['allowedCountries'],\n saved: 'Allowed countries saved.',\n },\n}\n\nfunction pickCard(\n settings: OutreachComplianceSettings,\n card: ComplianceCardKey,\n): Partial<OutreachComplianceSettings> {\n return Object.fromEntries(\n CARDS[card].fields.map((field) => [field, settings[field]]),\n )\n}\n\nfunction cardEqual(\n card: ComplianceCardKey,\n a: OutreachComplianceSettings,\n b: OutreachComplianceSettings,\n): boolean {\n return outreachComplianceSettingsEqual({ ...b, ...pickCard(a, card) }, b)\n}\n\n/**\n * Sequences → Compliance (AGL-2980): who every email says sent it, the\n * countries a sequence may send to at all, and — below the settings, with\n * no Save of its own — the domains no sequence emails (AGL-3244). Each\n * settings card saves its own fields from its header, the console's place\n * for a card's actions (AGL-3333).\n *\n * The footer is previewed from what is typed, by the engine's own\n * `composeOutreachFooter`, so the page shows the lines every email will end\n * with — or, while the legal name or the address is empty, the sentence\n * that explains why nothing can be sent and no sequence activated.\n */\nexport function OutreachComplianceSection(\n props: OutreachComplianceSectionProps,\n) {\n const { orgId } = props\n const api = useOutreachApi(orgId)\n const loaded = useOutreachComplianceSettings(api, orgId)\n const { enqueueSnackbar } = useSnackbar()\n const [form, setForm] = useState<OutreachComplianceSettings>(EMPTY)\n const [saving, setSaving] = useState<ComplianceCardKey | null>(null)\n const [serverIssues, setServerIssues] = useState<\n Partial<Record<OutreachComplianceField, string>>\n >({})\n const countries = useMemo(() => outreachCountryOptions(), [])\n\n const stored = loaded.settings\n const previousStored = useRef<OutreachComplianceSettings | null>(null)\n useEffect(() => {\n if (!stored) return\n const before = previousStored.current\n previousStored.current = stored\n // A fresh read replaces every card the member has not touched; a card\n // with unsaved edits keeps them, so saving one card never wipes another.\n setForm((current) => {\n if (!before) return { ...stored }\n let next: OutreachComplianceSettings = { ...stored }\n for (const card of Object.keys(CARDS) as ComplianceCardKey[]) {\n if (!cardEqual(card, current, before)) {\n next = { ...next, ...pickCard(current, card) }\n }\n }\n return next\n })\n }, [stored])\n\n const { settings: normalized, issues } =\n validateOutreachComplianceSettings(form)\n const fieldIssue = (field: OutreachComplianceField) =>\n serverIssues[field] ??\n issues.find((issue) => issue.field === field)?.message\n /** What saving one card would store: its own fields over the stored rest. */\n const candidate = (card: ComplianceCardKey) =>\n validateOutreachComplianceSettings(\n stored ? { ...stored, ...pickCard(form, card) } : form,\n )\n const cardDirty = (card: ComplianceCardKey) =>\n stored\n ? !outreachComplianceSettingsEqual(stored, candidate(card).settings)\n : false\n const cardBlocked = (card: ComplianceCardKey) =>\n candidate(card).issues.some((issue) =>\n (CARDS[card].fields as readonly string[]).includes(issue.field),\n )\n const footer = composeOutreachFooter(normalized)\n const set = (field: keyof OutreachComplianceSettings) => (value: string) => {\n setServerIssues((previous) => {\n const next = { ...previous }\n delete next[field as OutreachComplianceField]\n return next\n })\n setForm((previous) => ({ ...previous, [field]: value }))\n }\n\n const clearIssues = (card: ComplianceCardKey) =>\n setServerIssues((previous) => {\n const next = { ...previous }\n for (const field of CARDS[card].fields) {\n delete next[field as OutreachComplianceField]\n }\n return next\n })\n\n const discard = (card: ComplianceCardKey) => {\n clearIssues(card)\n if (stored)\n setForm((previous) => ({ ...previous, ...pickCard(stored, card) }))\n }\n\n const save = async (card: ComplianceCardKey) => {\n setSaving(card)\n try {\n const answer = await api.saveSettings(candidate(card).settings)\n clearIssues(card)\n setForm((previous) => ({\n ...previous,\n ...pickCard(answer.settings, card),\n }))\n loaded.reload()\n enqueueSnackbar(answer.changed ? CARDS[card].saved : 'Nothing changed.', {\n variant: answer.changed ? 'success' : 'info',\n })\n } catch (error) {\n if (error instanceof OutreachRouteError && error.issues.length) {\n setServerIssues((previous) => ({\n ...previous,\n ...Object.fromEntries(\n error.issues\n .filter(\n (\n issue,\n ): issue is {\n field: OutreachComplianceField\n message: string\n } => 'field' in issue,\n )\n .map((issue) => [issue.field, issue.message]),\n ),\n }))\n }\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setSaving(null)\n }\n }\n\n if (loaded.status === 'loading' || (!orgId && !stored)) {\n return <OutreachLoading label=\"Loading compliance settings…\" />\n }\n if (loaded.status === 'error' || loaded.status === 'refused') {\n return (\n <OutreachLoadProblem\n status={loaded.status}\n what=\"compliance settings\"\n message={loaded.message}\n onRetry={loaded.reload}\n />\n )\n }\n\n const selectedCountries = form.allowedCountries.map(\n (code) =>\n countries.find((option) => option.code === code) ?? {\n code,\n name: outreachCountryLabel(code),\n },\n )\n\n const cardActions = (card: ComplianceCardKey) => {\n const dirty = cardDirty(card)\n return (\n <Stack direction=\"row\" spacing={1}>\n <Button\n size=\"small\"\n variant=\"text\"\n disabled={!dirty || saving !== null}\n onClick={() => discard(card)}\n >\n Discard changes\n </Button>\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={!dirty || saving !== null || cardBlocked(card)}\n onClick={() => void save(card)}\n >\n {saving === card ? 'Saving…' : 'Save'}\n </Button>\n </Stack>\n )\n }\n\n return (\n <Stack spacing={2}>\n <CardDisplay\n header=\"Sender identity\"\n help={pluginDocsHelp('sequences', { anchor: '#compliance-settings' })}\n HeaderProps={{ action: cardActions('identity') }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Every email a sequence sends ends with a footer naming your organization, its postal ' +\n 'address, that the email is a sales email, and how to stop more of them. The law ' +\n 'requires these on commercial email, so every email gets the footer automatically.'}\n </Typography>\n <TextField\n label=\"Legal name\"\n value={form.legalName}\n onChange={(event) => set('legalName')(event.target.value)}\n error={Boolean(fieldIssue('legalName'))}\n helperText={\n fieldIssue('legalName') ??\n 'Your organization’s legal name, as the footer prints it.'\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_LEGAL_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n label=\"Brand name\"\n value={form.brandName}\n onChange={(event) => set('brandName')(event.target.value)}\n error={Boolean(fieldIssue('brandName'))}\n helperText={\n fieldIssue('brandName') ??\n 'The name the solicitation sentence uses. Leave empty to use the legal name.'\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_BRAND_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n label=\"Postal address\"\n value={form.postalAddress}\n onChange={(event) => set('postalAddress')(event.target.value)}\n error={Boolean(fieldIssue('postalAddress'))}\n helperText={\n fieldIssue('postalAddress') ?? OUTREACH_POSTAL_ADDRESS_HELP\n }\n slotProps={{\n htmlInput: { maxLength: OUTREACH_POSTAL_ADDRESS_MAX + 40 },\n }}\n multiline\n minRows={3}\n fullWidth\n />\n {footer.footer ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">Every email ends with</Typography>\n <Paper variant=\"outlined\" sx={{ p: 1.5 }}>\n <Typography\n variant=\"body2\"\n sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}\n data-testid=\"outreach-footer-preview\"\n >\n {footer.footer}\n </Typography>\n </Paper>\n </Stack>\n ) : (\n <Alert severity=\"warning\">\n {'No sequence can be activated, and no email sent, until you add your organization’s legal ' +\n 'name and postal address.'}\n </Alert>\n )}\n </Stack>\n </CardDisplay>\n\n <CardDisplay\n header=\"Allowed countries\"\n help={pluginDocsHelp('sequences', { anchor: '#allowed-countries' })}\n HeaderProps={{ action: cardActions('countries') }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Sequences may send only to people in these countries, and every sequence only to the ones ' +\n 'it and this list both allow. The United States is the default: Canada, the United Kingdom and most ' +\n 'of the European Union require a consent basis that an email to someone who never contacted ' +\n 'you does not have, so a cold email never goes outside the United States whatever this list ' +\n 'says. Add another country only for people who came to you first.'}\n </Typography>\n <Autocomplete<OutreachCountryOption, true>\n multiple\n options={countries}\n value={selectedCountries}\n getOptionLabel={(option) => option.name}\n isOptionEqualToValue={(option, value) => option.code === value.code}\n onChange={(_event, next) => {\n clearIssues('countries')\n setForm((previous) => ({\n ...previous,\n allowedCountries: next.map((option) => option.code),\n }))\n }}\n renderValue={(value, getItemProps) =>\n value.map((option, index) => {\n const { key, ...item } = getItemProps({ index })\n return (\n <Chip key={key} size=\"small\" label={option.name} {...item} />\n )\n })\n }\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Countries\"\n error={Boolean(fieldIssue('allowedCountries'))}\n helperText={fieldIssue('allowedCountries')}\n />\n )}\n />\n </Stack>\n </CardDisplay>\n\n {/* Its own list, saved as it is edited (AGL-3244): it has no Save. */}\n <OutreachDoNotContactDomainsCard orgId={orgId} />\n </Stack>\n )\n}\nOutreachComplianceSection.displayName = 'OutreachComplianceSection'\n\nexport default OutreachComplianceSection\n"],"names":["pluginDocsHelp","CardDisplay","useSnackbar","Alert","Autocomplete","Button","Chip","Paper","Stack","TextField","Typography","useEffect","useMemo","useRef","useState","composeOutreachFooter","outreachComplianceSettingsEqual","outreachCountryLabel","outreachCountryOptions","OUTREACH_BRAND_NAME_MAX","OUTREACH_LEGAL_NAME_MAX","OUTREACH_POSTAL_ADDRESS_MAX","validateOutreachComplianceSettings","OutreachDoNotContactDomainsCard","OutreachLoading","OutreachLoadProblem","OutreachRouteError","useOutreachApi","useOutreachComplianceSettings","OUTREACH_POSTAL_ADDRESS_HELP","EMPTY","legalName","brandName","postalAddress","allowedCountries","CARDS","identity","fields","saved","countries","pickCard","settings","card","Object","fromEntries","map","field","cardEqual","a","b","OutreachComplianceSection","props","fieldIssue","orgId","api","loaded","enqueueSnackbar","form","setForm","saving","setSaving","serverIssues","setServerIssues","stored","previousStored","before","current","next","keys","normalized","issues","find","issue","message","candidate","cardDirty","cardBlocked","some","includes","footer","set","value","previous","clearIssues","discard","save","answer","saveSettings","reload","changed","variant","error","length","filter","allowDuplicate","status","label","what","onRetry","selectedCountries","code","option","name","cardActions","dirty","direction","spacing","size","disabled","onClick","header","help","anchor","HeaderProps","action","contentGutterX","contentGutterY","color","onChange","event","target","Boolean","helperText","slotProps","htmlInput","maxLength","fullWidth","multiline","minRows","sx","p","whiteSpace","wordBreak","data-testid","severity","multiple","options","getOptionLabel","isOptionEqualToValue","_event","renderValue","getItemProps","index","key","item","renderInput","params","displayName"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { type OutreachEnrollPreviewPerson } from '../model/outreach-api';
1
+ import { type OutreachEnrollGateway, type OutreachEnrollPreviewPerson } from '../model/outreach-api';
2
2
  import { type OutreachAttestationKind, type OutreachSequence } from '../model/outreach.types';
3
3
  import { type OutreachCuratedDraftState } from './curated-step-editor';
4
4
  import type { OutreachApi } from './use-outreach-api';
@@ -12,6 +12,12 @@ export interface OutreachEnrollDialogProps {
12
12
  uid: string | null;
13
13
  api: OutreachApi;
14
14
  }
15
+ /**
16
+ * Whether the gateway in front of this person refused the sender this week
17
+ * (AGL-3326): the person is shown, and left un-ticked, rather than blocked
18
+ * — the founder decides, and ticking them is the decision.
19
+ */
20
+ export declare function outreachGatewayRefusedThisWeek(gateway: OutreachEnrollGateway | null | undefined): boolean;
15
21
  /** Where one person's curated drafts stand (AGL-3324). */
16
22
  type Curation = {
17
23
  status: 'idle';
@@ -20,6 +20,7 @@ import { pluginDocsHelp } from "@aglyn/aglyn";
20
20
  import { HelpTip } from "@aglyn/shared-ui-jsx";
21
21
  import { Alert, Button, Checkbox, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, FormGroup, List, ListItem, ListItemButton, ListItemIcon, ListItemText, MenuItem, Paper, Stack, Tab, Tabs, TextField, Typography, useMediaQuery, useTheme } from "@mui/material";
22
22
  import { useMemo, useState } from "react";
23
+ import { isOutreachGatewayFronted, outreachGatewayChip } from "../engine/mail-gateway.js";
23
24
  import { OUTREACH_ENROLL_BATCH_MAX } from "../model/outreach-api.js";
24
25
  import { OUTREACH_ATTESTATION_LABELS, OUTREACH_PERSONAL_LINE_MAX } from "../model/outreach.types.js";
25
26
  import { outreachEmailStepLabel } from "./curate-step-dialog.js";
@@ -36,6 +37,20 @@ const STATUS_COLORS = {
36
37
  needs_confirmation: 'warning',
37
38
  blocked: 'default'
38
39
  };
40
+ /** The gateway chip's color by its tone (AGL-3326). */ const GATEWAY_TONE_COLORS = {
41
+ refused: 'error',
42
+ delivered: 'success',
43
+ neutral: 'default',
44
+ blocked: 'default'
45
+ };
46
+ /**
47
+ * Whether the gateway in front of this person refused the sender this week
48
+ * (AGL-3326): the person is shown, and left un-ticked, rather than blocked
49
+ * — the founder decides, and ticking them is the decision.
50
+ */ export function outreachGatewayRefusedThisWeek(gateway) {
51
+ var _ref;
52
+ return ((_ref = gateway == null ? void 0 : gateway.blocked7) != null ? _ref : 0) > 0;
53
+ }
39
54
  /** The drafts the rep used, as the enroll and the preview carry them. */ function usedOverrides(supplied) {
40
55
  var _ref;
41
56
  return Object.values((_ref = supplied == null ? void 0 : supplied.drafts) != null ? _ref : {}).map(outreachDraftToOverride).filter((override)=>override !== null);
@@ -125,7 +140,9 @@ const STATUS_COLORS = {
125
140
  setSupplied(Object.fromEntries(answer.people.map((person)=>[
126
141
  person.personId,
127
142
  {
128
- include: person.status !== 'blocked',
143
+ // A person whose gateway refused the sender this week starts
144
+ // un-ticked (AGL-3326); the chip beside them says why.
145
+ include: person.status !== 'blocked' && !outreachGatewayRefusedThisWeek(person.gateway),
129
146
  personalLine: '',
130
147
  attestations: []
131
148
  }
@@ -661,11 +678,29 @@ const STATUS_COLORS = {
661
678
  OutreachEnrollDialog.displayName = 'OutreachEnrollDialog';
662
679
  function summary(answer) {
663
680
  const count = (status)=>answer.people.filter((person)=>person.status === status).length;
664
- return `${count('eligible')} eligible · ${count('needs_confirmation')} need you · ${count('blocked')} blocked`;
681
+ // The people behind a security gateway (AGL-3326): a Barracuda, a
682
+ // Proofpoint or a Mimecast refuses a sender for a whole company, and a
683
+ // LinkedIn note first is the cheaper first touch there.
684
+ const fronted = answer.people.filter((person)=>{
685
+ var _person_gateway;
686
+ return person.status !== 'blocked' && isOutreachGatewayFronted((_person_gateway = person.gateway) == null ? void 0 : _person_gateway.gateway);
687
+ }).length;
688
+ return `${count('eligible')} eligible · ${count('needs_confirmation')} need you · ${count('blocked')} blocked` + (fronted ? ` · ${fronted} gateway-fronted — LinkedIn-first?` : '');
689
+ }
690
+ /** The Check-people chip for one person's mail gateway (AGL-3326), or nothing. */ function GatewayChip(props) {
691
+ const chip = outreachGatewayChip(props.gateway);
692
+ if (!chip) return null;
693
+ return /*#__PURE__*/ _jsx(Chip, {
694
+ size: "small",
695
+ variant: chip.tone === 'neutral' ? 'outlined' : 'filled',
696
+ color: GATEWAY_TONE_COLORS[chip.tone],
697
+ label: chip.label,
698
+ "aria-label": `Mail gateway for ${props.name}: ${chip.label}`
699
+ });
665
700
  }
666
701
  function PersonRow(props) {
667
702
  var _ref, _ref1, _ref2, _ref3;
668
- var _props_stepTest, _props_stepTest1, _props_stepTest2, _props_stepTest3, _props_emailPreview, _props_emailPreview1, _props_emailPreview2;
703
+ var _person_gateway, _person_gateway1, _person_gateway2, _props_stepTest, _props_stepTest1, _props_stepTest2, _props_stepTest3, _props_emailPreview, _props_emailPreview1, _props_emailPreview2;
669
704
  const { person, supplied, disabled } = props;
670
705
  const name = person.name || person.email || person.personId;
671
706
  const line = (_ref = supplied == null ? void 0 : supplied.personalLine) != null ? _ref : '';
@@ -731,6 +766,10 @@ function PersonRow(props) {
731
766
  variant: "outlined",
732
767
  label: "Cold"
733
768
  }) : null,
769
+ /*#__PURE__*/ _jsx(GatewayChip, {
770
+ gateway: person.gateway,
771
+ name: name
772
+ }),
734
773
  /*#__PURE__*/ _jsx(Chip, {
735
774
  size: "small",
736
775
  label: STATUS_LABELS[person.status],
@@ -766,6 +805,11 @@ function PersonRow(props) {
766
805
  }),
767
806
  label: "Enroll this person"
768
807
  }),
808
+ (supplied == null ? void 0 : supplied.include) === false && outreachGatewayRefusedThisWeek(person.gateway) ? /*#__PURE__*/ _jsx(Typography, {
809
+ variant: "caption",
810
+ color: "text.secondary",
811
+ children: `Left un-ticked: their mail gateway refused ${((_person_gateway = person.gateway) == null ? void 0 : _person_gateway.blocked7) === 1 ? 'an email' : `${(_person_gateway1 = person.gateway) == null ? void 0 : _person_gateway1.blocked7} emails`} from this organization this week. Tick them to enroll anyway${((_person_gateway2 = person.gateway) == null ? void 0 : _person_gateway2.hold) ? ' — the email will go, and it is your call' : ''}.`
812
+ }) : null,
769
813
  (supplied == null ? void 0 : supplied.include) === false ? null : /*#__PURE__*/ _jsxs(_Fragment, {
770
814
  children: [
771
815
  /*#__PURE__*/ _jsx(TextField, {