@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.
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-marketing",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.283",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,21 +25,21 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
39
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
40
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
41
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
42
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.283",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.283",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.283",
|
|
31
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.283",
|
|
32
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.283",
|
|
33
|
+
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.283",
|
|
34
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.283",
|
|
35
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.283",
|
|
36
|
+
"@aglyn/shared-util-email": "1.0.0-beta.283",
|
|
37
|
+
"@aglyn/shared-util-http": "1.0.0-beta.283",
|
|
38
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.283",
|
|
39
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.283",
|
|
40
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.283",
|
|
41
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.283",
|
|
42
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.283",
|
|
43
43
|
"@swc/helpers": "0.5.23"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
@@ -20,11 +20,14 @@ import { buildRoute, checkQuota, Route } from "@aglyn/aglyn";
|
|
|
20
20
|
import { EMAIL_MERGE_TAGS, emailMessageMode, emailPlainTextState } from "@aglyn/aglyn/app-utils/recipient-email-render";
|
|
21
21
|
import { useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
22
22
|
import QuotaReadoutComponent from "@aglyn/shared-ui-jsx/components/quota-readout.component";
|
|
23
|
+
import { BlockedControl } from "@aglyn/shared-ui-jsx/components/blocked-control.component";
|
|
24
|
+
import { PlanUpsellNotice } from "@aglyn/shared-ui-jsx/components/plan-upsell-notice.component";
|
|
23
25
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
24
26
|
import { Alert, Box, Button, Chip, Divider, MenuItem, Stack, TextField, Typography } from "@mui/material";
|
|
25
27
|
import { collection, doc, limit, query } from "firebase/firestore";
|
|
26
28
|
import { CampaignDesignCreate, CampaignSenderEditor, CampaignTopicSelect } from "./campaign-email-zones.js";
|
|
27
29
|
import { useCampaignSendApi } from "./use-campaign-send-api.js";
|
|
30
|
+
import { CAMPAIGN_PLAN_CAP_REASON, campaignPlanLock } from "../model/campaign-plan-lock.js";
|
|
28
31
|
import { useSendingApi } from "@aglyn/tenant-feature-instance/hooks/use-sending-identity-api";
|
|
29
32
|
import { describeCallFailure } from "@aglyn/shared-util-http/authorized-token";
|
|
30
33
|
// By its own path: the one sentence, without the send path behind the barrel.
|
|
@@ -450,7 +453,8 @@ export function CampaignComposer(props) {
|
|
|
450
453
|
// the click.
|
|
451
454
|
return setPreview({
|
|
452
455
|
error: String((_ref11 = payload == null ? void 0 : payload.error) != null ? _ref11 : ''),
|
|
453
|
-
blocking: response.status === 409
|
|
456
|
+
blocking: response.status === 409,
|
|
457
|
+
planCapped: (payload == null ? void 0 : payload.reason) === CAMPAIGN_PLAN_CAP_REASON
|
|
454
458
|
});
|
|
455
459
|
}
|
|
456
460
|
setPreview({
|
|
@@ -1044,6 +1048,32 @@ export function CampaignComposer(props) {
|
|
|
1044
1048
|
org,
|
|
1045
1049
|
campaignSendsUsed
|
|
1046
1050
|
]);
|
|
1051
|
+
/*
|
|
1052
|
+
* THE PLAN'S ANSWER, BEFORE SEND.
|
|
1053
|
+
*
|
|
1054
|
+
* A plan without campaign email used to compose, schedule and save freely,
|
|
1055
|
+
* and learn the answer only when the processor marked the send `failed`.
|
|
1056
|
+
* `campaignPlanLock` reads the same limit and counter the server claims
|
|
1057
|
+
* against, so the lock and the refusal agree. Nothing locks while the plan
|
|
1058
|
+
* is still loading: `checkQuota(undefined)` answers with Free's zero.
|
|
1059
|
+
*
|
|
1060
|
+
* An audience bigger than what is left this month is only known from the
|
|
1061
|
+
* dry run, which refuses it with the plan-cap reason rather than a count;
|
|
1062
|
+
* that refusal's own sentence (it names the plan) becomes the notice.
|
|
1063
|
+
*/ const previewSendable = preview && !('error' in preview) ? preview.sendable : null;
|
|
1064
|
+
const planLock = useMemo(()=>orgReady ? campaignPlanLock({
|
|
1065
|
+
limit: quotaLimit,
|
|
1066
|
+
used: campaignSendsUsed,
|
|
1067
|
+
sendable: previewSendable
|
|
1068
|
+
}) : null, [
|
|
1069
|
+
orgReady,
|
|
1070
|
+
quotaLimit,
|
|
1071
|
+
campaignSendsUsed,
|
|
1072
|
+
previewSendable
|
|
1073
|
+
]);
|
|
1074
|
+
const previewPlanCapped = Boolean(preview && 'error' in preview && preview.planCapped);
|
|
1075
|
+
const planCapReason = planLock ? planLock.reason : previewPlanCapped && preview && 'error' in preview ? preview.error : null;
|
|
1076
|
+
const upgradeHref = orgSlug ? `/${orgSlug}/billing#plans` : undefined;
|
|
1047
1077
|
return /*#__PURE__*/ _jsxs(Stack, {
|
|
1048
1078
|
spacing: 1.5,
|
|
1049
1079
|
children: [
|
|
@@ -1052,6 +1082,15 @@ export function CampaignComposer(props) {
|
|
|
1052
1082
|
color: "text.secondary",
|
|
1053
1083
|
children: 'Send an update to your leads or site members. Every email ' + 'carries an unsubscribe link; monthly sends are capped by ' + 'your plan.'
|
|
1054
1084
|
}),
|
|
1085
|
+
planLock ? /*#__PURE__*/ _jsx(PlanUpsellNotice, {
|
|
1086
|
+
planLabel: planLock.planLabel,
|
|
1087
|
+
upgradeHref: upgradeHref,
|
|
1088
|
+
children: planLock.notice
|
|
1089
|
+
}) : previewPlanCapped && planCapReason ? /*#__PURE__*/ _jsx(PlanUpsellNotice, {
|
|
1090
|
+
planLabel: undefined,
|
|
1091
|
+
upgradeHref: upgradeHref,
|
|
1092
|
+
children: planCapReason
|
|
1093
|
+
}) : null,
|
|
1055
1094
|
/*#__PURE__*/ _jsxs(Stack, {
|
|
1056
1095
|
direction: "row",
|
|
1057
1096
|
spacing: 1,
|
|
@@ -1562,20 +1601,24 @@ export function CampaignComposer(props) {
|
|
|
1562
1601
|
alignItems: 'center'
|
|
1563
1602
|
},
|
|
1564
1603
|
children: [
|
|
1565
|
-
/*#__PURE__*/ _jsx(
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1604
|
+
/*#__PURE__*/ _jsx(BlockedControl, {
|
|
1605
|
+
blocked: Boolean(planCapReason),
|
|
1606
|
+
reason: planCapReason != null ? planCapReason : '',
|
|
1607
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
1608
|
+
variant: "contained",
|
|
1609
|
+
color: "primary",
|
|
1610
|
+
disabled: busy || !subject.trim() || !messageReady || /*
|
|
1611
|
+
* REFUSED BEFORE THE CLICK, not after it.
|
|
1612
|
+
*
|
|
1613
|
+
* The send would answer 409 anyway — that boundary holds whatever
|
|
1614
|
+
* this component does. What a disabled button buys is that the
|
|
1615
|
+
* merchant reads the reason instead of pressing Send and being
|
|
1616
|
+
* told; the two are the same information, and only one of them
|
|
1617
|
+
* arrives while there is still something to do about it.
|
|
1618
|
+
*/ Boolean(preview && 'error' in preview && preview.blocking) || Boolean(planCapReason),
|
|
1619
|
+
onClick: handleSend,
|
|
1620
|
+
children: busy ? 'Working…' : sendAt ? 'Schedule campaign' : 'Send campaign'
|
|
1621
|
+
})
|
|
1579
1622
|
}),
|
|
1580
1623
|
campaignId ? /*#__PURE__*/ _jsx(Button, {
|
|
1581
1624
|
size: "small",
|