@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.
- package/README.md +1 -1
- package/dist/guest/{driver-DBt5l1Z5.js → driver-BXuYP007.js} +4 -1
- package/dist/guest/driver.d.ts +1 -1
- package/dist/guest/driver.js +1 -1
- package/dist/guest/emails.d.ts +6 -0
- package/dist/guest/{index-CXcCEcAg.d.ts → index-EqBCZnpq.d.ts} +106 -15
- package/dist/guest/{journeys-BrcKEXz0.js → journeys-F8Yk8s1P.js} +292 -64
- package/dist/guest/journeys.d.ts +44 -14
- package/dist/guest/journeys.js +1 -1
- package/dist/guest/wasi.js +1 -1
- package/dist/index.js +1275 -610
- package/examples/abandoned-checkout/journeys/abandoned-checkout.ts +9 -1
- package/examples/activity-decay/journeys/activity-decay.ts +13 -2
- package/examples/winback/journeys/winback.ts +10 -2
- package/package.json +1 -1
- package/examples/cross-app-pitch/emails/cross-app-pitch.tsx +0 -77
- package/examples/cross-app-pitch/journeys/cross-app-pitch.ts +0 -34
- package/examples/cross-app-pitch/scenarios/cross-app-pitch.json +0 -17
|
@@ -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: "
|
|
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.
|
|
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: "
|
|
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.
|
|
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: "
|
|
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,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
|
-
}
|