@carlonicora/nextjs-jsonapi 5.2.0 → 5.4.0

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 (237) hide show
  1. package/dist/{BlockNoteEditor-EGY4ZAAM.js → BlockNoteEditor-4YCAIEPT.js} +23 -22
  2. package/dist/BlockNoteEditor-4YCAIEPT.js.map +1 -0
  3. package/dist/{BlockNoteEditor-2QJVK2HH.mjs → BlockNoteEditor-YQL2SGFR.mjs} +8 -7
  4. package/dist/BlockNoteEditor-YQL2SGFR.mjs.map +1 -0
  5. package/dist/{BlockNoteViewer-6BQXJ4TB.mjs → BlockNoteViewer-AEJKJ27Q.mjs} +2 -2
  6. package/dist/{BlockNoteViewer-I2MMFH3S.js → BlockNoteViewer-YL3J7J2Y.js} +3 -3
  7. package/dist/{BlockNoteViewer-I2MMFH3S.js.map → BlockNoteViewer-YL3J7J2Y.js.map} +1 -1
  8. package/dist/BlogCategoryFields-DO48vOTE.d.mts +14 -0
  9. package/dist/BlogCategoryFields-DO48vOTE.d.ts +14 -0
  10. package/dist/BlogPostInterface-BB_4wcDR.d.mts +124 -0
  11. package/dist/BlogPostInterface-bWAT1rfR.d.ts +124 -0
  12. package/dist/billing/index.js +441 -441
  13. package/dist/billing/index.mjs +3 -3
  14. package/dist/blog-public.types-BbrzsC1c.d.mts +30 -0
  15. package/dist/blog-public.types-BbrzsC1c.d.ts +30 -0
  16. package/dist/{chunk-3VEQ5JZJ.js → chunk-3NLAXVIN.js} +860 -849
  17. package/dist/{chunk-3VEQ5JZJ.js.map → chunk-3NLAXVIN.js.map} +1 -1
  18. package/dist/{chunk-YMUPTXOT.mjs → chunk-3YW35YHQ.mjs} +1 -101
  19. package/dist/chunk-3YW35YHQ.mjs.map +1 -0
  20. package/dist/{chunk-EXICQC4A.js → chunk-5PPG6PL7.js} +3 -3
  21. package/dist/{chunk-EXICQC4A.js.map → chunk-5PPG6PL7.js.map} +1 -1
  22. package/dist/{chunk-NPKFMRH5.mjs → chunk-6DC2WVVB.mjs} +2 -2
  23. package/dist/{chunk-2Z3ZCYXX.mjs → chunk-EUKTBOI2.mjs} +19 -8
  24. package/dist/chunk-EUKTBOI2.mjs.map +1 -0
  25. package/dist/{chunk-THKYH65N.js → chunk-KSYQEKJX.js} +11 -111
  26. package/dist/chunk-KSYQEKJX.js.map +1 -0
  27. package/dist/{chunk-S5MQDWWC.js → chunk-NFBOSPNJ.js} +1451 -176
  28. package/dist/chunk-NFBOSPNJ.js.map +1 -0
  29. package/dist/{chunk-JEZQTZQ4.mjs → chunk-QMYQRPQC.mjs} +2 -2
  30. package/dist/{chunk-HNOWSFM5.mjs → chunk-WC63PJSC.mjs} +1278 -3
  31. package/dist/chunk-WC63PJSC.mjs.map +1 -0
  32. package/dist/chunk-XCHSX3VC.mjs +107 -0
  33. package/dist/chunk-XCHSX3VC.mjs.map +1 -0
  34. package/dist/{chunk-75MLT74B.js → chunk-YNCQSWQW.js} +8 -8
  35. package/dist/{chunk-75MLT74B.js.map → chunk-YNCQSWQW.js.map} +1 -1
  36. package/dist/chunk-YW4ZCEWB.js +107 -0
  37. package/dist/chunk-YW4ZCEWB.js.map +1 -0
  38. package/dist/client/index.d.mts +6 -39
  39. package/dist/client/index.d.ts +6 -39
  40. package/dist/client/index.js +4 -4
  41. package/dist/client/index.mjs +3 -3
  42. package/dist/components/index.d.mts +15 -4
  43. package/dist/components/index.d.ts +15 -4
  44. package/dist/components/index.js +4 -4
  45. package/dist/components/index.mjs +3 -3
  46. package/dist/contexts/index.js +4 -4
  47. package/dist/contexts/index.mjs +3 -3
  48. package/dist/core/index.d.mts +400 -3
  49. package/dist/core/index.d.ts +400 -3
  50. package/dist/core/index.js +64 -2
  51. package/dist/core/index.js.map +1 -1
  52. package/dist/core/index.mjs +63 -1
  53. package/dist/email-automation-enrollment.fields-BZvF14qx.d.mts +82 -0
  54. package/dist/email-automation-enrollment.fields-Cp3VMzFq.d.ts +82 -0
  55. package/dist/email-automations/index.css +30 -0
  56. package/dist/email-automations/index.css.map +1 -0
  57. package/dist/email-automations/index.d.mts +303 -0
  58. package/dist/email-automations/index.d.ts +303 -0
  59. package/dist/email-automations/index.js +1705 -0
  60. package/dist/email-automations/index.js.map +1 -0
  61. package/dist/email-automations/index.mjs +1705 -0
  62. package/dist/email-automations/index.mjs.map +1 -0
  63. package/dist/features/analytics/index.js +122 -122
  64. package/dist/features/analytics/index.mjs +4 -4
  65. package/dist/features/analytics/server.js +6 -5
  66. package/dist/features/analytics/server.js.map +1 -1
  67. package/dist/features/analytics/server.mjs +4 -3
  68. package/dist/features/analytics/server.mjs.map +1 -1
  69. package/dist/features/blog/index.css +30 -0
  70. package/dist/features/blog/index.css.map +1 -0
  71. package/dist/features/blog/index.d.mts +266 -0
  72. package/dist/features/blog/index.d.ts +266 -0
  73. package/dist/features/blog/index.js +1799 -0
  74. package/dist/features/blog/index.js.map +1 -0
  75. package/dist/features/blog/index.mjs +1799 -0
  76. package/dist/features/blog/index.mjs.map +1 -0
  77. package/dist/features/blog/server.d.mts +33 -0
  78. package/dist/features/blog/server.d.ts +33 -0
  79. package/dist/features/blog/server.js +139 -0
  80. package/dist/features/blog/server.js.map +1 -0
  81. package/dist/features/blog/server.mjs +139 -0
  82. package/dist/features/blog/server.mjs.map +1 -0
  83. package/dist/features/help/index.js +37 -37
  84. package/dist/features/help/index.mjs +3 -3
  85. package/dist/features/tokenusage/index.js +125 -125
  86. package/dist/features/tokenusage/index.mjs +4 -4
  87. package/dist/index.d.mts +5 -2
  88. package/dist/index.d.ts +5 -2
  89. package/dist/index.js +65 -3
  90. package/dist/index.js.map +1 -1
  91. package/dist/index.mjs +64 -2
  92. package/dist/{s3.service-CY-jgOAM.d.ts → s3.service-Bz_K-tjz.d.ts} +11 -0
  93. package/dist/{s3.service-Sri0QP7Z.d.mts → s3.service-D5zNCEW3.d.mts} +11 -0
  94. package/dist/server/index.d.mts +1 -1
  95. package/dist/server/index.d.ts +1 -1
  96. package/dist/server/index.js +8 -6
  97. package/dist/server/index.js.map +1 -1
  98. package/dist/server/index.mjs +12 -10
  99. package/dist/types-70acd-8C.d.mts +39 -0
  100. package/dist/types-l8SFleXi.d.ts +39 -0
  101. package/messages/en.json +210 -0
  102. package/messages/it.json +210 -0
  103. package/package.json +21 -1
  104. package/src/components/editors/BlockNoteEditor.tsx +13 -3
  105. package/src/components/forms/FormBlockNote.tsx +4 -0
  106. package/src/components/forms/FormFieldWrapper.tsx +1 -1
  107. package/src/components/forms/FormTextarea.tsx +2 -1
  108. package/src/components/navigations/ContentTitle.tsx +14 -3
  109. package/src/core/index.ts +12 -0
  110. package/src/core/registry/ModuleRegistry.ts +11 -0
  111. package/src/email-automations/index.ts +5 -0
  112. package/src/features/blog/BlogModules.ts +49 -0
  113. package/src/features/blog/components/admin/BlogAdminContainer.tsx +32 -0
  114. package/src/features/blog/components/admin/BlogCategoriesList.tsx +56 -0
  115. package/src/features/blog/components/admin/BlogCategoryEditor.tsx +230 -0
  116. package/src/features/blog/components/admin/BlogCoverImageField.tsx +123 -0
  117. package/src/features/blog/components/admin/BlogLayoutSetting.tsx +86 -0
  118. package/src/features/blog/components/admin/BlogPostComparison.tsx +54 -0
  119. package/src/features/blog/components/admin/BlogPostContainer.tsx +102 -0
  120. package/src/features/blog/components/admin/BlogPostDeleter.tsx +35 -0
  121. package/src/features/blog/components/admin/BlogPostEditor.tsx +207 -0
  122. package/src/features/blog/components/admin/BlogPostPublishButton.tsx +50 -0
  123. package/src/features/blog/components/admin/BlogPostTranslationEditor.tsx +110 -0
  124. package/src/features/blog/components/admin/BlogPostTranslationPanel.tsx +136 -0
  125. package/src/features/blog/components/admin/BlogPostsList.tsx +58 -0
  126. package/src/features/blog/components/admin/BlogTranslationStatus.tsx +104 -0
  127. package/src/features/blog/components/admin/__tests__/BlogPostTranslationPanel.spec.tsx +100 -0
  128. package/src/features/blog/components/admin/__tests__/BlogPostsList.spec.tsx +30 -0
  129. package/src/features/blog/components/admin/index.ts +14 -0
  130. package/src/features/blog/components/public/BlogCategoryChips.tsx +59 -0
  131. package/src/features/blog/components/public/BlogList.tsx +21 -0
  132. package/src/features/blog/components/public/BlogListColumn.tsx +53 -0
  133. package/src/features/blog/components/public/BlogListFeatured.tsx +57 -0
  134. package/src/features/blog/components/public/BlogListParts.tsx +74 -0
  135. package/src/features/blog/components/public/BlogListRail.tsx +62 -0
  136. package/src/features/blog/components/public/BlogPostCard.tsx +132 -0
  137. package/src/features/blog/components/public/BlogPostMeta.tsx +49 -0
  138. package/src/features/blog/components/public/BlogPostView.tsx +74 -0
  139. package/src/features/blog/components/public/BlogSearchBox.tsx +52 -0
  140. package/src/features/blog/components/public/__tests__/BlogList.spec.tsx +173 -0
  141. package/src/features/blog/components/public/__tests__/BlogPostView.spec.tsx +128 -0
  142. package/src/features/blog/components/public/blogPublicHelpers.ts +30 -0
  143. package/src/features/blog/components/public/index.ts +3 -0
  144. package/src/features/blog/contexts/BlogPostContext.tsx +66 -0
  145. package/src/features/blog/contexts/index.ts +1 -0
  146. package/src/features/blog/data/BlogCategory.ts +93 -0
  147. package/src/features/blog/data/BlogCategoryFields.ts +4 -0
  148. package/src/features/blog/data/BlogCategoryInterface.ts +24 -0
  149. package/src/features/blog/data/BlogCategoryService.ts +54 -0
  150. package/src/features/blog/data/BlogCategoryTranslation.ts +90 -0
  151. package/src/features/blog/data/BlogCategoryTranslationInterface.ts +22 -0
  152. package/src/features/blog/data/BlogCategoryTranslationService.ts +30 -0
  153. package/src/features/blog/data/BlogPost.spec.ts +123 -0
  154. package/src/features/blog/data/BlogPost.ts +159 -0
  155. package/src/features/blog/data/BlogPostFields.ts +7 -0
  156. package/src/features/blog/data/BlogPostInterface.ts +39 -0
  157. package/src/features/blog/data/BlogPostService.ts +82 -0
  158. package/src/features/blog/data/BlogPostTranslation.spec.ts +73 -0
  159. package/src/features/blog/data/BlogPostTranslation.ts +110 -0
  160. package/src/features/blog/data/BlogPostTranslationInterface.ts +27 -0
  161. package/src/features/blog/data/BlogPostTranslationService.ts +30 -0
  162. package/src/features/blog/data/BlogSettings.ts +37 -0
  163. package/src/features/blog/data/BlogSettingsInterface.ts +11 -0
  164. package/src/features/blog/data/BlogSettingsService.ts +24 -0
  165. package/src/features/blog/data/index.ts +17 -0
  166. package/src/features/blog/hooks/index.ts +3 -0
  167. package/src/features/blog/hooks/useBlogCategoryNames.ts +34 -0
  168. package/src/features/blog/hooks/useBlogCategoryTableStructure.tsx +84 -0
  169. package/src/features/blog/hooks/useBlogPostTableStructure.tsx +126 -0
  170. package/src/features/blog/index.ts +15 -0
  171. package/src/features/blog/server/fetchers.ts +120 -0
  172. package/src/features/blog/server/toPublic.spec.ts +91 -0
  173. package/src/features/blog/server/toPublic.ts +36 -0
  174. package/src/features/blog/server-entry.ts +17 -0
  175. package/src/features/blog/types/blog-public.types.ts +34 -0
  176. package/src/features/blog/types/blog.constants.ts +40 -0
  177. package/src/features/email-automation/EmailAutomationEnrollmentModule.ts +11 -0
  178. package/src/features/email-automation/EmailAutomationModule.ts +30 -0
  179. package/src/features/email-automation/EmailAutomationStepModule.ts +11 -0
  180. package/src/features/email-automation/EmailAutomationTriggerModule.ts +11 -0
  181. package/src/features/email-automation/components/__tests__/EmailAutomationEditor.spec.tsx +104 -0
  182. package/src/features/email-automation/components/canvas/EmailAutomationActiveSwitch.tsx +49 -0
  183. package/src/features/email-automation/components/canvas/EmailAutomationCanvas.tsx +474 -0
  184. package/src/features/email-automation/components/canvas/EmailAutomationCanvasInspector.tsx +89 -0
  185. package/src/features/email-automation/components/canvas/EmailAutomationCanvasTab.tsx +32 -0
  186. package/src/features/email-automation/components/canvas/__tests__/buildCanvasGraph.spec.ts +67 -0
  187. package/src/features/email-automation/components/canvas/buildCanvasGraph.ts +141 -0
  188. package/src/features/email-automation/components/canvas/edges/DelayEdge.tsx +68 -0
  189. package/src/features/email-automation/components/canvas/nodes/EmailStepNode.tsx +43 -0
  190. package/src/features/email-automation/components/canvas/nodes/TriggerNode.tsx +23 -0
  191. package/src/features/email-automation/components/canvas/shared/CanvasToolbar.tsx +53 -0
  192. package/src/features/email-automation/components/canvas/shared/EmptyCanvas.tsx +11 -0
  193. package/src/features/email-automation/components/canvas/shared/__tests__/autoLayout.spec.ts +42 -0
  194. package/src/features/email-automation/components/canvas/shared/__tests__/longPress.spec.ts +51 -0
  195. package/src/features/email-automation/components/canvas/shared/autoLayout.ts +55 -0
  196. package/src/features/email-automation/components/canvas/shared/index.ts +4 -0
  197. package/src/features/email-automation/components/canvas/shared/longPress.ts +57 -0
  198. package/src/features/email-automation/components/containers/EmailAutomationContainer.tsx +80 -0
  199. package/src/features/email-automation/components/containers/EmailAutomationsListContainer.tsx +27 -0
  200. package/src/features/email-automation/components/forms/EmailAutomationEditor.tsx +153 -0
  201. package/src/features/email-automation/components/forms/EmailStepEditor.tsx +151 -0
  202. package/src/features/email-automation/components/index.ts +15 -0
  203. package/src/features/email-automation/components/lists/EmailAutomationRecipientsList.tsx +90 -0
  204. package/src/features/email-automation/components/lists/EmailAutomationsList.tsx +76 -0
  205. package/src/features/email-automation/components/unsubscribe/EmailUnsubscribe.tsx +99 -0
  206. package/src/features/email-automation/components/unsubscribe/__tests__/EmailUnsubscribe.spec.tsx +55 -0
  207. package/src/features/email-automation/contexts/EmailAutomationContext.tsx +175 -0
  208. package/src/features/email-automation/data/EmailAutomation.ts +87 -0
  209. package/src/features/email-automation/data/EmailAutomationEnrollment.ts +105 -0
  210. package/src/features/email-automation/data/EmailAutomationEnrollmentInterface.ts +18 -0
  211. package/src/features/email-automation/data/EmailAutomationEnrollmentService.ts +31 -0
  212. package/src/features/email-automation/data/EmailAutomationInterface.ts +22 -0
  213. package/src/features/email-automation/data/EmailAutomationService.ts +106 -0
  214. package/src/features/email-automation/data/EmailAutomationStep.ts +104 -0
  215. package/src/features/email-automation/data/EmailAutomationStepInterface.ts +23 -0
  216. package/src/features/email-automation/data/EmailAutomationStepService.ts +80 -0
  217. package/src/features/email-automation/data/EmailAutomationTrigger.ts +28 -0
  218. package/src/features/email-automation/data/EmailAutomationTriggerInterface.ts +6 -0
  219. package/src/features/email-automation/data/__tests__/email-automation-models.spec.ts +103 -0
  220. package/src/features/email-automation/data/email-automation-enrollment.fields.ts +8 -0
  221. package/src/features/email-automation/data/email-automation.fields.ts +9 -0
  222. package/src/features/email-automation/data/index.ts +13 -0
  223. package/src/features/email-automation/hooks/index.ts +2 -0
  224. package/src/features/email-automation/hooks/useEmailAutomationEnrollmentTableStructure.tsx +126 -0
  225. package/src/features/email-automation/hooks/useEmailAutomationTableStructure.tsx +119 -0
  226. package/src/features/email-automation/index.ts +5 -0
  227. package/src/features/s3/data/s3.service.ts +17 -0
  228. package/dist/BlockNoteEditor-2QJVK2HH.mjs.map +0 -1
  229. package/dist/BlockNoteEditor-EGY4ZAAM.js.map +0 -1
  230. package/dist/chunk-2Z3ZCYXX.mjs.map +0 -1
  231. package/dist/chunk-HNOWSFM5.mjs.map +0 -1
  232. package/dist/chunk-S5MQDWWC.js.map +0 -1
  233. package/dist/chunk-THKYH65N.js.map +0 -1
  234. package/dist/chunk-YMUPTXOT.mjs.map +0 -1
  235. /package/dist/{BlockNoteViewer-6BQXJ4TB.mjs.map → BlockNoteViewer-AEJKJ27Q.mjs.map} +0 -0
  236. /package/dist/{chunk-NPKFMRH5.mjs.map → chunk-6DC2WVVB.mjs.map} +0 -0
  237. /package/dist/{chunk-JEZQTZQ4.mjs.map → chunk-QMYQRPQC.mjs.map} +0 -0
@@ -0,0 +1,99 @@
1
+ "use client";
2
+
3
+ import { CheckCircle, Loader2, XCircle } from "lucide-react";
4
+ import { useTranslations } from "next-intl";
5
+ import { useEffect, useState } from "react";
6
+ import { Button } from "../../../../shadcnui";
7
+ import { SectionHeader } from "../../../../components/typography";
8
+ import { EmailAutomationEnrollmentService } from "../../data/EmailAutomationEnrollmentService";
9
+
10
+ type UnsubscribeScope = "automation" | "all";
11
+
12
+ interface Props {
13
+ token: string;
14
+ scope: UnsubscribeScope;
15
+ }
16
+
17
+ type UnsubscribeState = "loading" | "success" | "error";
18
+
19
+ export function EmailUnsubscribe({ token, scope }: Props) {
20
+ const t = useTranslations();
21
+ const [state, setState] = useState<UnsubscribeState>("loading");
22
+ const [stoppedScope, setStoppedScope] = useState<UnsubscribeScope>(scope);
23
+ const [automationName, setAutomationName] = useState<string>("");
24
+ const [isStoppingAll, setIsStoppingAll] = useState<boolean>(false);
25
+
26
+ useEffect(() => {
27
+ async function unsubscribe() {
28
+ try {
29
+ const enrollment = await EmailAutomationEnrollmentService.unsubscribe({ token, scope });
30
+ setAutomationName(enrollment?.automation?.name ?? "");
31
+ setStoppedScope(scope);
32
+ setState("success");
33
+ } catch {
34
+ setState("error");
35
+ }
36
+ }
37
+
38
+ unsubscribe();
39
+ }, [token, scope]);
40
+
41
+ const stopAll = async () => {
42
+ setIsStoppingAll(true);
43
+ try {
44
+ await EmailAutomationEnrollmentService.unsubscribe({ token, scope: "all" });
45
+ setStoppedScope("all");
46
+ } catch {
47
+ setState("error");
48
+ } finally {
49
+ setIsStoppingAll(false);
50
+ }
51
+ };
52
+
53
+ if (state === "loading") {
54
+ return (
55
+ <div className="flex flex-col items-center justify-center space-y-4 py-16 text-center">
56
+ <Loader2 className="text-primary h-12 w-12 animate-spin" />
57
+ <p className="text-muted-foreground text-sm">{t("emailAutomations.unsubscribe.loading")}</p>
58
+ </div>
59
+ );
60
+ }
61
+
62
+ if (state === "error") {
63
+ return (
64
+ <div className="flex flex-col items-center justify-center space-y-4 py-16 text-center">
65
+ <div className="bg-destructive/10 rounded-full p-4">
66
+ <XCircle className="text-destructive h-12 w-12" />
67
+ </div>
68
+ <SectionHeader level={2}>{t("emailAutomations.unsubscribe.invalidTitle")}</SectionHeader>
69
+ <p className="text-muted-foreground max-w-md text-sm leading-relaxed">
70
+ {t("emailAutomations.unsubscribe.invalidBody")}
71
+ </p>
72
+ </div>
73
+ );
74
+ }
75
+
76
+ return (
77
+ <div className="flex flex-col items-center justify-center space-y-4 py-16 text-center">
78
+ <div className="bg-primary/10 rounded-full p-4">
79
+ <CheckCircle className="text-primary h-12 w-12" />
80
+ </div>
81
+ <SectionHeader level={2}>{t("emailAutomations.unsubscribe.title")}</SectionHeader>
82
+ {stoppedScope === "all" ? (
83
+ <p className="text-muted-foreground max-w-md text-sm leading-relaxed">
84
+ {t("emailAutomations.unsubscribe.stoppedAll")}
85
+ </p>
86
+ ) : (
87
+ <>
88
+ <p className="text-muted-foreground max-w-md text-sm leading-relaxed">
89
+ {t("emailAutomations.unsubscribe.stoppedAutomation", { name: automationName })}
90
+ </p>
91
+ <Button variant="secondary" onClick={stopAll} disabled={isStoppingAll}>
92
+ {isStoppingAll && <Loader2 className="h-4 w-4 animate-spin" />}
93
+ {t("emailAutomations.unsubscribe.alsoStopAll")}
94
+ </Button>
95
+ </>
96
+ )}
97
+ </div>
98
+ );
99
+ }
@@ -0,0 +1,55 @@
1
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { EmailUnsubscribe } from "../EmailUnsubscribe";
4
+
5
+ const { unsubscribe } = vi.hoisted(() => ({
6
+ unsubscribe: vi.fn(),
7
+ }));
8
+
9
+ vi.mock("../../../data/EmailAutomationEnrollmentService", () => ({
10
+ EmailAutomationEnrollmentService: {
11
+ unsubscribe: (params: unknown) => unsubscribe(params),
12
+ },
13
+ }));
14
+
15
+ beforeEach(() => {
16
+ vi.clearAllMocks();
17
+ });
18
+
19
+ describe("EmailUnsubscribe", () => {
20
+ it("stops the automation and offers to stop every marketing email", async () => {
21
+ unsubscribe.mockResolvedValue({ automation: { name: "Welcome" } });
22
+
23
+ render(<EmailUnsubscribe token="tok" scope="automation" />);
24
+
25
+ expect(await screen.findByText("emailAutomations.unsubscribe.stoppedAutomation")).toBeInTheDocument();
26
+ expect(unsubscribe).toHaveBeenCalledWith({ token: "tok", scope: "automation" });
27
+
28
+ fireEvent.click(screen.getByRole("button", { name: "emailAutomations.unsubscribe.alsoStopAll" }));
29
+
30
+ expect(await screen.findByText("emailAutomations.unsubscribe.stoppedAll")).toBeInTheDocument();
31
+ expect(unsubscribe).toHaveBeenLastCalledWith({ token: "tok", scope: "all" });
32
+ expect(screen.queryByRole("button", { name: "emailAutomations.unsubscribe.alsoStopAll" })).not.toBeInTheDocument();
33
+ });
34
+
35
+ it("shows only the stopped-all message when the link already asks for every email", async () => {
36
+ unsubscribe.mockResolvedValue({ automation: { name: "Welcome" } });
37
+
38
+ render(<EmailUnsubscribe token="tok" scope="all" />);
39
+
40
+ expect(await screen.findByText("emailAutomations.unsubscribe.stoppedAll")).toBeInTheDocument();
41
+ expect(unsubscribe).toHaveBeenCalledWith({ token: "tok", scope: "all" });
42
+ expect(screen.queryByRole("button", { name: "emailAutomations.unsubscribe.alsoStopAll" })).not.toBeInTheDocument();
43
+ });
44
+
45
+ it("shows the invalid-link state when the call is rejected", async () => {
46
+ unsubscribe.mockRejectedValue(new Error("Not found"));
47
+
48
+ render(<EmailUnsubscribe token="unknown" scope="automation" />);
49
+
50
+ await waitFor(() => {
51
+ expect(screen.getByText("emailAutomations.unsubscribe.invalidTitle")).toBeInTheDocument();
52
+ });
53
+ expect(screen.getByText("emailAutomations.unsubscribe.invalidBody")).toBeInTheDocument();
54
+ });
55
+ });
@@ -0,0 +1,175 @@
1
+ "use client";
2
+
3
+ import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
4
+ import { v4 } from "uuid";
5
+ import { EmailAutomationInterface } from "../data/EmailAutomationInterface";
6
+ import { EmailAutomationService } from "../data/EmailAutomationService";
7
+ import { EmailAutomationStepInput, EmailAutomationStepInterface } from "../data/EmailAutomationStepInterface";
8
+ import { EmailAutomationStepService } from "../data/EmailAutomationStepService";
9
+ import { EmailAutomationTriggerInterface } from "../data/EmailAutomationTriggerInterface";
10
+
11
+ /** A new step: the provider always sets `automationId` and, when absent, a fresh `id`. */
12
+ export type EmailAutomationStepCreateInput = Omit<EmailAutomationStepInput, "id" | "automationId"> & {
13
+ id?: string;
14
+ automationId?: string;
15
+ };
16
+ /** A step update: the provider always sets `automationId`. */
17
+ export type EmailAutomationStepUpdateInput = Omit<EmailAutomationStepInput, "automationId"> & { automationId?: string };
18
+
19
+ export interface EmailAutomationContextType {
20
+ automation: EmailAutomationInterface | undefined;
21
+ steps: EmailAutomationStepInterface[];
22
+ reload: () => Promise<void>;
23
+ selectedStepId: string | null;
24
+ select: (id: string | null) => void;
25
+ createStep: (input: EmailAutomationStepCreateInput) => Promise<EmailAutomationStepInterface>;
26
+ updateStep: (input: EmailAutomationStepUpdateInput) => Promise<EmailAutomationStepInterface>;
27
+ deleteStep: (id: string) => Promise<void>;
28
+ setNext: (stepId: string, nextStepId: string | null) => Promise<void>;
29
+ updatePosition: (stepId: string, x: number, y: number) => Promise<void>;
30
+ tidy: () => Promise<void>;
31
+ triggers: EmailAutomationTriggerInterface[];
32
+ }
33
+
34
+ const EmailAutomationContext = createContext<EmailAutomationContextType | undefined>(undefined);
35
+
36
+ export type EmailAutomationProviderProps = {
37
+ automationId: string;
38
+ children: ReactNode;
39
+ };
40
+
41
+ export const EmailAutomationProvider = ({ automationId, children }: EmailAutomationProviderProps) => {
42
+ const [automation, setAutomation] = useState<EmailAutomationInterface | undefined>(undefined);
43
+ const [steps, setSteps] = useState<EmailAutomationStepInterface[]>([]);
44
+ const [triggers, setTriggers] = useState<EmailAutomationTriggerInterface[]>([]);
45
+ const [selectedStepId, setSelectedStepId] = useState<string | null>(null);
46
+
47
+ const reload = useCallback(async () => {
48
+ const [loadedAutomation, loadedSteps] = await Promise.all([
49
+ EmailAutomationService.findOne({ id: automationId }),
50
+ EmailAutomationStepService.findByAutomation({ automationId }),
51
+ ]);
52
+ setAutomation(loadedAutomation);
53
+ setSteps(loadedSteps);
54
+ }, [automationId]);
55
+
56
+ useEffect(() => {
57
+ void reload();
58
+ }, [reload]);
59
+
60
+ useEffect(() => {
61
+ let active = true;
62
+ EmailAutomationService.findTriggers()
63
+ .then((list) => {
64
+ if (active) setTriggers(list);
65
+ })
66
+ .catch(() => {});
67
+ return () => {
68
+ active = false;
69
+ };
70
+ }, []);
71
+
72
+ const select = useCallback((id: string | null) => setSelectedStepId(id), []);
73
+
74
+ const createStep = useCallback(
75
+ async (input: EmailAutomationStepCreateInput) => {
76
+ const saved = await EmailAutomationStepService.create({ ...input, id: input.id ?? v4(), automationId });
77
+ await reload();
78
+ return saved;
79
+ },
80
+ [automationId, reload],
81
+ );
82
+
83
+ const updateStep = useCallback(
84
+ async (input: EmailAutomationStepUpdateInput) => {
85
+ const saved = await EmailAutomationStepService.update({ ...input, automationId });
86
+ await reload();
87
+ return saved;
88
+ },
89
+ [automationId, reload],
90
+ );
91
+
92
+ const deleteStep = useCallback(
93
+ async (id: string) => {
94
+ await EmailAutomationStepService.delete({ id });
95
+ if (selectedStepId === id) setSelectedStepId(null);
96
+ await reload();
97
+ },
98
+ [reload, selectedStepId],
99
+ );
100
+
101
+ const setNext = useCallback(
102
+ async (stepId: string, nextStepId: string | null) => {
103
+ if (nextStepId) await EmailAutomationStepService.setNext({ stepId, nextStepId });
104
+ else await EmailAutomationStepService.clearNext({ stepId });
105
+ await reload();
106
+ },
107
+ [reload],
108
+ );
109
+
110
+ // PATCH with the step's current attributes plus the new position: the step
111
+ // input type requires subject and body, and sending them unchanged is a no-op.
112
+ const updatePosition = useCallback(
113
+ async (stepId: string, x: number, y: number) => {
114
+ const step = steps.find((candidate) => candidate.id === stepId);
115
+ if (!step) return;
116
+ await EmailAutomationStepService.update({
117
+ id: step.id,
118
+ automationId,
119
+ subject: step.subject,
120
+ body: step.body,
121
+ delayHours: step.delayHours,
122
+ positionX: x,
123
+ positionY: y,
124
+ });
125
+ await reload();
126
+ },
127
+ [automationId, reload, steps],
128
+ );
129
+
130
+ const tidy = useCallback(async () => {
131
+ await EmailAutomationService.tidy({ id: automationId });
132
+ await reload();
133
+ }, [automationId, reload]);
134
+
135
+ const value = useMemo<EmailAutomationContextType>(
136
+ () => ({
137
+ automation,
138
+ steps,
139
+ reload,
140
+ selectedStepId,
141
+ select,
142
+ createStep,
143
+ updateStep,
144
+ deleteStep,
145
+ setNext,
146
+ updatePosition,
147
+ tidy,
148
+ triggers,
149
+ }),
150
+ [
151
+ automation,
152
+ steps,
153
+ reload,
154
+ selectedStepId,
155
+ select,
156
+ createStep,
157
+ updateStep,
158
+ deleteStep,
159
+ setNext,
160
+ updatePosition,
161
+ tidy,
162
+ triggers,
163
+ ],
164
+ );
165
+
166
+ return <EmailAutomationContext.Provider value={value}>{children}</EmailAutomationContext.Provider>;
167
+ };
168
+
169
+ export const useEmailAutomation = (): EmailAutomationContextType => {
170
+ const context = useContext(EmailAutomationContext);
171
+ if (context === undefined) {
172
+ throw new Error("useEmailAutomation must be used within an EmailAutomationProvider");
173
+ }
174
+ return context;
175
+ };
@@ -0,0 +1,87 @@
1
+ import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
2
+ import { EmailAutomationInput, EmailAutomationInterface } from "./EmailAutomationInterface";
3
+
4
+ export class EmailAutomation extends AbstractApiData implements EmailAutomationInterface {
5
+ private _name?: string;
6
+ private _trigger?: string;
7
+ private _exitTrigger?: string;
8
+ private _replyTo?: string;
9
+ private _isActive: boolean = false;
10
+ private _stepCount?: number;
11
+ private _activeCount?: number;
12
+ private _completedCount?: number;
13
+ private _unsubscribedCount?: number;
14
+
15
+ get name(): string {
16
+ return this._name ?? "";
17
+ }
18
+
19
+ get trigger(): string | undefined {
20
+ return this._trigger;
21
+ }
22
+
23
+ get exitTrigger(): string | undefined {
24
+ return this._exitTrigger;
25
+ }
26
+
27
+ get replyTo(): string | undefined {
28
+ return this._replyTo;
29
+ }
30
+
31
+ get isActive(): boolean {
32
+ return this._isActive;
33
+ }
34
+
35
+ get stepCount(): number {
36
+ return this._stepCount ?? 0;
37
+ }
38
+
39
+ get activeCount(): number {
40
+ return this._activeCount ?? 0;
41
+ }
42
+
43
+ get completedCount(): number {
44
+ return this._completedCount ?? 0;
45
+ }
46
+
47
+ get unsubscribedCount(): number {
48
+ return this._unsubscribedCount ?? 0;
49
+ }
50
+
51
+ rehydrate(data: JsonApiHydratedDataInterface): this {
52
+ super.rehydrate(data);
53
+
54
+ this._name = data.jsonApi.attributes.name;
55
+ this._trigger = data.jsonApi.attributes.trigger ?? undefined;
56
+ this._exitTrigger = data.jsonApi.attributes.exitTrigger ?? undefined;
57
+ this._replyTo = data.jsonApi.attributes.replyTo ?? undefined;
58
+ this._isActive = data.jsonApi.attributes.isActive === true;
59
+ this._stepCount = data.jsonApi.attributes.stepCount;
60
+ this._activeCount = data.jsonApi.attributes.activeCount;
61
+ this._completedCount = data.jsonApi.attributes.completedCount;
62
+ this._unsubscribedCount = data.jsonApi.attributes.unsubscribedCount;
63
+
64
+ return this;
65
+ }
66
+
67
+ createJsonApi(data: EmailAutomationInput) {
68
+ const response: any = {
69
+ data: {
70
+ type: Modules.EmailAutomation.name,
71
+ id: data.id,
72
+ attributes: {},
73
+ meta: {},
74
+ relationships: {},
75
+ },
76
+ included: [],
77
+ };
78
+
79
+ if (data.name !== undefined) response.data.attributes.name = data.name;
80
+ if (data.trigger !== undefined) response.data.attributes.trigger = data.trigger;
81
+ if (data.exitTrigger !== undefined) response.data.attributes.exitTrigger = data.exitTrigger;
82
+ if (data.replyTo !== undefined) response.data.attributes.replyTo = data.replyTo;
83
+ if (data.isActive !== undefined) response.data.attributes.isActive = data.isActive;
84
+
85
+ return response;
86
+ }
87
+ }
@@ -0,0 +1,105 @@
1
+ import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
2
+ import {
3
+ EmailAutomationEnrollmentInterface,
4
+ EmailAutomationEnrollmentStatus,
5
+ } from "./EmailAutomationEnrollmentInterface";
6
+ import { EmailAutomationInterface } from "./EmailAutomationInterface";
7
+
8
+ /** JSON:API type of the POST email-automations/unsubscribe/:token request body (not a module: nothing is stored). */
9
+ export const EMAIL_AUTOMATION_UNSUBSCRIBE_TYPE = "email-automation-unsubscribes";
10
+
11
+ export class EmailAutomationEnrollment extends AbstractApiData implements EmailAutomationEnrollmentInterface {
12
+ private _email?: string;
13
+ private _name?: string;
14
+ private _status: EmailAutomationEnrollmentStatus = "active";
15
+ private _enrolledAt?: Date;
16
+ private _nextSendAt?: Date;
17
+ private _finishedAt?: Date;
18
+ private _attempts?: number;
19
+ private _lastError?: string;
20
+ private _currentStepSubject?: string;
21
+ private _sentCount?: number;
22
+ private _automation?: EmailAutomationInterface;
23
+
24
+ get email(): string {
25
+ return this._email ?? "";
26
+ }
27
+
28
+ get name(): string | undefined {
29
+ return this._name;
30
+ }
31
+
32
+ get status(): EmailAutomationEnrollmentStatus {
33
+ return this._status;
34
+ }
35
+
36
+ get enrolledAt(): Date {
37
+ return this._enrolledAt ?? new Date(0);
38
+ }
39
+
40
+ get nextSendAt(): Date | undefined {
41
+ return this._nextSendAt;
42
+ }
43
+
44
+ get finishedAt(): Date | undefined {
45
+ return this._finishedAt;
46
+ }
47
+
48
+ get attempts(): number {
49
+ return this._attempts ?? 0;
50
+ }
51
+
52
+ get lastError(): string | undefined {
53
+ return this._lastError;
54
+ }
55
+
56
+ get currentStepSubject(): string | undefined {
57
+ return this._currentStepSubject;
58
+ }
59
+
60
+ get sentCount(): number {
61
+ return this._sentCount ?? 0;
62
+ }
63
+
64
+ get automation(): EmailAutomationInterface | undefined {
65
+ return this._automation;
66
+ }
67
+
68
+ rehydrate(data: JsonApiHydratedDataInterface): this {
69
+ super.rehydrate(data);
70
+
71
+ const attributes = data.jsonApi.attributes;
72
+ this._email = attributes.email;
73
+ this._name = attributes.name ?? undefined;
74
+ this._status = attributes.status ?? "active";
75
+ this._enrolledAt = attributes.enrolledAt ? new Date(attributes.enrolledAt) : undefined;
76
+ this._nextSendAt = attributes.nextSendAt ? new Date(attributes.nextSendAt) : undefined;
77
+ this._finishedAt = attributes.finishedAt ? new Date(attributes.finishedAt) : undefined;
78
+ this._attempts = attributes.attempts;
79
+ this._lastError = attributes.lastError ?? undefined;
80
+ this._currentStepSubject = attributes.currentStepSubject ?? undefined;
81
+ this._sentCount = attributes.sentCount;
82
+
83
+ this._automation = this._readIncluded(data, "automation", Modules.EmailAutomation) as EmailAutomationInterface;
84
+
85
+ return this;
86
+ }
87
+
88
+ createJsonApi(_data?: any): any {
89
+ throw new Error("EmailAutomationEnrollment is created by the API and cannot be created from the client");
90
+ }
91
+
92
+ /**
93
+ * Body of POST email-automations/unsubscribe/:token.
94
+ */
95
+ createUnsubscribeJsonApi(params: { scope: "automation" | "all" }) {
96
+ return {
97
+ data: {
98
+ type: EMAIL_AUTOMATION_UNSUBSCRIBE_TYPE,
99
+ attributes: {
100
+ scope: params.scope,
101
+ },
102
+ },
103
+ };
104
+ }
105
+ }
@@ -0,0 +1,18 @@
1
+ import { ApiDataInterface } from "../../../core";
2
+ import { EmailAutomationInterface } from "./EmailAutomationInterface";
3
+
4
+ export type EmailAutomationEnrollmentStatus = "active" | "completed" | "unsubscribed" | "exited" | "failed";
5
+
6
+ export interface EmailAutomationEnrollmentInterface extends ApiDataInterface {
7
+ get email(): string;
8
+ get name(): string | undefined;
9
+ get status(): EmailAutomationEnrollmentStatus;
10
+ get enrolledAt(): Date;
11
+ get nextSendAt(): Date | undefined;
12
+ get finishedAt(): Date | undefined;
13
+ get attempts(): number;
14
+ get lastError(): string | undefined;
15
+ get currentStepSubject(): string | undefined;
16
+ get sentCount(): number;
17
+ get automation(): EmailAutomationInterface | undefined;
18
+ }
@@ -0,0 +1,31 @@
1
+ import { AbstractService, EndpointCreator, HttpMethod, Modules } from "../../../core";
2
+ import { EmailAutomationEnrollment } from "./EmailAutomationEnrollment";
3
+ import { EmailAutomationEnrollmentInterface } from "./EmailAutomationEnrollmentInterface";
4
+
5
+ export class EmailAutomationEnrollmentService extends AbstractService {
6
+ /**
7
+ * POST email-automations/unsubscribe/:token (public). The scope travels both as
8
+ * a query parameter and in the body, as the API reads either.
9
+ */
10
+ static async unsubscribe(params: {
11
+ token: string;
12
+ scope: "automation" | "all";
13
+ }): Promise<EmailAutomationEnrollmentInterface> {
14
+ const enrollment = new EmailAutomationEnrollment();
15
+ const endpoint = new EndpointCreator({
16
+ endpoint: Modules.EmailAutomation,
17
+ childEndpoint: "unsubscribe",
18
+ childId: params.token,
19
+ });
20
+
21
+ endpoint.addAdditionalParam("scope", params.scope);
22
+
23
+ return this.callApi<EmailAutomationEnrollmentInterface>({
24
+ type: Modules.EmailAutomationEnrollment,
25
+ method: HttpMethod.POST,
26
+ endpoint: endpoint.generate(),
27
+ input: enrollment.createUnsubscribeJsonApi({ scope: params.scope }),
28
+ overridesJsonApiCreation: true,
29
+ });
30
+ }
31
+ }
@@ -0,0 +1,22 @@
1
+ import { ApiDataInterface } from "../../../core";
2
+
3
+ export type EmailAutomationInput = {
4
+ id: string;
5
+ name: string;
6
+ trigger?: string;
7
+ exitTrigger?: string;
8
+ replyTo?: string;
9
+ isActive?: boolean;
10
+ };
11
+
12
+ export interface EmailAutomationInterface extends ApiDataInterface {
13
+ get name(): string;
14
+ get trigger(): string | undefined;
15
+ get exitTrigger(): string | undefined;
16
+ get replyTo(): string | undefined;
17
+ get isActive(): boolean;
18
+ get stepCount(): number;
19
+ get activeCount(): number;
20
+ get completedCount(): number;
21
+ get unsubscribedCount(): number;
22
+ }