@aglyn/plugins-marketing 1.0.0-beta.278 → 1.0.0-beta.283

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.
@@ -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, _ref7, _ref8;
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 _counted_payload2;
258
- return void enqueueSnackbar((_ref3 = (_counted_payload2 = counted.payload) == null ? void 0 : _counted_payload2.error) != null ? _ref3 : 'This email cannot be sent again', {
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 = (_counted_payload = counted.payload) == null ? void 0 : _counted_payload.sendable) != null ? _ref : 0);
264
- const already = Number((_ref1 = (_counted_payload1 = counted.payload) == null ? void 0 : _counted_payload1.alreadyReached) != null ? _ref1 : 0);
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.response.ok) {
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 _counted_payload, _counted_payload1;
383
- reaching = Number((_ref = (_ref1 = (_counted_payload = counted.payload) == null ? void 0 : _counted_payload.sendable) != null ? _ref1 : (_counted_payload1 = counted.payload) == null ? void 0 : _counted_payload1.sent) != null ? _ref : 0);
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 _counted_payload2;
387
- enqueueSnackbar((_ref2 = (_counted_payload2 = counted.payload) == null ? void 0 : _counted_payload2.error) != null ? _ref2 : 'This email cannot be sent', {
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(Button, {
627
- size: "small",
628
- variant: "contained",
629
- disabled: Boolean(busy),
630
- onClick: ()=>void handleSendNow(),
631
- children: busy === 'sendNow' ? 'Checking…' : 'Send now'
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(Button, {
649
- size: "small",
650
- variant: "contained",
651
- disabled: sendingMore,
652
- onClick: ()=>void handleSendToMore(),
653
- children: sendingMore ? 'Checking…' : 'Send to more recipients'
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: (_ref7 = template == null ? void 0 : template.displayName) != null ? _ref7 : 'Untitled template'
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((_ref8 = template == null ? void 0 : template.emailPreheader) != null ? _ref8 : ''),
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, {