@cowliss/cli 0.6.0 → 0.8.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.
@@ -6,12 +6,20 @@ import { defineJourney } from "@cowliss/cli/journeys";
6
6
  * recovered, a trait your segments and other journeys can read. Silence
7
7
  * spends exactly one reminder email and closes.
8
8
  *
9
+ * `marketing` and no `enrollment`, so one shopper goes through this once,
10
+ * ever: their second abandoned cart is not nagged about. Add
11
+ * `enrollment: { cooldown: "30d" }` to reach a repeat abandoner again, a
12
+ * month after their last reminder.
13
+ *
9
14
  * Example: add it with `cow add example abandoned-checkout`; rename the
10
15
  * events and the email to your domain's vocabulary.
11
16
  */
12
17
  export default defineJourney({
13
18
  trigger: { event: "checkout_started" },
14
- purpose: "emailMarketing",
19
+ purpose: "marketing",
20
+ from: "Acme <checkout@example.com>",
21
+ description:
22
+ "Reminds someone who left items in their cart, and stops if they buy.",
15
23
  tags: ["demo", "checkout"],
16
24
  run: async (event, api) => {
17
25
  const purchased = await api.waitForEvent("purchase_completed", {
@@ -6,7 +6,15 @@ import { defineJourney } from "@cowliss/cli/journeys";
6
6
  * later event restarts the execution, which resets the 30-day silence clock
7
7
  * with a fresh, empty journal; 30 days of silence unsets the flag (removed,
8
8
  * not set to false, so `exists` predicates stop matching) and the execution
9
- * completes. The next event starts a new one.
9
+ * completes.
10
+ *
11
+ * Restarting is not finishing, so a user who keeps showing up never counts
12
+ * as having been through this journey and the loop runs for as long as they
13
+ * do. The execution that ends after 30 days of silence does count, and with
14
+ * `marketing` and no `enrollment` that is the end of it: their next event
15
+ * does not re-flag them. A journey meant to track state for the life of an
16
+ * account wants to reopen as soon as it closes, which is what
17
+ * `enrollment: { cooldown: "1s" }` says.
10
18
  *
11
19
  * `"*"` is activity as it really arrives: whatever names your apps send,
12
20
  * page_view and purchase and login alike. It never matches an event Cowliss
@@ -20,7 +28,10 @@ import { defineJourney } from "@cowliss/cli/journeys";
20
28
  */
21
29
  export default defineJourney({
22
30
  trigger: { event: "*" },
23
- purpose: "emailMarketing",
31
+ purpose: "marketing",
32
+ from: "Acme <hello@example.com>",
33
+ description:
34
+ "Keeps track of whether someone has been active in the last 30 days.",
24
35
  tags: ["demo", "retention"],
25
36
  run: async (_event, api) => {
26
37
  await api.traits.set("active_30d", true);
@@ -4,14 +4,22 @@ import { defineJourney } from "@cowliss/cli/journeys";
4
4
  * Winback: `purchase_completed` starts (or restarts) an execution and marks
5
5
  * the profile as having bought recently; every further purchase inside 30
6
6
  * days resets the silence clock; after 30 days of silence the flag is unset
7
- * and one winback email goes out. The next purchase starts a fresh execution.
7
+ * and one winback email goes out.
8
+ *
9
+ * `marketing` and no `enrollment`, so each customer is won back once, ever:
10
+ * the purchase that answers the email does not start a second execution.
11
+ * That is the safe default for a mail nobody asked for. To win the same
12
+ * customer back every time they lapse, say how long to leave between
13
+ * attempts: `enrollment: { cooldown: "180d" }`.
8
14
  *
9
15
  * Example: add it with `cow add example winback`; rename the purchase event,
10
16
  * the flag trait, and the email to your domain's vocabulary.
11
17
  */
12
18
  export default defineJourney({
13
19
  trigger: { event: "purchase_completed" },
14
- purpose: "emailMarketing",
20
+ purpose: "marketing",
21
+ from: "Acme <hello@example.com>",
22
+ description: "Invites a customer back after 30 days without a purchase.",
15
23
  tags: ["demo", "retention"],
16
24
  run: async (_event, api) => {
17
25
  await api.traits.set("purchased_recently", true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cowliss/cli",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "cow": "./dist/index.js"
@@ -1,77 +0,0 @@
1
- import type { SendClass, Subject } from "@cowliss/cli/emails";
2
- import {
3
- Body,
4
- Container,
5
- Head,
6
- Heading,
7
- Html,
8
- Preview,
9
- Text,
10
- } from "@react-email/components";
11
- import type { CSSProperties, ReactElement } from "react";
12
- import { z } from "zod";
13
-
14
- /**
15
- * The cross-app-pitch example's one email: someone upgraded in one of your
16
- * apps, and a day later the other app is worth a mention. The plan name is a
17
- * prop rather than a hardcoded string, so the same email serves every tier
18
- * you pitch.
19
- *
20
- * Inline styles only, because email clients strip Tailwind and external
21
- * stylesheets alike. One-click unsubscribe rides in the RFC 8058 headers the
22
- * send path attaches, not in the body.
23
- */
24
-
25
- export const props = z.object({
26
- plan: z.string(),
27
- });
28
-
29
- export const subject: Subject<typeof props> = (p) =>
30
- `Your ${p.plan} plan works in our other app too`;
31
-
32
- export const sendClass: SendClass = "marketing";
33
-
34
- export const tags = ["demo", "cross-app"];
35
-
36
- const body: CSSProperties = {
37
- backgroundColor: "#f6f6f6",
38
- fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
39
- };
40
-
41
- const container: CSSProperties = {
42
- margin: "0 auto",
43
- padding: "32px 24px",
44
- maxWidth: "480px",
45
- backgroundColor: "#ffffff",
46
- };
47
-
48
- const heading: CSSProperties = {
49
- fontSize: "20px",
50
- fontWeight: 600,
51
- color: "#111111",
52
- };
53
-
54
- const text: CSSProperties = {
55
- fontSize: "14px",
56
- lineHeight: "22px",
57
- color: "#333333",
58
- };
59
-
60
- export default function CrossAppPitch(p: z.infer<typeof props>): ReactElement {
61
- return (
62
- <Html>
63
- <Head />
64
- <Preview>One upgrade, two apps</Preview>
65
- <Body style={body}>
66
- <Container style={container}>
67
- <Heading style={heading}>One upgrade, two apps</Heading>
68
- <Text style={text}>
69
- Thanks for going {p.plan}. The same account already works in our
70
- other app, so there is nothing to sign up for: open it and your plan
71
- follows you.
72
- </Text>
73
- </Container>
74
- </Body>
75
- </Html>
76
- );
77
- }
@@ -1,34 +0,0 @@
1
- import { defineJourney } from "@cowliss/cli/journeys";
2
-
3
- /**
4
- * Cross-app pitch: `plan_upgraded` from one app (the trigger's `appId` field
5
- * scopes a journey to one app of a multi-app org) starts the execution; after
6
- * a durable day it re-reads the profile and pitches the other app only to
7
- * users still on the free plan. This is the portfolio-operator pattern: one
8
- * org's journeys reacting across its own apps.
9
- *
10
- * Example: add it with `cow add example cross-app-pitch`; rename the app id,
11
- * the event, and the email to your own.
12
- */
13
- export default defineJourney({
14
- trigger: { event: "plan_upgraded", appId: "app_b" },
15
- purpose: "emailMarketing",
16
- // An address of its own rather than the shared one: a pitch for your other
17
- // product reads better from a product address than from the one your
18
- // receipts come from. Verify its domain under Settings, Domains first.
19
- from: "Product News <product-news@example.com>",
20
- tags: ["demo", "cross-app"],
21
- run: async (_event, api) => {
22
- await api.sleep("1d");
23
- // A fresh read, not a snapshot taken at the trigger: the plan may well
24
- // have changed during the wait, which is the whole point of waiting.
25
- const me = await api.profile.get();
26
- if (me.traits.plan !== "free") {
27
- return;
28
- }
29
- await api.send.email({
30
- template: "cross-app-pitch",
31
- props: { plan: "pro" },
32
- });
33
- },
34
- });
@@ -1,17 +0,0 @@
1
- {
2
- "user": {
3
- "id": "usr_1",
4
- "traits": { "email": "ada@example.com", "plan": "free" }
5
- },
6
- "events": [],
7
- "expect": [
8
- {
9
- "activity": "sendEmail",
10
- "input": {
11
- "template": "cross-app-pitch",
12
- "props": { "plan": "pro" },
13
- "from": "Product News <product-news@example.com>"
14
- }
15
- }
16
- ]
17
- }