@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.
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 +1177 -405
  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,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": "2d",
12
+ "event": "session.started"
13
+ }
14
+ ],
15
+ "expect": [
16
+ {
17
+ "activity": "sendEmail",
18
+ "input": {
19
+ "template": "lapsed-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": "lapsed-reminder",
15
+ "props": {
16
+ "firstName": "Ada"
17
+ }
18
+ }
19
+ },
20
+ {
21
+ "activity": "sendEmail",
22
+ "input": {
23
+ "template": "lapsed-offer",
24
+ "props": {
25
+ "firstName": "Ada"
26
+ }
27
+ }
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "user": {
3
+ "id": "usr_1",
4
+ "traits": {
5
+ "email": "ada@example.com",
6
+ "firstName": "Ada"
7
+ }
8
+ },
9
+ "events": [
10
+ {
11
+ "at": "3h",
12
+ "event": "tutorial.completed"
13
+ }
14
+ ],
15
+ "expect": []
16
+ }
@@ -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": "tutorial-nudge",
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
+ "lifetimeSpend": 120
8
+ }
9
+ },
10
+ "events": [],
11
+ "expect": [
12
+ {
13
+ "activity": "sendEmail",
14
+ "input": {
15
+ "template": "spender-thanks",
16
+ "props": {
17
+ "firstName": "Ada"
18
+ }
19
+ }
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "user": {
3
+ "id": "usr_1",
4
+ "traits": {
5
+ "email": "ada@example.com",
6
+ "firstName": "Ada"
7
+ }
8
+ },
9
+ "events": [
10
+ {
11
+ "at": "15m",
12
+ "event": "level.completed"
13
+ }
14
+ ],
15
+ "expect": []
16
+ }
@@ -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
+ {
11
+ "at": "20m",
12
+ "event": "level.failed"
13
+ },
14
+ {
15
+ "at": "50m",
16
+ "event": "level.failed"
17
+ }
18
+ ],
19
+ "expect": [
20
+ {
21
+ "activity": "sendEmail",
22
+ "input": {
23
+ "template": "stuck-hint",
24
+ "props": {
25
+ "firstName": "Ada"
26
+ }
27
+ }
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,30 @@
1
+ import { defineSegment } from "@cowliss/cli/segments";
2
+
3
+ /**
4
+ * Three sessions in the last 7 days: the habit, measured the way a game
5
+ * measures one. Frequency rather than spend, because a player who opens
6
+ * the game most days is the one who is still there next month, whatever
7
+ * they have paid.
8
+ *
9
+ * No journey in this blueprint enrolls from this audience, and that is on
10
+ * purpose. It is the number you watch and the slice you read the others
11
+ * against — how many of your new players became regulars, how many of your
12
+ * spenders still play — and a healthy game mails it nothing at all.
13
+ *
14
+ * Both literals are yours: 3 sessions in 7 days is a habit for a game
15
+ * people play on the bus, and far too little for one they play nightly.
16
+ */
17
+ export default defineSegment({
18
+ description: "Played at least three times in the last week.",
19
+ tags: ["game", "engagement"],
20
+ match: "all",
21
+ predicates: [
22
+ {
23
+ kind: "event",
24
+ name: "session.started",
25
+ op: "performed",
26
+ atLeast: 3,
27
+ withinDays: 7,
28
+ },
29
+ ],
30
+ });
@@ -0,0 +1,29 @@
1
+ import { defineSegment } from "@cowliss/cli/segments";
2
+
3
+ /**
4
+ * Played before, and nothing in 30 days. Both predicates are needed: the
5
+ * first keeps out everyone who installed and never played (they are the
6
+ * onboarding journey's problem), and the second is the silence that makes
7
+ * a player lapsed.
8
+ *
9
+ * 30 days is the number to argue with, and for a game it is shorter than
10
+ * you think. A session-a-day game has lost someone after a week; a puzzle
11
+ * game people dip into on holiday has not lost them after two months.
12
+ * Set it longer than the gap your own players normally leave between
13
+ * sessions, or the return offer chases people who were coming back anyway.
14
+ */
15
+ export default defineSegment({
16
+ description: "Played at some point, and nothing in the last 30 days.",
17
+ tags: ["game", "lifecycle"],
18
+ match: "all",
19
+ predicates: [
20
+ { kind: "event", name: "session.started", op: "performed", atLeast: 1 },
21
+ {
22
+ kind: "event",
23
+ name: "session.started",
24
+ op: "notPerformed",
25
+ atLeast: 1,
26
+ withinDays: 30,
27
+ },
28
+ ],
29
+ });
@@ -0,0 +1,31 @@
1
+ import { defineSegment } from "@cowliss/cli/segments";
2
+
3
+ /**
4
+ * Played in the last 7 days and has not finished the tutorial. The window
5
+ * is what makes this an audience rather than a graveyard: someone who
6
+ * installed the game in March and never opened it again is not a new
7
+ * player, and mailing them a tutorial nudge is how a game teaches people
8
+ * to ignore it.
9
+ *
10
+ * 7 days is the number to argue with. Set it to the gap after which you
11
+ * would rather leave someone alone than nudge them.
12
+ *
13
+ * `session.started` is the name most game analytics already send. If yours
14
+ * sends `game_open` or `Session Start`, change the name here and in the
15
+ * other segment files rather than renaming events in your own game.
16
+ */
17
+ export default defineSegment({
18
+ description: "Played in the last week and has not finished the tutorial.",
19
+ tags: ["game", "onboarding"],
20
+ match: "all",
21
+ predicates: [
22
+ {
23
+ kind: "event",
24
+ name: "session.started",
25
+ op: "performed",
26
+ atLeast: 1,
27
+ withinDays: 7,
28
+ },
29
+ { kind: "event", name: "tutorial.completed", op: "notPerformed" },
30
+ ],
31
+ });
@@ -0,0 +1,21 @@
1
+ import { defineSegment } from "@cowliss/cli/segments";
2
+
3
+ /**
4
+ * Has paid at all: one `purchase.completed`, ever. In a game that is a
5
+ * line crossed once, so entering this audience is the same moment as the
6
+ * first purchase — which is why the journey that enrolls from it
7
+ * (`journeys/first-purchase.ts`) runs once per player without counting
8
+ * anything itself.
9
+ *
10
+ * Deliberately not windowed, unlike the other three. Somebody who paid you
11
+ * two years ago is still a payer; whether they still play is what
12
+ * `active-players` and `lapsed-players` answer.
13
+ */
14
+ export default defineSegment({
15
+ description: "Has made at least one in-app purchase.",
16
+ tags: ["game", "revenue"],
17
+ match: "all",
18
+ predicates: [
19
+ { kind: "event", name: "purchase.completed", op: "performed", atLeast: 1 },
20
+ ],
21
+ });
@@ -0,0 +1,151 @@
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 nine mails in this blueprint share: one look for the
16
+ * programme rather than nine 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. The postal
25
+ * address is not decoration: a marketing mail is required to carry one in
26
+ * most of the world, and Cowliss does not know yours. The unsubscribe link
27
+ * is the one thing you do not add here — the send path appends it to every
28
+ * marketing mail, and one that is already in the body is left alone.
29
+ */
30
+
31
+ export const PRODUCT_NAME = "Acme";
32
+ export const APP_URL = "https://app.example.com";
33
+ export const DOCS_URL = "https://docs.example.com";
34
+ export const COMPANY_ADDRESS = "Acme Inc, 1 Example Street, Springfield";
35
+
36
+ export const bodyStyle: CSSProperties = {
37
+ backgroundColor: "#f4f4f5",
38
+ fontFamily:
39
+ "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
40
+ margin: "0",
41
+ padding: "32px 0",
42
+ };
43
+
44
+ export const containerStyle: CSSProperties = {
45
+ margin: "0 auto",
46
+ padding: "40px 36px",
47
+ maxWidth: "560px",
48
+ backgroundColor: "#ffffff",
49
+ borderRadius: "14px",
50
+ border: "1px solid #e4e4e7",
51
+ };
52
+
53
+ export const headingStyle: CSSProperties = {
54
+ fontSize: "24px",
55
+ lineHeight: "1.25",
56
+ fontWeight: 700,
57
+ letterSpacing: "-0.02em",
58
+ color: "#09090b",
59
+ margin: "0 0 18px",
60
+ };
61
+
62
+ export const textStyle: CSSProperties = {
63
+ fontSize: "16px",
64
+ lineHeight: "1.7",
65
+ color: "#27272a",
66
+ margin: "0 0 16px",
67
+ };
68
+
69
+ export const stepStyle: CSSProperties = {
70
+ ...textStyle,
71
+ margin: "0 0 10px",
72
+ paddingLeft: "14px",
73
+ borderLeft: "2px solid #e4e4e7",
74
+ };
75
+
76
+ export const buttonStyle: CSSProperties = {
77
+ display: "inline-block",
78
+ marginTop: "8px",
79
+ marginBottom: "16px",
80
+ padding: "14px 26px",
81
+ borderRadius: "10px",
82
+ backgroundColor: "#09090b",
83
+ color: "#ffffff",
84
+ fontSize: "15px",
85
+ fontWeight: 600,
86
+ lineHeight: 1,
87
+ textDecoration: "none",
88
+ };
89
+
90
+ const hrStyle: CSSProperties = {
91
+ borderColor: "#e4e4e7",
92
+ borderStyle: "solid",
93
+ borderWidth: "1px 0 0 0",
94
+ margin: "24px 0",
95
+ };
96
+
97
+ const footerStyle: CSSProperties = {
98
+ fontSize: "13px",
99
+ lineHeight: "1.6",
100
+ color: "#52525b",
101
+ margin: "0 0 10px",
102
+ };
103
+
104
+ /**
105
+ * The reply line is from `apps/platform-workspace`, and it is the reason
106
+ * these mails ask for a reply instead of linking a contact form: a trial
107
+ * is the week a developer decides whether anybody is home.
108
+ */
109
+ export function ProductEmail(props: {
110
+ preview: string;
111
+ heading: string;
112
+ children: ReactNode;
113
+ }): ReactElement {
114
+ return (
115
+ <Html dir="ltr" lang="en">
116
+ <Head>
117
+ <meta content="text/html; charset=UTF-8" httpEquiv="Content-Type" />
118
+ <meta name="x-apple-disable-message-reformatting" />
119
+ <meta name="color-scheme" content="light" />
120
+ <meta name="supported-color-schemes" content="light" />
121
+ </Head>
122
+ <Preview>{props.preview}</Preview>
123
+ <Body style={bodyStyle}>
124
+ <Container style={containerStyle}>
125
+ <Heading style={headingStyle}>{props.heading}</Heading>
126
+ {props.children}
127
+ <Hr style={hrStyle} />
128
+ <Text style={footerStyle}>
129
+ Questions? Reply to this email and a person reads it.
130
+ </Text>
131
+ <Text style={{ ...footerStyle, marginBottom: 0 }}>
132
+ {COMPANY_ADDRESS}
133
+ </Text>
134
+ </Container>
135
+ </Body>
136
+ </Html>
137
+ );
138
+ }
139
+
140
+ /** The one action a mail asks for. Every mail here has exactly one. */
141
+ export function Cta(props: { href: string; children: string }): ReactElement {
142
+ return (
143
+ <Button style={buttonStyle} href={props.href}>
144
+ {props.children}
145
+ </Button>
146
+ );
147
+ }
148
+
149
+ /** "Hi Ada," when the journey passed a first name, "Hi," when it did not. */
150
+ export const greeting = (firstName?: string): string =>
151
+ firstName ? `Hi ${firstName},` : "Hi,";
@@ -0,0 +1,61 @@
1
+ import type { SendClass } from "@cowliss/cli/emails";
2
+ import { Text } from "@react-email/components";
3
+ import type { ReactElement } from "react";
4
+ import { z } from "zod";
5
+ import {
6
+ APP_URL,
7
+ Cta,
8
+ greeting,
9
+ ProductEmail,
10
+ stepStyle,
11
+ textStyle,
12
+ } from "./_shell";
13
+
14
+ /**
15
+ * Their first project exists. This is the one mail in the trial that can
16
+ * assume the product worked, so it spends that on the second thing rather
17
+ * than congratulating them at length.
18
+ *
19
+ * Transactional, and it has no offer in it. A trial pitch here would land
20
+ * five minutes after somebody did what you asked, which is the worst
21
+ * possible moment to ask for a card.
22
+ */
23
+
24
+ export const props = z.object({
25
+ firstName: z.string().optional(),
26
+ });
27
+
28
+ export const subject = "Your first project is live";
29
+
30
+ export const sendClass: SendClass = "transactional";
31
+
32
+ export const tags = ["saas", "activation"];
33
+
34
+ export default function Activated(p: z.infer<typeof props>): ReactElement {
35
+ return (
36
+ <ProductEmail
37
+ preview="Three things worth doing next."
38
+ heading={greeting(p.firstName)}
39
+ >
40
+ <Text style={textStyle}>
41
+ Your first project is set up, which is the part most people never get
42
+ to. Everything after this is smaller.
43
+ </Text>
44
+ <Text style={textStyle}>
45
+ Three things worth doing while you have it open:
46
+ </Text>
47
+ <Text style={stepStyle}>
48
+ 1. Invite whoever else touches this work, so you are not the only one
49
+ who can see it.
50
+ </Text>
51
+ <Text style={stepStyle}>
52
+ 2. Connect the tool it has to talk to, because that is where the time
53
+ actually goes.
54
+ </Text>
55
+ <Text style={stepStyle}>
56
+ 3. Run it on something real, rather than on the sample.
57
+ </Text>
58
+ <Cta href={APP_URL}>Open your project</Cta>
59
+ </ProductEmail>
60
+ );
61
+ }
@@ -0,0 +1,49 @@
1
+ import type { SendClass } from "@cowliss/cli/emails";
2
+ import { Text } from "@react-email/components";
3
+ import type { ReactElement } from "react";
4
+ import { z } from "zod";
5
+ import { APP_URL, Cta, greeting, ProductEmail, textStyle } from "./_shell";
6
+
7
+ /**
8
+ * Day 5, and the sample project did not help either. This one stops
9
+ * instructing and asks a question, because at this point the useful
10
+ * information is going the other way: whatever stopped them is something
11
+ * you want to know before the trial runs out.
12
+ *
13
+ * It is the last activation mail. A third would be nagging, and the
14
+ * `trial-ending` journey still has one more thing to say.
15
+ */
16
+
17
+ export const props = z.object({
18
+ firstName: z.string().optional(),
19
+ });
20
+
21
+ export const subject = "What got in the way?";
22
+
23
+ export const sendClass: SendClass = "marketing";
24
+
25
+ export const tags = ["saas", "activation"];
26
+
27
+ export default function ActivationHelp(p: z.infer<typeof props>): ReactElement {
28
+ return (
29
+ <ProductEmail
30
+ preview="Tell us what stopped you and we will sort it out."
31
+ heading={greeting(p.firstName)}
32
+ >
33
+ <Text style={textStyle}>
34
+ You started a trial and have not set anything up, which usually means
35
+ one of three things: it needs data you have to get from someone else, it
36
+ does not do the thing you hoped, or the week got away from you.
37
+ </Text>
38
+ <Text style={textStyle}>
39
+ Whichever it is, reply and say so. If it is the first, we will do the
40
+ import for you. If it is the second, we would rather hear it than keep
41
+ mailing you.
42
+ </Text>
43
+ <Text style={textStyle}>
44
+ This is the last of these. Your trial stays open either way.
45
+ </Text>
46
+ <Cta href={APP_URL}>Open your workspace</Cta>
47
+ </ProductEmail>
48
+ );
49
+ }
@@ -0,0 +1,48 @@
1
+ import type { SendClass } from "@cowliss/cli/emails";
2
+ import { Text } from "@react-email/components";
3
+ import type { ReactElement } from "react";
4
+ import { z } from "zod";
5
+ import { APP_URL, Cta, greeting, ProductEmail, textStyle } from "./_shell";
6
+
7
+ /**
8
+ * Day 2 of a trial nobody has started. It repeats the one thing to do and
9
+ * removes the usual reason it has not been done: the blank page. No
10
+ * feature list and no case study, because neither answers "I have not got
11
+ * round to it".
12
+ */
13
+
14
+ export const props = z.object({
15
+ firstName: z.string().optional(),
16
+ });
17
+
18
+ export const subject = "A project you can start from";
19
+
20
+ export const sendClass: SendClass = "marketing";
21
+
22
+ export const tags = ["saas", "activation"];
23
+
24
+ export default function ActivationNudge(
25
+ p: z.infer<typeof props>,
26
+ ): ReactElement {
27
+ return (
28
+ <ProductEmail
29
+ preview="Start from a sample project instead of a blank one."
30
+ heading={greeting(p.firstName)}
31
+ >
32
+ <Text style={textStyle}>
33
+ Your trial has a few days on it and nothing in it yet, which is usually
34
+ the blank page rather than the product.
35
+ </Text>
36
+ <Text style={textStyle}>
37
+ So start from something. The sample project opens with real data in it:
38
+ rename it, empty it, or throw it away once you have seen how the pieces
39
+ fit together.
40
+ </Text>
41
+ <Cta href={`${APP_URL}/projects/new?sample=1`}>Open a sample project</Cta>
42
+ <Text style={textStyle}>
43
+ If you would rather bring your own work in, reply to this mail with the
44
+ format it is in and we will tell you whether we read it.
45
+ </Text>
46
+ </ProductEmail>
47
+ );
48
+ }
@@ -0,0 +1,58 @@
1
+ import type { SendClass } from "@cowliss/cli/emails";
2
+ import { Text } from "@react-email/components";
3
+ import type { ReactElement } from "react";
4
+ import { z } from "zod";
5
+ import {
6
+ APP_URL,
7
+ Cta,
8
+ greeting,
9
+ PRODUCT_NAME,
10
+ ProductEmail,
11
+ textStyle,
12
+ } from "./_shell";
13
+
14
+ /**
15
+ * Sent to somebody using the product alone, a few days after they got it
16
+ * working. It asks for one invite rather than a team, because one is a
17
+ * decision they can make without asking anyone.
18
+ *
19
+ * Marketing, and it reads like it: this is a favour, and it says why it is
20
+ * worth doing for them rather than for you. Seats are not mentioned. A
21
+ * trial that turns into a seat count conversation before anyone has seen
22
+ * the product together is a trial that ends there.
23
+ */
24
+
25
+ export const props = z.object({
26
+ firstName: z.string().optional(),
27
+ });
28
+
29
+ export const subject = "Who else needs to see this?";
30
+
31
+ export const sendClass: SendClass = "marketing";
32
+
33
+ export const tags = ["saas", "expansion"];
34
+
35
+ export default function InviteYourTeam(p: z.infer<typeof props>): ReactElement {
36
+ return (
37
+ <ProductEmail
38
+ preview="One invite is enough to tell."
39
+ heading={greeting(p.firstName)}
40
+ >
41
+ <Text style={textStyle}>
42
+ You have {PRODUCT_NAME} set up and you are the only one in it. That is
43
+ fine for deciding whether it works, and it is the wrong shape for
44
+ deciding whether your team will use it.
45
+ </Text>
46
+ <Text style={textStyle}>
47
+ Invite one person. The one who will be most annoyed if this becomes
48
+ another thing they have to check, ideally. Their opinion in week one is
49
+ worth more than yours in week three.
50
+ </Text>
51
+ <Text style={textStyle}>
52
+ Invites are free during the trial, and you can remove anyone later
53
+ without losing their work.
54
+ </Text>
55
+ <Cta href={`${APP_URL}/settings/members`}>Invite someone</Cta>
56
+ </ProductEmail>
57
+ );
58
+ }