@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,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,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,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
|
+
}
|