@aglyn/plugins-marketing 1.0.0-beta.277 → 1.0.0-beta.282
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +16 -16
- package/src/lib/components/campaign-composer.js +58 -15
- package/src/lib/components/campaign-composer.js.map +1 -1
- package/src/lib/components/email-detail.js +70 -27
- package/src/lib/components/email-detail.js.map +1 -1
- package/src/lib/model/campaign-plan-lock.d.ts +60 -0
- package/src/lib/model/campaign-plan-lock.js +77 -0
- package/src/lib/model/campaign-plan-lock.js.map +1 -0
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/server/campaign-send.d.ts +12 -2
- package/src/lib/server/campaign-send.js +104 -27
- package/src/lib/server/campaign-send.js.map +1 -1
|
@@ -16,20 +16,23 @@
|
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
18
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
|
-
import { PageHeaderRecord, pluginDocsHelp } from "@aglyn/aglyn";
|
|
19
|
+
import { checkQuota, PageHeaderRecord, pluginDocsHelp } from "@aglyn/aglyn";
|
|
20
20
|
import { pluginRecordHref } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
|
|
21
21
|
import { mdiCalendarClockOutline, mdiCloseCircleOutline, mdiDeleteOutline, mdiPencilOutline } from "@aglyn/shared-data-mdi";
|
|
22
22
|
import { AppLink, CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
23
23
|
import RowActionsMenu from "@aglyn/shared-ui-jsx/components/row-actions-menu.component";
|
|
24
24
|
import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
|
|
25
25
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
26
|
-
import { useFirestore, useFirestoreDoc } from "@aglyn/tenant-feature-instance";
|
|
26
|
+
import { useConsoleHostRoute, useFirestore, useFirestoreDoc, useOrgPlan } from "@aglyn/tenant-feature-instance";
|
|
27
|
+
import { BlockedControl } from "@aglyn/shared-ui-jsx/components/blocked-control.component";
|
|
28
|
+
import { PlanUpsellNotice } from "@aglyn/shared-ui-jsx/components/plan-upsell-notice.component";
|
|
27
29
|
import { Alert, Button, Chip, Divider, Stack, TableBody, TableCell, TableHead, TableRow, Typography } from "@mui/material";
|
|
28
30
|
import { doc } from "firebase/firestore";
|
|
29
31
|
import { useRouter } from "next/navigation";
|
|
30
32
|
import { useCallback, useMemo, useState } from "react";
|
|
31
33
|
import { sendLinkReport } from "@aglyn/shared-ui-email-campaigns/model/send-report";
|
|
32
34
|
import { campaignReport } from "../model/campaign-report.js";
|
|
35
|
+
import { campaignPlanLock } from "../model/campaign-plan-lock.js";
|
|
33
36
|
import { campaignHeldForReviewNotice, campaignSendDisplay, CAMPAIGN_SEND_CONTAINER_FIELD } from "../model/campaign-container.js";
|
|
34
37
|
import { emailAudienceLabel, emailIsUnsent, emailNeverSent, emailSendTimeMs, emailSentAs } from "../model/email-record.js";
|
|
35
38
|
import { emailPlainTextState } from "@aglyn/aglyn/app-utils/recipient-email-render";
|
|
@@ -84,7 +87,7 @@ const emailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
84
87
|
* written before sends named their site cannot be acted on from there, so it
|
|
85
88
|
* says so instead of guessing a site.
|
|
86
89
|
*/ export function EmailDetail(props) {
|
|
87
|
-
var _props_hostId, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _sentAs_fromName, _sentAs_replyTo,
|
|
90
|
+
var _props_hostId, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _sentAs_fromName, _sentAs_replyTo, _ref9, _ref10;
|
|
88
91
|
const { emailId, basePath } = props;
|
|
89
92
|
// The sibling hub: a campaign's page belongs to the Marketing console.
|
|
90
93
|
const marketingHub = useMarketingHubPath();
|
|
@@ -241,27 +244,51 @@ const emailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
241
244
|
const { confirm } = useConfirmationContext();
|
|
242
245
|
const { enqueueSnackbar } = useSnackbar();
|
|
243
246
|
const [sendingMore, setSendingMore] = useState(false);
|
|
247
|
+
/*
|
|
248
|
+
* THE PLAN, SAID BEFORE "Send now" OR "Send to more recipients".
|
|
249
|
+
*
|
|
250
|
+
* A plan without campaign email is known from the plan alone, so it locks
|
|
251
|
+
* both buttons the moment the page loads, exactly as it locks the
|
|
252
|
+
* composer's Send. A month whose allowance is spent, or an audience bigger
|
|
253
|
+
* than what is left, is only known from the dry run each button asks for
|
|
254
|
+
* first; that answer carries the same `planLock`, and it replaces the
|
|
255
|
+
* confirmation rather than arriving as a refusal after it.
|
|
256
|
+
*/ const { org, ready: orgReady } = useOrgPlan(hostId != null ? hostId : undefined);
|
|
257
|
+
const { orgSlug } = useConsoleHostRoute(hostId);
|
|
258
|
+
const [dryRunPlanLock, setDryRunPlanLock] = useState(null);
|
|
259
|
+
const planLock = useMemo(()=>dryRunPlanLock != null ? dryRunPlanLock : orgReady ? campaignPlanLock({
|
|
260
|
+
limit: checkQuota(org, 'emailSendsPerMonth', 0).limit,
|
|
261
|
+
used: 0
|
|
262
|
+
}) : null, [
|
|
263
|
+
dryRunPlanLock,
|
|
264
|
+
org,
|
|
265
|
+
orgReady
|
|
266
|
+
]);
|
|
267
|
+
const upgradeHref = orgSlug ? `/${orgSlug}/billing#plans` : undefined;
|
|
244
268
|
const handleSendToMore = useCallback(async ()=>{
|
|
245
269
|
if (sendingMore) return;
|
|
246
270
|
setSendingMore(true);
|
|
247
271
|
try {
|
|
248
272
|
var _ref, _ref1, _ref2;
|
|
249
|
-
var _counted_payload, _counted_payload1, _result_payload;
|
|
273
|
+
var _counted_payload, _counted_payload1, _counted_payload2, _result_payload;
|
|
250
274
|
const counted = await campaignSendApi({
|
|
251
275
|
action: 'followUp',
|
|
252
276
|
campaignId: emailId,
|
|
253
277
|
dryRun: true
|
|
254
278
|
});
|
|
279
|
+
if ((_counted_payload = counted.payload) == null ? void 0 : _counted_payload.planLock) {
|
|
280
|
+
return void setDryRunPlanLock(counted.payload.planLock);
|
|
281
|
+
}
|
|
255
282
|
if (!counted.response.ok) {
|
|
256
283
|
var _ref3;
|
|
257
|
-
var
|
|
258
|
-
return void enqueueSnackbar((_ref3 = (
|
|
284
|
+
var _counted_payload3;
|
|
285
|
+
return void enqueueSnackbar((_ref3 = (_counted_payload3 = counted.payload) == null ? void 0 : _counted_payload3.error) != null ? _ref3 : 'This email cannot be sent again', {
|
|
259
286
|
variant: 'warning',
|
|
260
287
|
allowDuplicate: true
|
|
261
288
|
});
|
|
262
289
|
}
|
|
263
|
-
const reaching = Number((_ref = (
|
|
264
|
-
const already = Number((_ref1 = (
|
|
290
|
+
const reaching = Number((_ref = (_counted_payload1 = counted.payload) == null ? void 0 : _counted_payload1.sendable) != null ? _ref : 0);
|
|
291
|
+
const already = Number((_ref1 = (_counted_payload2 = counted.payload) == null ? void 0 : _counted_payload2.alreadyReached) != null ? _ref1 : 0);
|
|
265
292
|
if (!reaching) {
|
|
266
293
|
return void enqueueSnackbar('Everyone in this audience already has this email', {
|
|
267
294
|
variant: 'info',
|
|
@@ -372,19 +399,22 @@ const emailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
372
399
|
* describe, and the failure branches below return rather than reaching it.
|
|
373
400
|
*/ let reaching = null;
|
|
374
401
|
try {
|
|
402
|
+
var _counted_payload;
|
|
375
403
|
const counted = await campaignSendApi({
|
|
376
404
|
action: 'sendNow',
|
|
377
405
|
campaignId: emailId,
|
|
378
406
|
dryRun: true
|
|
379
407
|
});
|
|
380
|
-
if (counted.
|
|
408
|
+
if ((_counted_payload = counted.payload) == null ? void 0 : _counted_payload.planLock) {
|
|
409
|
+
setDryRunPlanLock(counted.payload.planLock);
|
|
410
|
+
} else if (counted.response.ok) {
|
|
381
411
|
var _ref, _ref1;
|
|
382
|
-
var
|
|
383
|
-
reaching = Number((_ref = (_ref1 = (
|
|
412
|
+
var _counted_payload1, _counted_payload2;
|
|
413
|
+
reaching = Number((_ref = (_ref1 = (_counted_payload1 = counted.payload) == null ? void 0 : _counted_payload1.sendable) != null ? _ref1 : (_counted_payload2 = counted.payload) == null ? void 0 : _counted_payload2.sent) != null ? _ref : 0);
|
|
384
414
|
} else {
|
|
385
415
|
var _ref2;
|
|
386
|
-
var
|
|
387
|
-
enqueueSnackbar((_ref2 = (
|
|
416
|
+
var _counted_payload3;
|
|
417
|
+
enqueueSnackbar((_ref2 = (_counted_payload3 = counted.payload) == null ? void 0 : _counted_payload3.error) != null ? _ref2 : 'This email cannot be sent', {
|
|
388
418
|
variant: 'warning',
|
|
389
419
|
allowDuplicate: true
|
|
390
420
|
});
|
|
@@ -623,12 +653,16 @@ const emailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
623
653
|
* no primary act at all — it was withdrawn deliberately, and offering a way
|
|
624
654
|
* to un-withdraw it would be a resurrect path this model does not have —
|
|
625
655
|
* and neither does one that is mid-send.
|
|
626
|
-
*/ const primaryAction = held ? null : (draft || scheduled) && !midFlight ? /*#__PURE__*/ _jsx(
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
656
|
+
*/ const primaryAction = held ? null : (draft || scheduled) && !midFlight ? /*#__PURE__*/ _jsx(BlockedControl, {
|
|
657
|
+
blocked: Boolean(planLock),
|
|
658
|
+
reason: (_ref7 = planLock == null ? void 0 : planLock.reason) != null ? _ref7 : '',
|
|
659
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
660
|
+
size: "small",
|
|
661
|
+
variant: "contained",
|
|
662
|
+
disabled: Boolean(busy) || Boolean(planLock),
|
|
663
|
+
onClick: ()=>void handleSendNow(),
|
|
664
|
+
children: busy === 'sendNow' ? 'Checking…' : 'Send now'
|
|
665
|
+
})
|
|
632
666
|
}) : midFlight ? /*
|
|
633
667
|
A CAMPAIGN THAT IS ALREADY GOING OUT HAS ONE ACT: STOPPING IT.
|
|
634
668
|
|
|
@@ -645,12 +679,16 @@ const emailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
645
679
|
disabled: Boolean(busy),
|
|
646
680
|
onClick: ()=>void handleCancel(),
|
|
647
681
|
children: 'Stop sending'
|
|
648
|
-
}) : state === 'sent' ? /*#__PURE__*/ _jsx(
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
682
|
+
}) : state === 'sent' ? /*#__PURE__*/ _jsx(BlockedControl, {
|
|
683
|
+
blocked: Boolean(planLock),
|
|
684
|
+
reason: (_ref8 = planLock == null ? void 0 : planLock.reason) != null ? _ref8 : '',
|
|
685
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
686
|
+
size: "small",
|
|
687
|
+
variant: "contained",
|
|
688
|
+
disabled: sendingMore || Boolean(planLock),
|
|
689
|
+
onClick: ()=>void handleSendToMore(),
|
|
690
|
+
children: sendingMore ? 'Checking…' : 'Send to more recipients'
|
|
691
|
+
})
|
|
654
692
|
}) : null;
|
|
655
693
|
/**
|
|
656
694
|
* WHERE THIS EMAIL IS WRITTEN, which is no longer this page.
|
|
@@ -756,6 +794,11 @@ const emailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
756
794
|
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
757
795
|
spacing: 3,
|
|
758
796
|
children: [
|
|
797
|
+
planLock && primaryAction && !midFlight && !siteless ? /*#__PURE__*/ _jsx(PlanUpsellNotice, {
|
|
798
|
+
planLabel: planLock.planLabel,
|
|
799
|
+
upgradeHref: upgradeHref,
|
|
800
|
+
children: planLock.notice
|
|
801
|
+
}) : null,
|
|
759
802
|
report.caveats.map((caveat)=>/*#__PURE__*/ _jsx(Alert, {
|
|
760
803
|
severity: "info",
|
|
761
804
|
children: caveat.message
|
|
@@ -916,7 +959,7 @@ const emailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
916
959
|
align: "right",
|
|
917
960
|
children: templateHref ? /*#__PURE__*/ _jsx(AppLink, {
|
|
918
961
|
href: templateHref,
|
|
919
|
-
children: (
|
|
962
|
+
children: (_ref9 = template == null ? void 0 : template.displayName) != null ? _ref9 : 'Untitled template'
|
|
920
963
|
}) : /*#__PURE__*/ _jsx(Typography, {
|
|
921
964
|
variant: "body2",
|
|
922
965
|
color: "text.secondary",
|
|
@@ -1191,7 +1234,7 @@ const emailDocsHelp = pluginDocsHelp('emailCampaigns', {
|
|
|
1191
1234
|
nodes: templateVersion == null ? void 0 : templateVersion.nodes,
|
|
1192
1235
|
loading: template === undefined || templateVersion === undefined,
|
|
1193
1236
|
subject: subject,
|
|
1194
|
-
preheader: String((
|
|
1237
|
+
preheader: String((_ref10 = template == null ? void 0 : template.emailPreheader) != null ? _ref10 : ''),
|
|
1195
1238
|
emptyMessage: 'The template this email was built from is empty or has ' + 'been deleted, so there is nothing to draw.',
|
|
1196
1239
|
note: 'The template as it stands today. The mail itself is ' + 'rendered per recipient at send time and not kept, so a ' + 'template edited since this went out previews as it is now.'
|
|
1197
1240
|
}) : /*#__PURE__*/ _jsx(EmailDesignPreview, {
|