@cowliss/cli 0.9.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 +1191 -398
- 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,57 @@
|
|
|
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
|
+
* They subscribed. Sent the moment it happens rather than when the trial
|
|
16
|
+
* would have ended.
|
|
17
|
+
*
|
|
18
|
+
* It is not a receipt: your billing provider already sent one with the
|
|
19
|
+
* numbers on it. This is the note that says the trial is over and nothing
|
|
20
|
+
* is about to switch off, which is the thing somebody who just paid
|
|
21
|
+
* actually wants confirmed.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export const props = z.object({
|
|
25
|
+
firstName: z.string().optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const subject = `You are in`;
|
|
29
|
+
|
|
30
|
+
export const sendClass: SendClass = "transactional";
|
|
31
|
+
|
|
32
|
+
export const tags = ["saas", "revenue"];
|
|
33
|
+
|
|
34
|
+
export default function TrialConverted(p: z.infer<typeof props>): ReactElement {
|
|
35
|
+
return (
|
|
36
|
+
<ProductEmail
|
|
37
|
+
preview="Nothing switches off now."
|
|
38
|
+
heading={greeting(p.firstName)}
|
|
39
|
+
>
|
|
40
|
+
<Text style={textStyle}>
|
|
41
|
+
Your subscription is active, so the trial countdown is gone and nothing
|
|
42
|
+
switches off. Everything you built during the trial carries over as it
|
|
43
|
+
is.
|
|
44
|
+
</Text>
|
|
45
|
+
<Text style={textStyle}>
|
|
46
|
+
Your receipt and invoices come from our billing provider in a separate
|
|
47
|
+
mail. Seats, plan, and cancellation all live on the billing page, and
|
|
48
|
+
you can change any of them yourself.
|
|
49
|
+
</Text>
|
|
50
|
+
<Text style={textStyle}>
|
|
51
|
+
Thanks for picking {PRODUCT_NAME}. If something is not working the way
|
|
52
|
+
you expected, reply to this message.
|
|
53
|
+
</Text>
|
|
54
|
+
<Cta href={APP_URL}>Open your workspace</Cta>
|
|
55
|
+
</ProductEmail>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
* The trial ran out and nobody subscribed. One mail, no chasing.
|
|
9
|
+
*
|
|
10
|
+
* Its whole job is to remove the worry that the work is gone, because the
|
|
11
|
+
* worry is what makes people avoid the mail rather than read it. Saying
|
|
12
|
+
* how long the data is kept, in plain days, is the useful part.
|
|
13
|
+
*
|
|
14
|
+
* Transactional, because losing access to your own work is a fact you are
|
|
15
|
+
* owed regardless of what you ticked. It stays transactional only as long
|
|
16
|
+
* as it carries no offer: a discount here makes it a win-back campaign,
|
|
17
|
+
* and a win-back campaign belongs in a journey with a marketing purpose.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const props = z.object({
|
|
21
|
+
firstName: z.string().optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const subject = "Your trial has ended";
|
|
25
|
+
|
|
26
|
+
export const sendClass: SendClass = "transactional";
|
|
27
|
+
|
|
28
|
+
export const tags = ["saas", "trial"];
|
|
29
|
+
|
|
30
|
+
export default function TrialEnded(p: z.infer<typeof props>): ReactElement {
|
|
31
|
+
return (
|
|
32
|
+
<ProductEmail
|
|
33
|
+
preview="Your projects are kept for 60 days."
|
|
34
|
+
heading={greeting(p.firstName)}
|
|
35
|
+
>
|
|
36
|
+
<Text style={textStyle}>
|
|
37
|
+
Your trial ended today. Nothing has been deleted: your account still
|
|
38
|
+
opens, your projects are still there to read, and you can export them
|
|
39
|
+
whenever you like.
|
|
40
|
+
</Text>
|
|
41
|
+
<Text style={textStyle}>
|
|
42
|
+
We keep everything for 60 days. Subscribe inside that window and you
|
|
43
|
+
pick up exactly where you left off, with nothing to set up again.
|
|
44
|
+
</Text>
|
|
45
|
+
<Text style={textStyle}>
|
|
46
|
+
This is the last mail about the trial. If it was not the right fit, we
|
|
47
|
+
would still like to know why — reply and tell us.
|
|
48
|
+
</Text>
|
|
49
|
+
<Cta href={`${APP_URL}/billing`}>Choose a plan</Cta>
|
|
50
|
+
</ProductEmail>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
* Three days left, to somebody who has work in the product. They are
|
|
9
|
+
* deciding what happens to it, so the mail answers that and nothing else:
|
|
10
|
+
* the date, what stays, what stops.
|
|
11
|
+
*
|
|
12
|
+
* Transactional, and the one mail here that most deserves to be. Their
|
|
13
|
+
* access is about to change and the work is theirs; that is not something
|
|
14
|
+
* an unsubscribe should be able to stop. It keeps that standing by
|
|
15
|
+
* carrying no discount and no pitch — see the journey's comment for why
|
|
16
|
+
* adding one would be a bigger change than it looks.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const props = z.object({
|
|
20
|
+
firstName: z.string().optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const subject = "Three days left on your trial";
|
|
24
|
+
|
|
25
|
+
export const sendClass: SendClass = "transactional";
|
|
26
|
+
|
|
27
|
+
export const tags = ["saas", "trial"];
|
|
28
|
+
|
|
29
|
+
export default function TrialEndingActivated(
|
|
30
|
+
p: z.infer<typeof props>,
|
|
31
|
+
): ReactElement {
|
|
32
|
+
return (
|
|
33
|
+
<ProductEmail
|
|
34
|
+
preview="What happens to your projects on Friday."
|
|
35
|
+
heading={greeting(p.firstName)}
|
|
36
|
+
>
|
|
37
|
+
<Text style={textStyle}>
|
|
38
|
+
Your trial ends in three days. Here is what that means for the work you
|
|
39
|
+
have in it.
|
|
40
|
+
</Text>
|
|
41
|
+
<Text style={textStyle}>
|
|
42
|
+
Your projects stay exactly as they are. You keep read access to all of
|
|
43
|
+
them, and you can export everything at any time. What stops is editing,
|
|
44
|
+
sharing, and anything running on a schedule.
|
|
45
|
+
</Text>
|
|
46
|
+
<Text style={textStyle}>
|
|
47
|
+
Subscribing before Friday means nothing changes at all, and no gap to
|
|
48
|
+
explain to whoever you brought in.
|
|
49
|
+
</Text>
|
|
50
|
+
<Cta href={`${APP_URL}/billing`}>Choose a plan</Cta>
|
|
51
|
+
</ProductEmail>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
* Three days left, to somebody who never set anything up. There is no work
|
|
9
|
+
* to protect, so there is nothing to warn them about: this is an ask, and
|
|
10
|
+
* an ask is marketing.
|
|
11
|
+
*
|
|
12
|
+
* It is the pair to `trial-ending-activated.tsx`, which is transactional.
|
|
13
|
+
* Same journey, same day, different `sendClass`, because the difference is
|
|
14
|
+
* not the timing — it is that one mail is owed and the other is wanted.
|
|
15
|
+
* The journey's comment has the whole rule.
|
|
16
|
+
*
|
|
17
|
+
* What it offers is time, not a discount. Somebody who never opened the
|
|
18
|
+
* product has not priced it yet.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const props = z.object({
|
|
22
|
+
firstName: z.string().optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const subject = "Want another two weeks?";
|
|
26
|
+
|
|
27
|
+
export const sendClass: SendClass = "marketing";
|
|
28
|
+
|
|
29
|
+
export const tags = ["saas", "trial"];
|
|
30
|
+
|
|
31
|
+
export default function TrialEndingUnactivated(
|
|
32
|
+
p: z.infer<typeof props>,
|
|
33
|
+
): ReactElement {
|
|
34
|
+
return (
|
|
35
|
+
<ProductEmail
|
|
36
|
+
preview="Your trial ends Friday. We can move it."
|
|
37
|
+
heading={greeting(p.firstName)}
|
|
38
|
+
>
|
|
39
|
+
<Text style={textStyle}>
|
|
40
|
+
Your trial ends in three days and you have not had a chance to use it,
|
|
41
|
+
so ending it would not tell either of us much.
|
|
42
|
+
</Text>
|
|
43
|
+
<Text style={textStyle}>
|
|
44
|
+
Reply with the word "extend" and we will give you another two weeks,
|
|
45
|
+
starting whenever you are ready rather than today. No card, no call.
|
|
46
|
+
</Text>
|
|
47
|
+
<Text style={textStyle}>
|
|
48
|
+
Or open it now: the sample project takes about ten minutes and is enough
|
|
49
|
+
to tell whether this is worth your time.
|
|
50
|
+
</Text>
|
|
51
|
+
<Cta href={`${APP_URL}/projects/new?sample=1`}>Open a sample project</Cta>
|
|
52
|
+
</ProductEmail>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
stepStyle,
|
|
12
|
+
textStyle,
|
|
13
|
+
} from "./_shell";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The mail that opens the trial. It names one thing to do, in three steps
|
|
17
|
+
* somebody can finish before their coffee goes cold, and links nothing
|
|
18
|
+
* else. A tour of every feature on day one is a tour nobody takes.
|
|
19
|
+
*
|
|
20
|
+
* The subject is a plain string rather than a function, because it does
|
|
21
|
+
* not depend on the props. `apps/platform-workspace` writes its constant
|
|
22
|
+
* subjects this way and it is one less indirection to read past.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export const props = z.object({
|
|
26
|
+
firstName: z.string().optional(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const subject = `Your ${PRODUCT_NAME} trial starts now`;
|
|
30
|
+
|
|
31
|
+
export const sendClass: SendClass = "transactional";
|
|
32
|
+
|
|
33
|
+
export const tags = ["saas", "trial"];
|
|
34
|
+
|
|
35
|
+
export default function TrialWelcome(p: z.infer<typeof props>): ReactElement {
|
|
36
|
+
return (
|
|
37
|
+
<ProductEmail
|
|
38
|
+
preview="Your first project is about ten minutes away."
|
|
39
|
+
heading={greeting(p.firstName)}
|
|
40
|
+
>
|
|
41
|
+
<Text style={textStyle}>
|
|
42
|
+
Your 14-day trial of {PRODUCT_NAME} is open. Everything is unlocked,
|
|
43
|
+
there is no card on file, and nothing switches off without warning.
|
|
44
|
+
</Text>
|
|
45
|
+
<Text style={textStyle}>
|
|
46
|
+
The fastest way to see whether this is for you is to create one project
|
|
47
|
+
with real work in it. That takes about ten minutes:
|
|
48
|
+
</Text>
|
|
49
|
+
<Text style={stepStyle}>1. Create a project and give it a name.</Text>
|
|
50
|
+
<Text style={stepStyle}>
|
|
51
|
+
2. Import or paste in something you already have.
|
|
52
|
+
</Text>
|
|
53
|
+
<Text style={stepStyle}>3. Open it and change one thing.</Text>
|
|
54
|
+
<Cta href={`${APP_URL}/projects/new`}>Create your first project</Cta>
|
|
55
|
+
</ProductEmail>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one thing all six journeys share. An `_`-prefixed file under
|
|
3
|
+
* `journeys/` is a module, not a journey: `cow build` skips it in
|
|
4
|
+
* discovery and bundles it into whichever journey imports it.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately nothing else lives here, not even the from-address.
|
|
7
|
+
* Addresses, waits and trial lengths stay written out in the journey that
|
|
8
|
+
* uses them, so moving the activation nudge from day 2 to day 4 is one
|
|
9
|
+
* edit in `activation-nudge.ts` and not a hunt through a shared config.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** A trait, which is `unknown` on the way out of a profile, as a string. */
|
|
13
|
+
export const str = (value: unknown): string | undefined =>
|
|
14
|
+
typeof value === "string" ? value : undefined;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineJourney } from "@cowliss/cli/journeys";
|
|
2
|
+
import { str } from "./_helpers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The activation mail: their first project exists, so this is the moment
|
|
6
|
+
* they have seen the product work and the moment to say what comes next.
|
|
7
|
+
*
|
|
8
|
+
* `activated` is one trait doing two jobs. It guards this journey, which
|
|
9
|
+
* triggers on every `project.created` and must only mail the first one;
|
|
10
|
+
* and it is how `trial-ending.ts` knows which of its two mails to send
|
|
11
|
+
* three days before the trial runs out. Delete this journey and that split
|
|
12
|
+
* quietly collapses into its unactivated branch, so change the two files
|
|
13
|
+
* together.
|
|
14
|
+
*
|
|
15
|
+
* Setting it after the send rather than before is deliberate: a send that
|
|
16
|
+
* fails leaves the trait unset, and the next project mails them instead of
|
|
17
|
+
* skipping them forever.
|
|
18
|
+
*
|
|
19
|
+
* `transactional`: this is about something they did in their own account,
|
|
20
|
+
* five minutes ago, and it carries no offer. `apps/platform-workspace`
|
|
21
|
+
* sends its own activation mail under the same purpose for the same
|
|
22
|
+
* reason.
|
|
23
|
+
*/
|
|
24
|
+
export default defineJourney({
|
|
25
|
+
trigger: { event: "project.created" },
|
|
26
|
+
purpose: "transactional",
|
|
27
|
+
from: "Acme <hello@example.com>",
|
|
28
|
+
description:
|
|
29
|
+
"Congratulates someone on their first project and says what is next.",
|
|
30
|
+
tags: ["saas", "activation"],
|
|
31
|
+
run: async (_event, api) => {
|
|
32
|
+
const me = await api.profile.get();
|
|
33
|
+
if (me.traits.activated === true) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
await api.send.email({
|
|
37
|
+
template: "activated",
|
|
38
|
+
props: { firstName: str(me.traits.firstName) },
|
|
39
|
+
});
|
|
40
|
+
await api.traits.set("activated", true);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { defineJourney } from "@cowliss/cli/journeys";
|
|
2
|
+
import { str } from "./_helpers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Two touches for a trial that has not started, stopping the moment it
|
|
6
|
+
* does:
|
|
7
|
+
*
|
|
8
|
+
* day 2 activation-nudge the ten-minute version of the first project
|
|
9
|
+
* day 5 activation-help no instructions, just "what got in the way?"
|
|
10
|
+
*
|
|
11
|
+
* Two literals to argue with: leave the first at a couple of days if
|
|
12
|
+
* people evaluate your product in one sitting, and stretch both if they
|
|
13
|
+
* need a colleague's approval before they can try anything.
|
|
14
|
+
*
|
|
15
|
+
* It enrolls from `segments/unactivated.ts`, so the milestone is defined
|
|
16
|
+
* in one file and this journey only decides what to do about it. Each wait
|
|
17
|
+
* is on `project.created`, and activating at any point ends the sequence:
|
|
18
|
+
* somebody who creates their first project on day 4 gets the walkthrough
|
|
19
|
+
* and not the "what got in the way" mail.
|
|
20
|
+
*
|
|
21
|
+
* `marketing`: a nudge is a mail nobody asked for, so it is gated on
|
|
22
|
+
* consent and counts against the frequency cap. No cooldown, which means
|
|
23
|
+
* once ever — and nobody re-enters `unactivated` anyway, because the
|
|
24
|
+
* milestone cannot be un-reached.
|
|
25
|
+
*/
|
|
26
|
+
export default defineJourney({
|
|
27
|
+
trigger: { segment: "unactivated" },
|
|
28
|
+
purpose: "marketing",
|
|
29
|
+
from: "Acme <hello@example.com>",
|
|
30
|
+
description: "Helps a trial that has not created its first project yet.",
|
|
31
|
+
tags: ["saas", "activation"],
|
|
32
|
+
run: async (_event, api) => {
|
|
33
|
+
const me = await api.profile.get();
|
|
34
|
+
const props = { firstName: str(me.traits.firstName) };
|
|
35
|
+
if (await api.waitForEvent("project.created", { timeout: "2d" })) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
await api.send.email({ template: "activation-nudge", props });
|
|
39
|
+
if (await api.waitForEvent("project.created", { timeout: "3d" })) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
await api.send.email({ template: "activation-help", props });
|
|
43
|
+
},
|
|
44
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineJourney } from "@cowliss/cli/journeys";
|
|
2
|
+
import { str } from "./_helpers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One person is evaluating a product their team will use. Asking them to
|
|
6
|
+
* bring a colleague in is the highest-value mail in this blueprint, and
|
|
7
|
+
* the easiest one to send at the wrong moment.
|
|
8
|
+
*
|
|
9
|
+
* So it enrolls from `segments/activated-users.ts` rather than from the
|
|
10
|
+
* trial: there is no point asking someone to show their team a workspace
|
|
11
|
+
* that is still empty. Then it waits three days on `invite.sent` and says
|
|
12
|
+
* nothing at all if one arrives, because somebody who has already invited
|
|
13
|
+
* their team does not need to be asked to.
|
|
14
|
+
*
|
|
15
|
+
* The three days are the literal to argue with. Shorten it if your product
|
|
16
|
+
* is single-sitting; lengthen it if setting up a shared workspace is a
|
|
17
|
+
* week's work.
|
|
18
|
+
*
|
|
19
|
+
* `marketing` with no cooldown, so one person is asked once, ever. A
|
|
20
|
+
* cooldown is the knob for asking again, and it would do nothing here:
|
|
21
|
+
* activating is permanent, so nobody re-enters the audience to be asked a
|
|
22
|
+
* second time. Widen the audience to one people leave and return to (add
|
|
23
|
+
* a `notPerformed invite.sent withinDays` predicate, say) and the cooldown
|
|
24
|
+
* becomes the thing standing between a year and a fortnight.
|
|
25
|
+
*/
|
|
26
|
+
export default defineJourney({
|
|
27
|
+
trigger: { segment: "activated-users" },
|
|
28
|
+
purpose: "marketing",
|
|
29
|
+
from: "Acme <hello@example.com>",
|
|
30
|
+
description: "Asks someone working alone to bring their team in.",
|
|
31
|
+
tags: ["saas", "expansion"],
|
|
32
|
+
run: async (_event, api) => {
|
|
33
|
+
if (await api.waitForEvent("invite.sent", { timeout: "3d" })) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const me = await api.profile.get();
|
|
37
|
+
await api.send.email({
|
|
38
|
+
template: "invite-your-team",
|
|
39
|
+
props: { firstName: str(me.traits.firstName) },
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineJourney } from "@cowliss/cli/journeys";
|
|
2
|
+
import { str } from "./_helpers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How a trial finishes, both ways. It starts with the trial and waits the
|
|
6
|
+
* whole 14 days for `subscription.created`:
|
|
7
|
+
*
|
|
8
|
+
* it arrives trial-converted sent the day they subscribe, not day 14
|
|
9
|
+
* it does not trial-ended sent when the trial runs out
|
|
10
|
+
*
|
|
11
|
+
* The converted mail going out early is the point. Subscribing ends the
|
|
12
|
+
* trial, so that is when there is something to say, and waiting until
|
|
13
|
+
* Friday to acknowledge a payment made on Tuesday reads as a program that
|
|
14
|
+
* is not paying attention.
|
|
15
|
+
*
|
|
16
|
+
* `transactional`, both endings: whether someone still has an account on
|
|
17
|
+
* Monday is a fact about their access, not a campaign, and neither mail
|
|
18
|
+
* carries an offer. That is the line. Put a "come back for 30% off" in
|
|
19
|
+
* `trial-ended.tsx` and it has become marketing — move it to a journey
|
|
20
|
+
* with a marketing purpose rather than changing its `sendClass` here,
|
|
21
|
+
* because the purpose is what the consent gate reads.
|
|
22
|
+
*/
|
|
23
|
+
export default defineJourney({
|
|
24
|
+
trigger: { event: "trial.started" },
|
|
25
|
+
purpose: "transactional",
|
|
26
|
+
from: "Acme <hello@example.com>",
|
|
27
|
+
description:
|
|
28
|
+
"Closes a trial, either with a welcome or with what happens now.",
|
|
29
|
+
tags: ["saas", "trial"],
|
|
30
|
+
run: async (_event, api) => {
|
|
31
|
+
const me = await api.profile.get();
|
|
32
|
+
const props = { firstName: str(me.traits.firstName) };
|
|
33
|
+
if (await api.waitForEvent("subscription.created", { timeout: "14d" })) {
|
|
34
|
+
await api.send.email({ template: "trial-converted", props });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
await api.send.email({ template: "trial-ended", props });
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { defineJourney } from "@cowliss/cli/journeys";
|
|
2
|
+
import { str } from "./_helpers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Three days before the end of a 14-day trial, split on whether they ever
|
|
6
|
+
* got the product working. The two literals are the 11 days below and the
|
|
7
|
+
* 14 in `trial-ended.ts`: they are one trial length between them, so if
|
|
8
|
+
* yours is 30 days, make them 27 and 30.
|
|
9
|
+
*
|
|
10
|
+
* The wait does two jobs. It is the eleven days, and it is the exit: a
|
|
11
|
+
* `subscription.created` inside the window means they already bought, and
|
|
12
|
+
* `trial-ended.ts` welcomes them. Waiting rather than sleeping is what
|
|
13
|
+
* stops this journey mailing a paying customer about an expiry.
|
|
14
|
+
*
|
|
15
|
+
* The profile is read AFTER the wait, not before. `activated.ts` sets the
|
|
16
|
+
* trait the moment their first project lands, which can be day 9, and a
|
|
17
|
+
* read at enrollment would have missed it. (`apps/platform-workspace`
|
|
18
|
+
* reads first in `platform-dormant.ts`, for the opposite reason: the
|
|
19
|
+
* event it waits on can no longer fire for the profiles it wants to skip.)
|
|
20
|
+
*
|
|
21
|
+
* This is the journey worth reading twice, because it sends one mail of
|
|
22
|
+
* each class:
|
|
23
|
+
*
|
|
24
|
+
* activated trial-ending-activated transactional
|
|
25
|
+
* not yet trial-ending-unactivated marketing
|
|
26
|
+
*
|
|
27
|
+
* A template's `sendClass` decides which gates run for THAT mail: a
|
|
28
|
+
* transactional one skips consent and the frequency cap, a marketing one
|
|
29
|
+
* does not. A journey's `purpose` is the consent category the marketing
|
|
30
|
+
* gate then asks about. So the purpose here is `marketing` — the stricter
|
|
31
|
+
* of the two — and the transactional mail is unaffected by it. Declaring
|
|
32
|
+
* `transactional` instead would let the marketing mail through the
|
|
33
|
+
* marketing gate untested, which is the one mistake this file exists to
|
|
34
|
+
* prevent.
|
|
35
|
+
*
|
|
36
|
+
* Somebody whose trial is ending and who never got started is the reason
|
|
37
|
+
* for the split: telling them their trial expires on Friday is a fact they
|
|
38
|
+
* are owed, and asking them to try again is an ask they can decline.
|
|
39
|
+
*/
|
|
40
|
+
export default defineJourney({
|
|
41
|
+
trigger: { segment: "trialing" },
|
|
42
|
+
purpose: "marketing",
|
|
43
|
+
from: "Acme <hello@example.com>",
|
|
44
|
+
description: "Gives three days' notice before a trial ends.",
|
|
45
|
+
tags: ["saas", "trial"],
|
|
46
|
+
run: async (_event, api) => {
|
|
47
|
+
if (await api.waitForEvent("subscription.created", { timeout: "11d" })) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const me = await api.profile.get();
|
|
51
|
+
const props = { firstName: str(me.traits.firstName) };
|
|
52
|
+
if (me.traits.activated === true) {
|
|
53
|
+
await api.send.email({ template: "trial-ending-activated", props });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
await api.send.email({ template: "trial-ending-unactivated", props });
|
|
57
|
+
},
|
|
58
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineJourney } from "@cowliss/cli/journeys";
|
|
2
|
+
import { str } from "./_helpers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The first mail of the trial, sent on `trial.started`. One job: get them
|
|
6
|
+
* to the thing the product is for, which here is their first project.
|
|
7
|
+
*
|
|
8
|
+
* Four journeys start within a second of each other at the top of a trial
|
|
9
|
+
* — this one, `activation-nudge`, `trial-ending` and `trial-ended` — and
|
|
10
|
+
* this is the only one that sends anything today. The other three are
|
|
11
|
+
* waiting to see what happens.
|
|
12
|
+
*
|
|
13
|
+
* `transactional`: somebody asked for a trial and this is the mail that
|
|
14
|
+
* opens it, the same way a receipt follows an order. It is not gated on
|
|
15
|
+
* marketing consent and takes no cooldown, because a second trial is a
|
|
16
|
+
* second trial.
|
|
17
|
+
*
|
|
18
|
+
* That is also the line to watch. Put a case study or a pricing pitch in
|
|
19
|
+
* here and it stops being the mail they asked for; those belong in
|
|
20
|
+
* `activation-nudge`, which is gated on consent.
|
|
21
|
+
*/
|
|
22
|
+
export default defineJourney({
|
|
23
|
+
trigger: { event: "trial.started" },
|
|
24
|
+
purpose: "transactional",
|
|
25
|
+
from: "Acme <hello@example.com>",
|
|
26
|
+
description: "Opens a trial with the one thing to do first.",
|
|
27
|
+
tags: ["saas", "trial"],
|
|
28
|
+
run: async (_event, api) => {
|
|
29
|
+
const me = await api.profile.get();
|
|
30
|
+
await api.send.email({
|
|
31
|
+
template: "trial-welcome",
|
|
32
|
+
props: { firstName: str(me.traits.firstName) },
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": {
|
|
3
|
+
"id": "usr_1",
|
|
4
|
+
"traits": { "email": "ada@example.com", "firstName": "Ada" }
|
|
5
|
+
},
|
|
6
|
+
"events": [],
|
|
7
|
+
"expect": [
|
|
8
|
+
{
|
|
9
|
+
"activity": "sendEmail",
|
|
10
|
+
"input": { "template": "activated", "props": { "firstName": "Ada" } }
|
|
11
|
+
},
|
|
12
|
+
{ "activity": "setTrait", "input": { "key": "activated", "value": true } }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": {
|
|
3
|
+
"id": "usr_1",
|
|
4
|
+
"traits": { "email": "ada@example.com", "firstName": "Ada" }
|
|
5
|
+
},
|
|
6
|
+
"events": [{ "at": "4d", "event": "project.created" }],
|
|
7
|
+
"expect": [
|
|
8
|
+
{
|
|
9
|
+
"activity": "sendEmail",
|
|
10
|
+
"input": {
|
|
11
|
+
"template": "activation-nudge",
|
|
12
|
+
"props": { "firstName": "Ada" }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": {
|
|
3
|
+
"id": "usr_1",
|
|
4
|
+
"traits": { "email": "ada@example.com", "firstName": "Ada" }
|
|
5
|
+
},
|
|
6
|
+
"events": [],
|
|
7
|
+
"expect": [
|
|
8
|
+
{
|
|
9
|
+
"activity": "sendEmail",
|
|
10
|
+
"input": {
|
|
11
|
+
"template": "activation-nudge",
|
|
12
|
+
"props": { "firstName": "Ada" }
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"activity": "sendEmail",
|
|
17
|
+
"input": {
|
|
18
|
+
"template": "activation-help",
|
|
19
|
+
"props": { "firstName": "Ada" }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": {
|
|
3
|
+
"id": "usr_1",
|
|
4
|
+
"traits": { "email": "ada@example.com", "firstName": "Ada" }
|
|
5
|
+
},
|
|
6
|
+
"events": [],
|
|
7
|
+
"expect": [
|
|
8
|
+
{
|
|
9
|
+
"activity": "sendEmail",
|
|
10
|
+
"input": {
|
|
11
|
+
"template": "invite-your-team",
|
|
12
|
+
"props": { "firstName": "Ada" }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|