@cowliss/cli 0.10.0 → 0.11.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/{examples → blueprints}/abandoned-checkout/journeys/abandoned-checkout.ts +1 -1
- package/{examples → blueprints}/activity-decay/journeys/activity-decay.ts +1 -1
- package/blueprints/ecommerce/emails/_shell.tsx +133 -0
- package/blueprints/ecommerce/emails/abandoned-checkout-last-call.tsx +46 -0
- package/blueprints/ecommerce/emails/abandoned-checkout.tsx +46 -0
- package/blueprints/ecommerce/emails/order-thanks.tsx +44 -0
- package/blueprints/ecommerce/emails/replenishment.tsx +40 -0
- package/blueprints/ecommerce/emails/review-request.tsx +44 -0
- package/blueprints/ecommerce/emails/vip-thanks.tsx +51 -0
- package/blueprints/ecommerce/emails/welcome.tsx +48 -0
- package/blueprints/ecommerce/emails/winback-offer.tsx +41 -0
- package/blueprints/ecommerce/emails/winback-reminder.tsx +45 -0
- package/blueprints/ecommerce/journeys/_helpers.ts +14 -0
- package/blueprints/ecommerce/journeys/abandoned-checkout.ts +44 -0
- package/blueprints/ecommerce/journeys/first-order-thanks.ts +40 -0
- package/blueprints/ecommerce/journeys/replenishment.ts +39 -0
- package/blueprints/ecommerce/journeys/vip.ts +29 -0
- package/blueprints/ecommerce/journeys/welcome.ts +42 -0
- package/blueprints/ecommerce/journeys/winback.ts +35 -0
- package/blueprints/ecommerce/scenarios/abandoned-checkout.last-call.json +30 -0
- package/blueprints/ecommerce/scenarios/abandoned-checkout.recovered.json +24 -0
- package/blueprints/ecommerce/scenarios/abandoned-checkout.reminded.json +33 -0
- package/blueprints/ecommerce/scenarios/first-order-thanks.delivered.json +35 -0
- package/blueprints/ecommerce/scenarios/first-order-thanks.never-delivered.json +21 -0
- package/blueprints/ecommerce/scenarios/replenishment.due.json +21 -0
- package/blueprints/ecommerce/scenarios/replenishment.reordered.json +20 -0
- package/blueprints/ecommerce/scenarios/vip.crossed.json +22 -0
- package/blueprints/ecommerce/scenarios/welcome.new-shopper.json +21 -0
- package/blueprints/ecommerce/scenarios/winback.returned.json +26 -0
- package/blueprints/ecommerce/scenarios/winback.still-gone.json +30 -0
- package/blueprints/ecommerce/segments/first-time-buyers.ts +24 -0
- package/blueprints/ecommerce/segments/lapsed.ts +27 -0
- package/blueprints/ecommerce/segments/repeat-buyers.ts +18 -0
- package/blueprints/ecommerce/segments/vip-customers.ts +20 -0
- package/blueprints/mobile-game/emails/_shell.tsx +138 -0
- package/blueprints/mobile-game/emails/day-two-return.tsx +41 -0
- package/blueprints/mobile-game/emails/lapsed-offer.tsx +41 -0
- package/blueprints/mobile-game/emails/lapsed-reminder.tsx +48 -0
- package/blueprints/mobile-game/emails/purchase-receipt.tsx +47 -0
- package/blueprints/mobile-game/emails/spender-thanks.tsx +53 -0
- package/blueprints/mobile-game/emails/stuck-hint.tsx +48 -0
- package/blueprints/mobile-game/emails/tutorial-nudge.tsx +48 -0
- package/blueprints/mobile-game/journeys/_helpers.ts +14 -0
- package/blueprints/mobile-game/journeys/day-two-return.ts +40 -0
- package/blueprints/mobile-game/journeys/first-purchase.ts +34 -0
- package/blueprints/mobile-game/journeys/lapsed-player.ts +39 -0
- package/blueprints/mobile-game/journeys/onboarding.ts +35 -0
- package/blueprints/mobile-game/journeys/spender-care.ts +41 -0
- package/blueprints/mobile-game/journeys/stuck.ts +58 -0
- package/blueprints/mobile-game/scenarios/day-two-return.came-back.json +16 -0
- package/blueprints/mobile-game/scenarios/day-two-return.no-show.json +21 -0
- package/blueprints/mobile-game/scenarios/first-purchase.paid.json +21 -0
- package/blueprints/mobile-game/scenarios/lapsed-player.returned.json +26 -0
- package/blueprints/mobile-game/scenarios/lapsed-player.still-gone.json +30 -0
- package/blueprints/mobile-game/scenarios/onboarding.finished.json +16 -0
- package/blueprints/mobile-game/scenarios/onboarding.stalled.json +21 -0
- package/blueprints/mobile-game/scenarios/spender-care.crossed.json +22 -0
- package/blueprints/mobile-game/scenarios/stuck.cleared.json +16 -0
- package/blueprints/mobile-game/scenarios/stuck.three-fails.json +30 -0
- package/blueprints/mobile-game/segments/active-players.ts +30 -0
- package/blueprints/mobile-game/segments/lapsed-players.ts +29 -0
- package/blueprints/mobile-game/segments/new-players.ts +31 -0
- package/blueprints/mobile-game/segments/paying-players.ts +21 -0
- package/blueprints/saas-trial/emails/_shell.tsx +151 -0
- package/blueprints/saas-trial/emails/activated.tsx +61 -0
- package/blueprints/saas-trial/emails/activation-help.tsx +49 -0
- package/blueprints/saas-trial/emails/activation-nudge.tsx +48 -0
- package/blueprints/saas-trial/emails/invite-your-team.tsx +58 -0
- package/blueprints/saas-trial/emails/trial-converted.tsx +57 -0
- package/blueprints/saas-trial/emails/trial-ended.tsx +52 -0
- package/blueprints/saas-trial/emails/trial-ending-activated.tsx +53 -0
- package/blueprints/saas-trial/emails/trial-ending-unactivated.tsx +54 -0
- package/blueprints/saas-trial/emails/trial-welcome.tsx +57 -0
- package/blueprints/saas-trial/journeys/_helpers.ts +14 -0
- package/blueprints/saas-trial/journeys/activated.ts +42 -0
- package/blueprints/saas-trial/journeys/activation-nudge.ts +44 -0
- package/blueprints/saas-trial/journeys/invite-your-team.ts +42 -0
- package/blueprints/saas-trial/journeys/trial-ended.ts +39 -0
- package/blueprints/saas-trial/journeys/trial-ending.ts +58 -0
- package/blueprints/saas-trial/journeys/trial-welcome.ts +35 -0
- package/blueprints/saas-trial/scenarios/activated.first-project.json +14 -0
- package/blueprints/saas-trial/scenarios/activated.second-project.json +12 -0
- package/blueprints/saas-trial/scenarios/activation-nudge.activated-late.json +16 -0
- package/blueprints/saas-trial/scenarios/activation-nudge.activated.json +8 -0
- package/blueprints/saas-trial/scenarios/activation-nudge.stalled.json +23 -0
- package/blueprints/saas-trial/scenarios/invite-your-team.alone.json +16 -0
- package/blueprints/saas-trial/scenarios/invite-your-team.invited.json +8 -0
- package/blueprints/saas-trial/scenarios/trial-ended.converted.json +16 -0
- package/blueprints/saas-trial/scenarios/trial-ended.expired.json +13 -0
- package/blueprints/saas-trial/scenarios/trial-ending.activated.json +20 -0
- package/blueprints/saas-trial/scenarios/trial-ending.converted.json +12 -0
- package/blueprints/saas-trial/scenarios/trial-ending.unactivated.json +16 -0
- package/blueprints/saas-trial/scenarios/trial-welcome.started.json +13 -0
- package/blueprints/saas-trial/segments/activated-users.ts +25 -0
- package/blueprints/saas-trial/segments/converted.ts +28 -0
- package/blueprints/saas-trial/segments/trialing.ts +26 -0
- package/blueprints/saas-trial/segments/unactivated.ts +27 -0
- package/{examples → blueprints}/winback/journeys/winback.ts +18 -7
- package/blueprints/winback/scenarios/winback.bought-again.json +14 -0
- package/{examples/winback/scenarios/winback.json → blueprints/winback/scenarios/winback.lapsed.json} +2 -5
- package/dist/guest/{driver-poSdZIj8.js → driver-B_pjmy5g.js} +2 -1
- package/dist/guest/driver-emails.js +3 -2
- package/dist/guest/driver.d.ts +1 -1
- package/dist/guest/driver.js +1 -1
- package/dist/guest/{index-EqBCZnpq.d.ts → index-DADBwPaG.d.ts} +63 -12
- package/dist/guest/{journeys-F8Yk8s1P.js → journeys-v2-Djgs8U91.js} +87 -71
- package/dist/guest/journeys.d.ts +1 -1
- package/dist/guest/journeys.js +58 -1
- package/dist/guest/segments.d.ts +81 -0
- package/dist/guest/segments.js +35 -0
- package/dist/index.js +1177 -405
- package/package.json +6 -2
- /package/{examples → blueprints}/abandoned-checkout/emails/abandoned-checkout.tsx +0 -0
- /package/{examples → blueprints}/abandoned-checkout/scenarios/abandoned-checkout.recovered.json +0 -0
- /package/{examples → blueprints}/abandoned-checkout/scenarios/abandoned-checkout.timeout.json +0 -0
- /package/{examples → blueprints}/activity-decay/scenarios/activity-decay.json +0 -0
- /package/{examples → blueprints}/winback/emails/winback.tsx +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineJourney } from "@cowliss/cli/journeys";
|
|
2
|
+
import { str } from "./_helpers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The reorder nudge, timed off the last order. It enrolls a customer once
|
|
6
|
+
* they have ordered twice (`segments/repeat-buyers.ts`), because two orders
|
|
7
|
+
* are what tells you they buy this again at all, and then re-arms itself
|
|
8
|
+
* for as long as they keep buying.
|
|
9
|
+
*
|
|
10
|
+
* 45 days is the consumable cycle: long enough that the mail lands when
|
|
11
|
+
* they are running low rather than while the last one is still full. Set
|
|
12
|
+
* it to whatever your product actually lasts.
|
|
13
|
+
*
|
|
14
|
+
* `api.restart()` rather than a loop. Restarting starts the wait again
|
|
15
|
+
* with an empty journal; looping would journal one entry per order and
|
|
16
|
+
* your best customer is then the one whose execution hits the call cap.
|
|
17
|
+
*
|
|
18
|
+
* `marketing` with no cooldown: the restart is what makes it recur, and it
|
|
19
|
+
* only ever recurs off an order the customer placed.
|
|
20
|
+
*/
|
|
21
|
+
export default defineJourney({
|
|
22
|
+
trigger: { segment: "repeat-buyers" },
|
|
23
|
+
purpose: "marketing",
|
|
24
|
+
from: "Acme <hello@example.com>",
|
|
25
|
+
description: "Reminds a regular customer to reorder before they run out.",
|
|
26
|
+
tags: ["shop", "retention"],
|
|
27
|
+
run: async (_event, api) => {
|
|
28
|
+
if (await api.waitForEvent("order.placed", { timeout: "45d" })) {
|
|
29
|
+
// They reordered on their own. Start the clock again from this order,
|
|
30
|
+
// with a fresh journal.
|
|
31
|
+
await api.restart();
|
|
32
|
+
}
|
|
33
|
+
const me = await api.profile.get();
|
|
34
|
+
await api.send.email({
|
|
35
|
+
template: "replenishment",
|
|
36
|
+
props: { firstName: str(me.traits.firstName) },
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineJourney } from "@cowliss/cli/journeys";
|
|
2
|
+
import { str } from "./_helpers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A customer crosses the lifetime-value threshold and hears about it: one
|
|
6
|
+
* thank-you carrying one perk.
|
|
7
|
+
*
|
|
8
|
+
* It enrolls from `segments/vip-customers.ts`, where the threshold is; the
|
|
9
|
+
* perk itself is in the mail. Give it something that costs you a little
|
|
10
|
+
* and reads as recognition, and keep it to one: a list of benefits reads
|
|
11
|
+
* as a tier announcement, and nobody thanked you for a tier.
|
|
12
|
+
*
|
|
13
|
+
* `marketing` with no cooldown, so crossing the threshold is worth one
|
|
14
|
+
* mail, once.
|
|
15
|
+
*/
|
|
16
|
+
export default defineJourney({
|
|
17
|
+
trigger: { segment: "vip-customers" },
|
|
18
|
+
purpose: "marketing",
|
|
19
|
+
from: "Acme <hello@example.com>",
|
|
20
|
+
description: "Thanks a customer who has spent past the VIP threshold.",
|
|
21
|
+
tags: ["shop", "value"],
|
|
22
|
+
run: async (_event, api) => {
|
|
23
|
+
const me = await api.profile.get();
|
|
24
|
+
await api.send.email({
|
|
25
|
+
template: "vip-thanks",
|
|
26
|
+
props: { firstName: str(me.traits.firstName) },
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineJourney } from "@cowliss/cli/journeys";
|
|
2
|
+
import { str } from "./_helpers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Welcome: someone with an address who has looked at a product and has
|
|
6
|
+
* never ordered. One mail, sent once.
|
|
7
|
+
*
|
|
8
|
+
* The trigger is a segment written out here rather than in `segments/`,
|
|
9
|
+
* because no other journey wants this audience (ADR 0023). The four in
|
|
10
|
+
* `segments/` are shared; this one is the journey's own.
|
|
11
|
+
*
|
|
12
|
+
* A product view rather than the signup alone. An address by itself only
|
|
13
|
+
* says a form was filled in, and the mail reads better sent to someone who
|
|
14
|
+
* has seen what you sell. Drop that predicate and the welcome goes out on
|
|
15
|
+
* the identify call instead.
|
|
16
|
+
*
|
|
17
|
+
* `marketing` with no `enrollment`, so one person is welcomed once, ever,
|
|
18
|
+
* however many times they leave and re-enter the audience.
|
|
19
|
+
*/
|
|
20
|
+
export default defineJourney({
|
|
21
|
+
trigger: {
|
|
22
|
+
segment: {
|
|
23
|
+
match: "all",
|
|
24
|
+
predicates: [
|
|
25
|
+
{ kind: "trait", name: "email", op: "exists" },
|
|
26
|
+
{ kind: "event", name: "product.viewed", op: "performed", atLeast: 1 },
|
|
27
|
+
{ kind: "event", name: "order.placed", op: "notPerformed" },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
purpose: "marketing",
|
|
32
|
+
from: "Acme <hello@example.com>",
|
|
33
|
+
description: "Introduces the store to a shopper who has not ordered yet.",
|
|
34
|
+
tags: ["shop", "lifecycle"],
|
|
35
|
+
run: async (_event, api) => {
|
|
36
|
+
const me = await api.profile.get();
|
|
37
|
+
await api.send.email({
|
|
38
|
+
template: "welcome",
|
|
39
|
+
props: { firstName: str(me.traits.firstName) },
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineJourney } from "@cowliss/cli/journeys";
|
|
2
|
+
import { str } from "./_helpers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Win-back, in two touches, stopping the moment an order arrives.
|
|
6
|
+
*
|
|
7
|
+
* It enrolls from `segments/lapsed.ts`, so the definition of lapsed lives
|
|
8
|
+
* in one file and this journey only decides what to do about it. Change
|
|
9
|
+
* the 120 days there, not here.
|
|
10
|
+
*
|
|
11
|
+
* The reminder goes first and carries no discount: plenty of people come
|
|
12
|
+
* back for the reminder alone, and a store that opens with money off
|
|
13
|
+
* teaches its customers to wait for it. The offer is the second touch, 14
|
|
14
|
+
* days later, and only for someone who ignored the first.
|
|
15
|
+
*
|
|
16
|
+
* `marketing` with a 180-day cooldown, so a customer who lapses again next
|
|
17
|
+
* year is won back again, and one who lapses twice in a season is not.
|
|
18
|
+
*/
|
|
19
|
+
export default defineJourney({
|
|
20
|
+
trigger: { segment: "lapsed" },
|
|
21
|
+
purpose: "marketing",
|
|
22
|
+
enrollment: { cooldown: "180d" },
|
|
23
|
+
from: "Acme <hello@example.com>",
|
|
24
|
+
description: "Invites a customer back who has not ordered in months.",
|
|
25
|
+
tags: ["shop", "retention"],
|
|
26
|
+
run: async (_event, api) => {
|
|
27
|
+
const me = await api.profile.get();
|
|
28
|
+
const props = { firstName: str(me.traits.firstName) };
|
|
29
|
+
await api.send.email({ template: "winback-reminder", props });
|
|
30
|
+
if (await api.waitForEvent("order.placed", { timeout: "14d" })) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
await api.send.email({ template: "winback-offer", props });
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": {
|
|
3
|
+
"id": "usr_1",
|
|
4
|
+
"traits": {
|
|
5
|
+
"email": "ada@example.com",
|
|
6
|
+
"firstName": "Ada"
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"events": [],
|
|
10
|
+
"expect": [
|
|
11
|
+
{
|
|
12
|
+
"activity": "sendEmail",
|
|
13
|
+
"input": {
|
|
14
|
+
"template": "abandoned-checkout",
|
|
15
|
+
"props": {
|
|
16
|
+
"firstName": "Ada"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"activity": "sendEmail",
|
|
22
|
+
"input": {
|
|
23
|
+
"template": "abandoned-checkout-last-call",
|
|
24
|
+
"props": {
|
|
25
|
+
"firstName": "Ada"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": {
|
|
3
|
+
"id": "usr_1",
|
|
4
|
+
"traits": {
|
|
5
|
+
"email": "ada@example.com",
|
|
6
|
+
"firstName": "Ada"
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"events": [
|
|
10
|
+
{
|
|
11
|
+
"at": "1h",
|
|
12
|
+
"event": "order.placed"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"expect": [
|
|
16
|
+
{
|
|
17
|
+
"activity": "setTrait",
|
|
18
|
+
"input": {
|
|
19
|
+
"key": "cartRecovered",
|
|
20
|
+
"value": true
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": {
|
|
3
|
+
"id": "usr_1",
|
|
4
|
+
"traits": {
|
|
5
|
+
"email": "ada@example.com",
|
|
6
|
+
"firstName": "Ada"
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"events": [
|
|
10
|
+
{
|
|
11
|
+
"at": "30h",
|
|
12
|
+
"event": "order.placed"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"expect": [
|
|
16
|
+
{
|
|
17
|
+
"activity": "sendEmail",
|
|
18
|
+
"input": {
|
|
19
|
+
"template": "abandoned-checkout",
|
|
20
|
+
"props": {
|
|
21
|
+
"firstName": "Ada"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"activity": "setTrait",
|
|
27
|
+
"input": {
|
|
28
|
+
"key": "cartRecovered",
|
|
29
|
+
"value": true
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": {
|
|
3
|
+
"id": "usr_1",
|
|
4
|
+
"traits": {
|
|
5
|
+
"email": "ada@example.com",
|
|
6
|
+
"firstName": "Ada"
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"events": [
|
|
10
|
+
{
|
|
11
|
+
"at": "2d",
|
|
12
|
+
"event": "order.delivered"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"expect": [
|
|
16
|
+
{
|
|
17
|
+
"activity": "sendEmail",
|
|
18
|
+
"input": {
|
|
19
|
+
"template": "order-thanks",
|
|
20
|
+
"props": {
|
|
21
|
+
"firstName": "Ada"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"activity": "sendEmail",
|
|
27
|
+
"input": {
|
|
28
|
+
"template": "review-request",
|
|
29
|
+
"props": {
|
|
30
|
+
"firstName": "Ada"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": {
|
|
3
|
+
"id": "usr_1",
|
|
4
|
+
"traits": {
|
|
5
|
+
"email": "ada@example.com",
|
|
6
|
+
"firstName": "Ada"
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"events": [],
|
|
10
|
+
"expect": [
|
|
11
|
+
{
|
|
12
|
+
"activity": "sendEmail",
|
|
13
|
+
"input": {
|
|
14
|
+
"template": "order-thanks",
|
|
15
|
+
"props": {
|
|
16
|
+
"firstName": "Ada"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": {
|
|
3
|
+
"id": "usr_1",
|
|
4
|
+
"traits": {
|
|
5
|
+
"email": "ada@example.com",
|
|
6
|
+
"firstName": "Ada"
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"events": [],
|
|
10
|
+
"expect": [
|
|
11
|
+
{
|
|
12
|
+
"activity": "sendEmail",
|
|
13
|
+
"input": {
|
|
14
|
+
"template": "replenishment",
|
|
15
|
+
"props": {
|
|
16
|
+
"firstName": "Ada"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": {
|
|
3
|
+
"id": "usr_1",
|
|
4
|
+
"traits": {
|
|
5
|
+
"email": "ada@example.com",
|
|
6
|
+
"firstName": "Ada",
|
|
7
|
+
"lifetimeValue": 780
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"events": [],
|
|
11
|
+
"expect": [
|
|
12
|
+
{
|
|
13
|
+
"activity": "sendEmail",
|
|
14
|
+
"input": {
|
|
15
|
+
"template": "vip-thanks",
|
|
16
|
+
"props": {
|
|
17
|
+
"firstName": "Ada"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": {
|
|
3
|
+
"id": "usr_1",
|
|
4
|
+
"traits": {
|
|
5
|
+
"email": "ada@example.com",
|
|
6
|
+
"firstName": "Ada"
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"events": [],
|
|
10
|
+
"expect": [
|
|
11
|
+
{
|
|
12
|
+
"activity": "sendEmail",
|
|
13
|
+
"input": {
|
|
14
|
+
"template": "welcome",
|
|
15
|
+
"props": {
|
|
16
|
+
"firstName": "Ada"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": {
|
|
3
|
+
"id": "usr_1",
|
|
4
|
+
"traits": {
|
|
5
|
+
"email": "ada@example.com",
|
|
6
|
+
"firstName": "Ada"
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"events": [
|
|
10
|
+
{
|
|
11
|
+
"at": "3d",
|
|
12
|
+
"event": "order.placed"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"expect": [
|
|
16
|
+
{
|
|
17
|
+
"activity": "sendEmail",
|
|
18
|
+
"input": {
|
|
19
|
+
"template": "winback-reminder",
|
|
20
|
+
"props": {
|
|
21
|
+
"firstName": "Ada"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": {
|
|
3
|
+
"id": "usr_1",
|
|
4
|
+
"traits": {
|
|
5
|
+
"email": "ada@example.com",
|
|
6
|
+
"firstName": "Ada"
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"events": [],
|
|
10
|
+
"expect": [
|
|
11
|
+
{
|
|
12
|
+
"activity": "sendEmail",
|
|
13
|
+
"input": {
|
|
14
|
+
"template": "winback-reminder",
|
|
15
|
+
"props": {
|
|
16
|
+
"firstName": "Ada"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"activity": "sendEmail",
|
|
22
|
+
"input": {
|
|
23
|
+
"template": "winback-offer",
|
|
24
|
+
"props": {
|
|
25
|
+
"firstName": "Ada"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineSegment } from "@cowliss/cli/segments";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bought exactly once: one `order.placed` and no second one. "Exactly one"
|
|
5
|
+
* is two predicates, because a predicate counts upwards — performed at
|
|
6
|
+
* least once, and NOT performed at least twice.
|
|
7
|
+
*
|
|
8
|
+
* A file rather than a trigger's inline definition (ADR 0023) because the
|
|
9
|
+
* dashboard reads it too: this is the audience you slice your users by
|
|
10
|
+
* while the journeys that enroll from it run.
|
|
11
|
+
*
|
|
12
|
+
* `order.placed` is the name most stores already send. If yours sends
|
|
13
|
+
* `purchase` or `Order Completed`, change the two names here and in the
|
|
14
|
+
* other segment files rather than renaming events in your own app.
|
|
15
|
+
*/
|
|
16
|
+
export default defineSegment({
|
|
17
|
+
description: "Placed one order and has not ordered again.",
|
|
18
|
+
tags: ["shop", "lifecycle"],
|
|
19
|
+
match: "all",
|
|
20
|
+
predicates: [
|
|
21
|
+
{ kind: "event", name: "order.placed", op: "performed", atLeast: 1 },
|
|
22
|
+
{ kind: "event", name: "order.placed", op: "notPerformed", atLeast: 2 },
|
|
23
|
+
],
|
|
24
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineSegment } from "@cowliss/cli/segments";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bought before, and nothing in 120 days. Both predicates are needed: the
|
|
5
|
+
* first keeps out everyone who never bought at all (they belong to the
|
|
6
|
+
* welcome journey), and the second is the silence that makes a customer
|
|
7
|
+
* lapsed.
|
|
8
|
+
*
|
|
9
|
+
* 120 days is the number to argue with. It should be longer than the time
|
|
10
|
+
* your customers normally leave between orders, or the win-back journey
|
|
11
|
+
* chases people who are simply between purchases.
|
|
12
|
+
*/
|
|
13
|
+
export default defineSegment({
|
|
14
|
+
description: "Ordered at some point, and nothing in the last 120 days.",
|
|
15
|
+
tags: ["shop", "lifecycle"],
|
|
16
|
+
match: "all",
|
|
17
|
+
predicates: [
|
|
18
|
+
{ kind: "event", name: "order.placed", op: "performed", atLeast: 1 },
|
|
19
|
+
{
|
|
20
|
+
kind: "event",
|
|
21
|
+
name: "order.placed",
|
|
22
|
+
op: "notPerformed",
|
|
23
|
+
atLeast: 1,
|
|
24
|
+
withinDays: 120,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineSegment } from "@cowliss/cli/segments";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ordered twice or more. The second order is where a shopper becomes a
|
|
5
|
+
* customer, which is why it is the threshold: raise the 2 if your store
|
|
6
|
+
* sells something people buy in threes before they settle into a habit.
|
|
7
|
+
*
|
|
8
|
+
* The replenishment journey enrolls from this one, and the dashboard
|
|
9
|
+
* counts it beside the other three.
|
|
10
|
+
*/
|
|
11
|
+
export default defineSegment({
|
|
12
|
+
description: "Placed at least two orders.",
|
|
13
|
+
tags: ["shop", "lifecycle"],
|
|
14
|
+
match: "all",
|
|
15
|
+
predicates: [
|
|
16
|
+
{ kind: "event", name: "order.placed", op: "performed", atLeast: 2 },
|
|
17
|
+
],
|
|
18
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineSegment } from "@cowliss/cli/segments";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Spent 500 or more, all time. A trait rather than a count of events:
|
|
5
|
+
* lifetime value is a number your store already knows, so send it on
|
|
6
|
+
* identify as `lifetimeValue` and this segment follows it.
|
|
7
|
+
*
|
|
8
|
+
* The threshold is the literal below. Pick one only a small share of your
|
|
9
|
+
* customers reach, because the VIP journey promises them attention.
|
|
10
|
+
*
|
|
11
|
+
* Named `vip-customers` and not `vip`: a segment name and a journey key
|
|
12
|
+
* share one namespace per organization, and the journey that enrolls from
|
|
13
|
+
* this is called `vip`.
|
|
14
|
+
*/
|
|
15
|
+
export default defineSegment({
|
|
16
|
+
description: "Lifetime spend of 500 or more.",
|
|
17
|
+
tags: ["shop", "value"],
|
|
18
|
+
match: "all",
|
|
19
|
+
predicates: [{ kind: "trait", name: "lifetimeValue", op: "gte", value: 500 }],
|
|
20
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Body,
|
|
3
|
+
Button,
|
|
4
|
+
Container,
|
|
5
|
+
Head,
|
|
6
|
+
Heading,
|
|
7
|
+
Hr,
|
|
8
|
+
Html,
|
|
9
|
+
Preview,
|
|
10
|
+
Text,
|
|
11
|
+
} from "@react-email/components";
|
|
12
|
+
import type { CSSProperties, ReactElement, ReactNode } from "react";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The chrome all seven mails in this blueprint share: one look for the
|
|
16
|
+
* programme rather than seven copies of the same inline styles. An
|
|
17
|
+
* `_`-prefixed file under `emails/` is a shared module, not a template:
|
|
18
|
+
* `cow build` skips it in discovery and bundles it into whichever template
|
|
19
|
+
* imports it.
|
|
20
|
+
*
|
|
21
|
+
* Inline styles only, no Tailwind and no stylesheet, because email clients
|
|
22
|
+
* strip both.
|
|
23
|
+
*
|
|
24
|
+
* The four constants below are the first thing to change. `GAME_LINK` is
|
|
25
|
+
* the one that matters most: every mail here sends the player back into
|
|
26
|
+
* the game, so make it a link your app opens directly (a universal link on
|
|
27
|
+
* iOS, an App Link on Android) rather than a store page a player who
|
|
28
|
+
* already installed the game has to walk through. The postal address is
|
|
29
|
+
* not decoration either: a marketing mail is required to carry one in most
|
|
30
|
+
* of the world, and Cowliss does not know yours. The unsubscribe link is
|
|
31
|
+
* the one thing you do not add here — the send path appends it to every
|
|
32
|
+
* marketing mail, and one that is already in the body is left alone.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export const GAME_NAME = "Acme Quest";
|
|
36
|
+
export const GAME_URL = "https://example.com";
|
|
37
|
+
export const GAME_LINK = "https://example.com/play";
|
|
38
|
+
export const STUDIO_ADDRESS = "Acme Games, 1 Example Street, Springfield";
|
|
39
|
+
|
|
40
|
+
export const bodyStyle: CSSProperties = {
|
|
41
|
+
backgroundColor: "#f4f4f5",
|
|
42
|
+
fontFamily:
|
|
43
|
+
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
|
|
44
|
+
margin: "0",
|
|
45
|
+
padding: "32px 0",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const containerStyle: CSSProperties = {
|
|
49
|
+
margin: "0 auto",
|
|
50
|
+
padding: "40px 36px",
|
|
51
|
+
maxWidth: "560px",
|
|
52
|
+
backgroundColor: "#ffffff",
|
|
53
|
+
borderRadius: "14px",
|
|
54
|
+
border: "1px solid #e4e4e7",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const headingStyle: CSSProperties = {
|
|
58
|
+
fontSize: "24px",
|
|
59
|
+
lineHeight: "1.25",
|
|
60
|
+
fontWeight: 700,
|
|
61
|
+
letterSpacing: "-0.02em",
|
|
62
|
+
color: "#09090b",
|
|
63
|
+
margin: "0 0 18px",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const textStyle: CSSProperties = {
|
|
67
|
+
fontSize: "16px",
|
|
68
|
+
lineHeight: "1.7",
|
|
69
|
+
color: "#27272a",
|
|
70
|
+
margin: "0 0 16px",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const buttonStyle: CSSProperties = {
|
|
74
|
+
display: "inline-block",
|
|
75
|
+
marginTop: "8px",
|
|
76
|
+
marginBottom: "16px",
|
|
77
|
+
padding: "14px 26px",
|
|
78
|
+
borderRadius: "10px",
|
|
79
|
+
backgroundColor: "#09090b",
|
|
80
|
+
color: "#ffffff",
|
|
81
|
+
fontSize: "15px",
|
|
82
|
+
fontWeight: 600,
|
|
83
|
+
lineHeight: 1,
|
|
84
|
+
textDecoration: "none",
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const hrStyle: CSSProperties = {
|
|
88
|
+
borderColor: "#e4e4e7",
|
|
89
|
+
borderStyle: "solid",
|
|
90
|
+
borderWidth: "1px 0 0 0",
|
|
91
|
+
margin: "24px 0",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const footerStyle: CSSProperties = {
|
|
95
|
+
fontSize: "13px",
|
|
96
|
+
lineHeight: "1.6",
|
|
97
|
+
color: "#52525b",
|
|
98
|
+
margin: "0",
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export function GameEmail(props: {
|
|
102
|
+
preview: string;
|
|
103
|
+
heading: string;
|
|
104
|
+
children: ReactNode;
|
|
105
|
+
}): ReactElement {
|
|
106
|
+
return (
|
|
107
|
+
<Html dir="ltr" lang="en">
|
|
108
|
+
<Head>
|
|
109
|
+
<meta content="text/html; charset=UTF-8" httpEquiv="Content-Type" />
|
|
110
|
+
<meta name="x-apple-disable-message-reformatting" />
|
|
111
|
+
<meta name="color-scheme" content="light" />
|
|
112
|
+
<meta name="supported-color-schemes" content="light" />
|
|
113
|
+
</Head>
|
|
114
|
+
<Preview>{props.preview}</Preview>
|
|
115
|
+
<Body style={bodyStyle}>
|
|
116
|
+
<Container style={containerStyle}>
|
|
117
|
+
<Heading style={headingStyle}>{props.heading}</Heading>
|
|
118
|
+
{props.children}
|
|
119
|
+
<Hr style={hrStyle} />
|
|
120
|
+
<Text style={footerStyle}>{STUDIO_ADDRESS}</Text>
|
|
121
|
+
</Container>
|
|
122
|
+
</Body>
|
|
123
|
+
</Html>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The one action a mail asks for. Every mail here has at most one. */
|
|
128
|
+
export function Cta(props: { href: string; children: string }): ReactElement {
|
|
129
|
+
return (
|
|
130
|
+
<Button style={buttonStyle} href={props.href}>
|
|
131
|
+
{props.children}
|
|
132
|
+
</Button>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** "Hi Ada," when the journey passed a first name, "Hi," when it did not. */
|
|
137
|
+
export const greeting = (firstName?: string): string =>
|
|
138
|
+
firstName ? `Hi ${firstName},` : "Hi,";
|