@cowliss/cli 0.10.0 → 0.12.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 (117) hide show
  1. package/{examples → blueprints}/abandoned-checkout/journeys/abandoned-checkout.ts +1 -1
  2. package/{examples → blueprints}/activity-decay/journeys/activity-decay.ts +1 -1
  3. package/blueprints/ecommerce/emails/_shell.tsx +133 -0
  4. package/blueprints/ecommerce/emails/abandoned-checkout-last-call.tsx +46 -0
  5. package/blueprints/ecommerce/emails/abandoned-checkout.tsx +46 -0
  6. package/blueprints/ecommerce/emails/order-thanks.tsx +44 -0
  7. package/blueprints/ecommerce/emails/replenishment.tsx +40 -0
  8. package/blueprints/ecommerce/emails/review-request.tsx +44 -0
  9. package/blueprints/ecommerce/emails/vip-thanks.tsx +51 -0
  10. package/blueprints/ecommerce/emails/welcome.tsx +48 -0
  11. package/blueprints/ecommerce/emails/winback-offer.tsx +41 -0
  12. package/blueprints/ecommerce/emails/winback-reminder.tsx +45 -0
  13. package/blueprints/ecommerce/journeys/_helpers.ts +14 -0
  14. package/blueprints/ecommerce/journeys/abandoned-checkout.ts +44 -0
  15. package/blueprints/ecommerce/journeys/first-order-thanks.ts +40 -0
  16. package/blueprints/ecommerce/journeys/replenishment.ts +39 -0
  17. package/blueprints/ecommerce/journeys/vip.ts +29 -0
  18. package/blueprints/ecommerce/journeys/welcome.ts +42 -0
  19. package/blueprints/ecommerce/journeys/winback.ts +35 -0
  20. package/blueprints/ecommerce/scenarios/abandoned-checkout.last-call.json +30 -0
  21. package/blueprints/ecommerce/scenarios/abandoned-checkout.recovered.json +24 -0
  22. package/blueprints/ecommerce/scenarios/abandoned-checkout.reminded.json +33 -0
  23. package/blueprints/ecommerce/scenarios/first-order-thanks.delivered.json +35 -0
  24. package/blueprints/ecommerce/scenarios/first-order-thanks.never-delivered.json +21 -0
  25. package/blueprints/ecommerce/scenarios/replenishment.due.json +21 -0
  26. package/blueprints/ecommerce/scenarios/replenishment.reordered.json +20 -0
  27. package/blueprints/ecommerce/scenarios/vip.crossed.json +22 -0
  28. package/blueprints/ecommerce/scenarios/welcome.new-shopper.json +21 -0
  29. package/blueprints/ecommerce/scenarios/winback.returned.json +26 -0
  30. package/blueprints/ecommerce/scenarios/winback.still-gone.json +30 -0
  31. package/blueprints/ecommerce/segments/first-time-buyers.ts +24 -0
  32. package/blueprints/ecommerce/segments/lapsed.ts +27 -0
  33. package/blueprints/ecommerce/segments/repeat-buyers.ts +18 -0
  34. package/blueprints/ecommerce/segments/vip-customers.ts +20 -0
  35. package/blueprints/mobile-game/emails/_shell.tsx +138 -0
  36. package/blueprints/mobile-game/emails/day-two-return.tsx +41 -0
  37. package/blueprints/mobile-game/emails/lapsed-offer.tsx +41 -0
  38. package/blueprints/mobile-game/emails/lapsed-reminder.tsx +48 -0
  39. package/blueprints/mobile-game/emails/purchase-receipt.tsx +47 -0
  40. package/blueprints/mobile-game/emails/spender-thanks.tsx +53 -0
  41. package/blueprints/mobile-game/emails/stuck-hint.tsx +48 -0
  42. package/blueprints/mobile-game/emails/tutorial-nudge.tsx +48 -0
  43. package/blueprints/mobile-game/journeys/_helpers.ts +14 -0
  44. package/blueprints/mobile-game/journeys/day-two-return.ts +40 -0
  45. package/blueprints/mobile-game/journeys/first-purchase.ts +34 -0
  46. package/blueprints/mobile-game/journeys/lapsed-player.ts +39 -0
  47. package/blueprints/mobile-game/journeys/onboarding.ts +35 -0
  48. package/blueprints/mobile-game/journeys/spender-care.ts +41 -0
  49. package/blueprints/mobile-game/journeys/stuck.ts +58 -0
  50. package/blueprints/mobile-game/scenarios/day-two-return.came-back.json +16 -0
  51. package/blueprints/mobile-game/scenarios/day-two-return.no-show.json +21 -0
  52. package/blueprints/mobile-game/scenarios/first-purchase.paid.json +21 -0
  53. package/blueprints/mobile-game/scenarios/lapsed-player.returned.json +26 -0
  54. package/blueprints/mobile-game/scenarios/lapsed-player.still-gone.json +30 -0
  55. package/blueprints/mobile-game/scenarios/onboarding.finished.json +16 -0
  56. package/blueprints/mobile-game/scenarios/onboarding.stalled.json +21 -0
  57. package/blueprints/mobile-game/scenarios/spender-care.crossed.json +22 -0
  58. package/blueprints/mobile-game/scenarios/stuck.cleared.json +16 -0
  59. package/blueprints/mobile-game/scenarios/stuck.three-fails.json +30 -0
  60. package/blueprints/mobile-game/segments/active-players.ts +30 -0
  61. package/blueprints/mobile-game/segments/lapsed-players.ts +29 -0
  62. package/blueprints/mobile-game/segments/new-players.ts +31 -0
  63. package/blueprints/mobile-game/segments/paying-players.ts +21 -0
  64. package/blueprints/saas-trial/emails/_shell.tsx +151 -0
  65. package/blueprints/saas-trial/emails/activated.tsx +61 -0
  66. package/blueprints/saas-trial/emails/activation-help.tsx +49 -0
  67. package/blueprints/saas-trial/emails/activation-nudge.tsx +48 -0
  68. package/blueprints/saas-trial/emails/invite-your-team.tsx +58 -0
  69. package/blueprints/saas-trial/emails/trial-converted.tsx +57 -0
  70. package/blueprints/saas-trial/emails/trial-ended.tsx +52 -0
  71. package/blueprints/saas-trial/emails/trial-ending-activated.tsx +53 -0
  72. package/blueprints/saas-trial/emails/trial-ending-unactivated.tsx +54 -0
  73. package/blueprints/saas-trial/emails/trial-welcome.tsx +57 -0
  74. package/blueprints/saas-trial/journeys/_helpers.ts +14 -0
  75. package/blueprints/saas-trial/journeys/activated.ts +42 -0
  76. package/blueprints/saas-trial/journeys/activation-nudge.ts +44 -0
  77. package/blueprints/saas-trial/journeys/invite-your-team.ts +42 -0
  78. package/blueprints/saas-trial/journeys/trial-ended.ts +39 -0
  79. package/blueprints/saas-trial/journeys/trial-ending.ts +58 -0
  80. package/blueprints/saas-trial/journeys/trial-welcome.ts +35 -0
  81. package/blueprints/saas-trial/scenarios/activated.first-project.json +14 -0
  82. package/blueprints/saas-trial/scenarios/activated.second-project.json +12 -0
  83. package/blueprints/saas-trial/scenarios/activation-nudge.activated-late.json +16 -0
  84. package/blueprints/saas-trial/scenarios/activation-nudge.activated.json +8 -0
  85. package/blueprints/saas-trial/scenarios/activation-nudge.stalled.json +23 -0
  86. package/blueprints/saas-trial/scenarios/invite-your-team.alone.json +16 -0
  87. package/blueprints/saas-trial/scenarios/invite-your-team.invited.json +8 -0
  88. package/blueprints/saas-trial/scenarios/trial-ended.converted.json +16 -0
  89. package/blueprints/saas-trial/scenarios/trial-ended.expired.json +13 -0
  90. package/blueprints/saas-trial/scenarios/trial-ending.activated.json +20 -0
  91. package/blueprints/saas-trial/scenarios/trial-ending.converted.json +12 -0
  92. package/blueprints/saas-trial/scenarios/trial-ending.unactivated.json +16 -0
  93. package/blueprints/saas-trial/scenarios/trial-welcome.started.json +13 -0
  94. package/blueprints/saas-trial/segments/activated-users.ts +25 -0
  95. package/blueprints/saas-trial/segments/converted.ts +28 -0
  96. package/blueprints/saas-trial/segments/trialing.ts +26 -0
  97. package/blueprints/saas-trial/segments/unactivated.ts +27 -0
  98. package/{examples → blueprints}/winback/journeys/winback.ts +18 -7
  99. package/blueprints/winback/scenarios/winback.bought-again.json +14 -0
  100. package/{examples/winback/scenarios/winback.json → blueprints/winback/scenarios/winback.lapsed.json} +2 -5
  101. package/dist/guest/{driver-poSdZIj8.js → driver-B_pjmy5g.js} +2 -1
  102. package/dist/guest/driver-emails.js +3 -2
  103. package/dist/guest/driver.d.ts +1 -1
  104. package/dist/guest/driver.js +1 -1
  105. package/dist/guest/{index-EqBCZnpq.d.ts → index-DADBwPaG.d.ts} +63 -12
  106. package/dist/guest/{journeys-F8Yk8s1P.js → journeys-v2-Djgs8U91.js} +87 -71
  107. package/dist/guest/journeys.d.ts +1 -1
  108. package/dist/guest/journeys.js +58 -1
  109. package/dist/guest/segments.d.ts +81 -0
  110. package/dist/guest/segments.js +35 -0
  111. package/dist/index.js +1476 -638
  112. package/package.json +6 -2
  113. /package/{examples → blueprints}/abandoned-checkout/emails/abandoned-checkout.tsx +0 -0
  114. /package/{examples → blueprints}/abandoned-checkout/scenarios/abandoned-checkout.recovered.json +0 -0
  115. /package/{examples → blueprints}/abandoned-checkout/scenarios/abandoned-checkout.timeout.json +0 -0
  116. /package/{examples → blueprints}/activity-decay/scenarios/activity-decay.json +0 -0
  117. /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,20 @@
1
+ {
2
+ "user": {
3
+ "id": "usr_1",
4
+ "traits": {
5
+ "email": "ada@example.com",
6
+ "firstName": "Ada"
7
+ }
8
+ },
9
+ "events": [
10
+ {
11
+ "at": "20d",
12
+ "event": "order.placed"
13
+ }
14
+ ],
15
+ "expect": [
16
+ {
17
+ "activity": "restart"
18
+ }
19
+ ]
20
+ }
@@ -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,";