@company-semantics/contracts 27.12.0 → 27.13.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/package.json +1 -1
- package/src/email/README.md +16 -8
- package/src/email/index.ts +6 -0
- package/src/email/render/__tests__/__snapshots__/render-snapshot.test.ts.snap +973 -0
- package/src/email/render/__tests__/render-snapshot.test.ts +214 -0
- package/src/email/render/auth-otp.ts +91 -0
- package/src/email/render/base-text.ts +26 -0
- package/src/email/render/blocks.ts +308 -0
- package/src/email/render/chat-shared.ts +89 -0
- package/src/email/render/company-md-access-approved.ts +67 -0
- package/src/email/render/company-md-access-denied.ts +63 -0
- package/src/email/render/company-md-access-requested.ts +79 -0
- package/src/email/render/constants.ts +24 -0
- package/src/email/render/escape-html.ts +16 -0
- package/src/email/render/index.ts +90 -0
- package/src/email/render/org-invite.ts +61 -0
- package/src/email/render/ownership-transfer-completed.ts +31 -0
- package/src/email/render/ownership-transfer.ts +36 -0
- package/src/email/render/render-email.ts +166 -0
- package/src/email/render/security-alert.ts +62 -0
- package/src/email/render/share-granted.ts +95 -0
- package/src/email/render/unit-owner-granted.ts +79 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat-shared email (someone shared a chat). Plain text + HTML.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { EmailPayloads } from "../types";
|
|
6
|
+
|
|
7
|
+
import { baseTextLayout } from "./base-text";
|
|
8
|
+
import {
|
|
9
|
+
asciiCtaBox,
|
|
10
|
+
bodyParagraph,
|
|
11
|
+
chatAssistantBoxText,
|
|
12
|
+
chatAssistantBubble,
|
|
13
|
+
chatDots,
|
|
14
|
+
chatDotsText,
|
|
15
|
+
chatUserBoxText,
|
|
16
|
+
chatUserBubble,
|
|
17
|
+
ctaBox,
|
|
18
|
+
footer,
|
|
19
|
+
htmlShell,
|
|
20
|
+
monoParagraph,
|
|
21
|
+
NOTICE,
|
|
22
|
+
signatureLine,
|
|
23
|
+
} from "./blocks";
|
|
24
|
+
import {
|
|
25
|
+
COMPANY_NAME,
|
|
26
|
+
PREVIEW_TRUNCATE_LENGTH,
|
|
27
|
+
TITLE_TRUNCATE_LENGTH,
|
|
28
|
+
} from "./constants";
|
|
29
|
+
import { escapeHtml } from "./escape-html";
|
|
30
|
+
|
|
31
|
+
export type ChatSharedPayload = EmailPayloads["chat.shared"];
|
|
32
|
+
|
|
33
|
+
function truncate(text: string, max: number): string {
|
|
34
|
+
return text.length > max ? text.slice(0, max - 3) + "..." : text;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function renderChatSharedBody(payload: ChatSharedPayload): string {
|
|
38
|
+
const { chatTitle, shareUrl, previewText } = payload;
|
|
39
|
+
|
|
40
|
+
const sections: string[] = [];
|
|
41
|
+
sections.push("A chat has been shared with you.");
|
|
42
|
+
sections.push("");
|
|
43
|
+
sections.push(chatUserBoxText(chatTitle));
|
|
44
|
+
|
|
45
|
+
if (previewText) {
|
|
46
|
+
sections.push("");
|
|
47
|
+
sections.push(chatAssistantBoxText(previewText));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
sections.push("");
|
|
51
|
+
sections.push(chatDotsText());
|
|
52
|
+
sections.push("");
|
|
53
|
+
sections.push(asciiCtaBox("SEE MORE"));
|
|
54
|
+
sections.push("");
|
|
55
|
+
sections.push(shareUrl);
|
|
56
|
+
sections.push("");
|
|
57
|
+
sections.push(`This share was sent via ${COMPANY_NAME}.`);
|
|
58
|
+
sections.push("");
|
|
59
|
+
sections.push(NOTICE);
|
|
60
|
+
|
|
61
|
+
return baseTextLayout(sections.join("\n"));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function renderChatSharedHtml(payload: ChatSharedPayload): string {
|
|
65
|
+
const { chatTitle, shareUrl, previewText } = payload;
|
|
66
|
+
|
|
67
|
+
const title = escapeHtml(truncate(chatTitle, TITLE_TRUNCATE_LENGTH));
|
|
68
|
+
const previewHtml = previewText
|
|
69
|
+
? chatAssistantBubble(
|
|
70
|
+
escapeHtml(truncate(previewText, PREVIEW_TRUNCATE_LENGTH)),
|
|
71
|
+
)
|
|
72
|
+
: "";
|
|
73
|
+
|
|
74
|
+
return htmlShell([
|
|
75
|
+
bodyParagraph("A chat has been shared with you."),
|
|
76
|
+
chatUserBubble(title) + previewHtml,
|
|
77
|
+
chatDots(),
|
|
78
|
+
ctaBox({
|
|
79
|
+
label: "SEE MORE",
|
|
80
|
+
href: shareUrl,
|
|
81
|
+
padding: "12px 20px",
|
|
82
|
+
fontSize: "14px",
|
|
83
|
+
maxWidth: "200px",
|
|
84
|
+
}),
|
|
85
|
+
footer(`This share was sent via ${COMPANY_NAME}.`),
|
|
86
|
+
monoParagraph(NOTICE, "tight"),
|
|
87
|
+
signatureLine(),
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Company.md access-approved email (sent to the requester). Text + HTML.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { EmailPayloads } from "../types";
|
|
6
|
+
|
|
7
|
+
import { baseTextLayout } from "./base-text";
|
|
8
|
+
import {
|
|
9
|
+
ACCESS_PHRASE,
|
|
10
|
+
asciiCtaBox,
|
|
11
|
+
bodyParagraph,
|
|
12
|
+
ctaBox,
|
|
13
|
+
footer,
|
|
14
|
+
greeting,
|
|
15
|
+
htmlShell,
|
|
16
|
+
} from "./blocks";
|
|
17
|
+
import { COMPANY_NAME } from "./constants";
|
|
18
|
+
import { escapeHtml } from "./escape-html";
|
|
19
|
+
|
|
20
|
+
export type AccessApprovedPayload =
|
|
21
|
+
EmailPayloads["companyMd.access_request_approved"];
|
|
22
|
+
|
|
23
|
+
export function renderAccessApprovedBody(
|
|
24
|
+
payload: AccessApprovedPayload,
|
|
25
|
+
): string {
|
|
26
|
+
const { approverName, docTitle, accessLevel, docUrl } = payload;
|
|
27
|
+
|
|
28
|
+
const sections: string[] = [];
|
|
29
|
+
sections.push("Hi,");
|
|
30
|
+
sections.push("");
|
|
31
|
+
sections.push(
|
|
32
|
+
`${approverName} approved your request to access "${docTitle}" — you ${ACCESS_PHRASE[accessLevel]}.`,
|
|
33
|
+
);
|
|
34
|
+
sections.push("");
|
|
35
|
+
sections.push(asciiCtaBox("OPEN"));
|
|
36
|
+
sections.push("");
|
|
37
|
+
sections.push(docUrl);
|
|
38
|
+
sections.push("");
|
|
39
|
+
sections.push(`This notification was sent via ${COMPANY_NAME}.`);
|
|
40
|
+
|
|
41
|
+
return baseTextLayout(sections.join("\n"));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function renderAccessApprovedHtml(
|
|
45
|
+
payload: AccessApprovedPayload,
|
|
46
|
+
): string {
|
|
47
|
+
const { approverName, docTitle, accessLevel, docUrl } = payload;
|
|
48
|
+
|
|
49
|
+
return htmlShell([
|
|
50
|
+
greeting(),
|
|
51
|
+
bodyParagraph(
|
|
52
|
+
`<b>${escapeHtml(approverName)}</b> approved your request to access <b>"${escapeHtml(docTitle)}"</b> — you ${ACCESS_PHRASE[accessLevel]}.`,
|
|
53
|
+
),
|
|
54
|
+
ctaBox({
|
|
55
|
+
label: "OPEN",
|
|
56
|
+
href: docUrl,
|
|
57
|
+
padding: "16px 24px",
|
|
58
|
+
fontSize: "16px",
|
|
59
|
+
maxWidth: "220px",
|
|
60
|
+
}),
|
|
61
|
+
footer(
|
|
62
|
+
`This notification was sent via ${COMPANY_NAME}.`,
|
|
63
|
+
undefined,
|
|
64
|
+
"none",
|
|
65
|
+
),
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Company.md access-denied email (sent to the requester). No CTA. Text + HTML.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { EmailPayloads } from "../types";
|
|
6
|
+
|
|
7
|
+
import { baseTextLayout } from "./base-text";
|
|
8
|
+
import {
|
|
9
|
+
bodyParagraph,
|
|
10
|
+
footer,
|
|
11
|
+
greeting,
|
|
12
|
+
htmlShell,
|
|
13
|
+
quoteBlock,
|
|
14
|
+
textQuote,
|
|
15
|
+
} from "./blocks";
|
|
16
|
+
import { COMPANY_NAME } from "./constants";
|
|
17
|
+
import { escapeHtml } from "./escape-html";
|
|
18
|
+
|
|
19
|
+
export type AccessDeniedPayload =
|
|
20
|
+
EmailPayloads["companyMd.access_request_denied"];
|
|
21
|
+
|
|
22
|
+
export function renderAccessDeniedBody(payload: AccessDeniedPayload): string {
|
|
23
|
+
const { approverName, docTitle, reason } = payload;
|
|
24
|
+
|
|
25
|
+
const sections: string[] = [];
|
|
26
|
+
sections.push("Hi,");
|
|
27
|
+
sections.push("");
|
|
28
|
+
sections.push(
|
|
29
|
+
`${approverName} declined your request to access "${docTitle}".`,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (reason) {
|
|
33
|
+
sections.push("");
|
|
34
|
+
sections.push(textQuote(`Reason from ${approverName}`, reason));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
sections.push("");
|
|
38
|
+
sections.push(`This notification was sent via ${COMPANY_NAME}.`);
|
|
39
|
+
|
|
40
|
+
return baseTextLayout(sections.join("\n"));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function renderAccessDeniedHtml(payload: AccessDeniedPayload): string {
|
|
44
|
+
const { approverName, docTitle, reason } = payload;
|
|
45
|
+
|
|
46
|
+
return htmlShell([
|
|
47
|
+
greeting(),
|
|
48
|
+
bodyParagraph(
|
|
49
|
+
`<b>${escapeHtml(approverName)}</b> declined your request to access <b>"${escapeHtml(docTitle)}"</b>.`,
|
|
50
|
+
),
|
|
51
|
+
reason
|
|
52
|
+
? quoteBlock(
|
|
53
|
+
`Reason from ${escapeHtml(approverName)}:`,
|
|
54
|
+
escapeHtml(reason),
|
|
55
|
+
)
|
|
56
|
+
: "",
|
|
57
|
+
footer(
|
|
58
|
+
`This notification was sent via ${COMPANY_NAME}.`,
|
|
59
|
+
undefined,
|
|
60
|
+
"none",
|
|
61
|
+
),
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Company.md access-requested email (sent to the doc owner). Text + HTML.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { EmailPayloads } from "../types";
|
|
6
|
+
|
|
7
|
+
import { baseTextLayout } from "./base-text";
|
|
8
|
+
import {
|
|
9
|
+
asciiCtaBox,
|
|
10
|
+
bodyParagraph,
|
|
11
|
+
ctaBox,
|
|
12
|
+
footer,
|
|
13
|
+
greeting,
|
|
14
|
+
htmlShell,
|
|
15
|
+
quoteBlock,
|
|
16
|
+
signatureLine,
|
|
17
|
+
textQuote,
|
|
18
|
+
} from "./blocks";
|
|
19
|
+
import { COMPANY_NAME } from "./constants";
|
|
20
|
+
import { escapeHtml } from "./escape-html";
|
|
21
|
+
|
|
22
|
+
export type AccessRequestedPayload =
|
|
23
|
+
EmailPayloads["companyMd.access_requested"];
|
|
24
|
+
|
|
25
|
+
const OWNER_NOTE = "You are receiving this because you own this document.";
|
|
26
|
+
|
|
27
|
+
export function renderAccessRequestedBody(
|
|
28
|
+
payload: AccessRequestedPayload,
|
|
29
|
+
): string {
|
|
30
|
+
const { requesterName, docTitle, message, reviewUrl } = payload;
|
|
31
|
+
|
|
32
|
+
const sections: string[] = [];
|
|
33
|
+
sections.push("Hi,");
|
|
34
|
+
sections.push("");
|
|
35
|
+
sections.push(`${requesterName} is requesting access to "${docTitle}".`);
|
|
36
|
+
|
|
37
|
+
if (message) {
|
|
38
|
+
sections.push("");
|
|
39
|
+
sections.push(textQuote(`Message from ${requesterName}`, message));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
sections.push("");
|
|
43
|
+
sections.push(asciiCtaBox("REVIEW"));
|
|
44
|
+
sections.push("");
|
|
45
|
+
sections.push(reviewUrl);
|
|
46
|
+
sections.push("");
|
|
47
|
+
sections.push(`This notification was sent via ${COMPANY_NAME}.`);
|
|
48
|
+
sections.push(OWNER_NOTE);
|
|
49
|
+
|
|
50
|
+
return baseTextLayout(sections.join("\n"));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function renderAccessRequestedHtml(
|
|
54
|
+
payload: AccessRequestedPayload,
|
|
55
|
+
): string {
|
|
56
|
+
const { requesterName, docTitle, message, reviewUrl } = payload;
|
|
57
|
+
|
|
58
|
+
return htmlShell([
|
|
59
|
+
greeting(),
|
|
60
|
+
bodyParagraph(
|
|
61
|
+
`<b>${escapeHtml(requesterName)}</b> is requesting access to <b>"${escapeHtml(docTitle)}"</b>.`,
|
|
62
|
+
),
|
|
63
|
+
message
|
|
64
|
+
? quoteBlock(
|
|
65
|
+
`Message from ${escapeHtml(requesterName)}:`,
|
|
66
|
+
escapeHtml(message),
|
|
67
|
+
)
|
|
68
|
+
: "",
|
|
69
|
+
ctaBox({
|
|
70
|
+
label: "REVIEW",
|
|
71
|
+
href: reviewUrl,
|
|
72
|
+
padding: "16px 24px",
|
|
73
|
+
fontSize: "16px",
|
|
74
|
+
maxWidth: "220px",
|
|
75
|
+
}),
|
|
76
|
+
footer(`This notification was sent via ${COMPANY_NAME}.`, OWNER_NOTE),
|
|
77
|
+
signatureLine(),
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email render constants.
|
|
3
|
+
*
|
|
4
|
+
* Branding + layout constants shared by every email template. These are the
|
|
5
|
+
* single source of truth for both the backend (real sent emails) and the app
|
|
6
|
+
* (Ladle preview). `EMAIL_FROM` (SES envelope) is infra and stays in the
|
|
7
|
+
* backend, not here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Product name shown in footers and signatures. */
|
|
11
|
+
export const COMPANY_NAME = "Company Semantics";
|
|
12
|
+
|
|
13
|
+
/** Support address shown in the plain-text footer. */
|
|
14
|
+
export const SUPPORT_EMAIL = "support@companysemantics.ai";
|
|
15
|
+
|
|
16
|
+
/** Monospace font stack for HTML emails. */
|
|
17
|
+
export const MONO_FONT_STACK =
|
|
18
|
+
"'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace";
|
|
19
|
+
|
|
20
|
+
/** Max chat-title length before truncation (chat-shared). */
|
|
21
|
+
export const TITLE_TRUNCATE_LENGTH = 30;
|
|
22
|
+
|
|
23
|
+
/** Max preview-text length before truncation (chat-shared). */
|
|
24
|
+
export const PREVIEW_TRUNCATE_LENGTH = 70;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML escaping for email templates.
|
|
3
|
+
*
|
|
4
|
+
* INVARIANT: every user-controlled field in an HTML variant MUST pass through
|
|
5
|
+
* this before interpolation (prevents XSS in email clients).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Escape HTML special characters. */
|
|
9
|
+
export function escapeHtml(text: string): string {
|
|
10
|
+
return text
|
|
11
|
+
.replace(/&/g, "&")
|
|
12
|
+
.replace(/</g, "<")
|
|
13
|
+
.replace(/>/g, ">")
|
|
14
|
+
.replace(/"/g, """)
|
|
15
|
+
.replace(/'/g, "'");
|
|
16
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email render layer — the single source of truth for transactional email
|
|
3
|
+
* markup, composed by both the backend (real sends) and the app (Ladle preview).
|
|
4
|
+
*
|
|
5
|
+
* Import from `@company-semantics/contracts/email`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Constants
|
|
9
|
+
export {
|
|
10
|
+
COMPANY_NAME,
|
|
11
|
+
SUPPORT_EMAIL,
|
|
12
|
+
MONO_FONT_STACK,
|
|
13
|
+
TITLE_TRUNCATE_LENGTH,
|
|
14
|
+
PREVIEW_TRUNCATE_LENGTH,
|
|
15
|
+
} from "./constants";
|
|
16
|
+
|
|
17
|
+
// Primitives
|
|
18
|
+
export { escapeHtml } from "./escape-html";
|
|
19
|
+
export { baseTextLayout } from "./base-text";
|
|
20
|
+
|
|
21
|
+
// Shared building blocks
|
|
22
|
+
export {
|
|
23
|
+
htmlShell,
|
|
24
|
+
bodyParagraph,
|
|
25
|
+
greeting,
|
|
26
|
+
monoParagraph,
|
|
27
|
+
metaRow,
|
|
28
|
+
footer,
|
|
29
|
+
signatureLine,
|
|
30
|
+
NOTICE,
|
|
31
|
+
ACCESS_PHRASE,
|
|
32
|
+
chatDots,
|
|
33
|
+
ctaBox,
|
|
34
|
+
quoteBlock,
|
|
35
|
+
chatUserBubble,
|
|
36
|
+
chatAssistantBubble,
|
|
37
|
+
asciiCtaBox,
|
|
38
|
+
asciiCodeBox,
|
|
39
|
+
textQuote,
|
|
40
|
+
chatDotsText,
|
|
41
|
+
chatUserBoxText,
|
|
42
|
+
chatAssistantBoxText,
|
|
43
|
+
type Spacing,
|
|
44
|
+
type CtaBoxOptions,
|
|
45
|
+
} from "./blocks";
|
|
46
|
+
|
|
47
|
+
// Dispatcher
|
|
48
|
+
export {
|
|
49
|
+
renderEmail,
|
|
50
|
+
IMPLEMENTED_EMAIL_KINDS,
|
|
51
|
+
type ImplementedEmailKind,
|
|
52
|
+
type RenderedEmail,
|
|
53
|
+
type RenderEmailOptions,
|
|
54
|
+
} from "./render-email";
|
|
55
|
+
|
|
56
|
+
// Templates
|
|
57
|
+
export {
|
|
58
|
+
renderAuthOtpBody,
|
|
59
|
+
renderAuthOtpHtml,
|
|
60
|
+
type RenderOptions,
|
|
61
|
+
} from "./auth-otp";
|
|
62
|
+
export { renderOrgInviteBody, renderOrgInviteHtml } from "./org-invite";
|
|
63
|
+
export { renderChatSharedBody, renderChatSharedHtml } from "./chat-shared";
|
|
64
|
+
export {
|
|
65
|
+
renderSecurityAlertBody,
|
|
66
|
+
SECURITY_ALERT_TYPES,
|
|
67
|
+
type SecurityAlertType,
|
|
68
|
+
} from "./security-alert";
|
|
69
|
+
export {
|
|
70
|
+
renderUnitOwnerGrantedBody,
|
|
71
|
+
renderUnitOwnerGrantedHtml,
|
|
72
|
+
} from "./unit-owner-granted";
|
|
73
|
+
export {
|
|
74
|
+
renderShareGrantedBody,
|
|
75
|
+
renderShareGrantedHtml,
|
|
76
|
+
} from "./share-granted";
|
|
77
|
+
export { renderOwnershipTransferBody } from "./ownership-transfer";
|
|
78
|
+
export { renderOwnershipTransferCompletedBody } from "./ownership-transfer-completed";
|
|
79
|
+
export {
|
|
80
|
+
renderAccessRequestedBody,
|
|
81
|
+
renderAccessRequestedHtml,
|
|
82
|
+
} from "./company-md-access-requested";
|
|
83
|
+
export {
|
|
84
|
+
renderAccessApprovedBody,
|
|
85
|
+
renderAccessApprovedHtml,
|
|
86
|
+
} from "./company-md-access-approved";
|
|
87
|
+
export {
|
|
88
|
+
renderAccessDeniedBody,
|
|
89
|
+
renderAccessDeniedHtml,
|
|
90
|
+
} from "./company-md-access-denied";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Org invite email (join a workspace). Plain text + HTML.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { EmailPayloads } from "../types";
|
|
6
|
+
|
|
7
|
+
import { baseTextLayout } from "./base-text";
|
|
8
|
+
import {
|
|
9
|
+
asciiCtaBox,
|
|
10
|
+
bodyParagraph,
|
|
11
|
+
ctaBox,
|
|
12
|
+
footer,
|
|
13
|
+
htmlShell,
|
|
14
|
+
metaRow,
|
|
15
|
+
NOTICE,
|
|
16
|
+
signatureLine,
|
|
17
|
+
} from "./blocks";
|
|
18
|
+
import { COMPANY_NAME } from "./constants";
|
|
19
|
+
import { escapeHtml } from "./escape-html";
|
|
20
|
+
|
|
21
|
+
export type OrgInvitePayload = EmailPayloads["org.invite"];
|
|
22
|
+
|
|
23
|
+
export function renderOrgInviteBody(payload: OrgInvitePayload): string {
|
|
24
|
+
const { inviterName, orgName, role, acceptUrl, expiresInDays } = payload;
|
|
25
|
+
|
|
26
|
+
const sections: string[] = [];
|
|
27
|
+
sections.push("Workspace invitation.");
|
|
28
|
+
sections.push(asciiCtaBox("JOIN"));
|
|
29
|
+
sections.push("");
|
|
30
|
+
sections.push(`From: ${inviterName}`);
|
|
31
|
+
sections.push(`Workspace: ${orgName}`);
|
|
32
|
+
sections.push(`Role: ${role}`);
|
|
33
|
+
sections.push(`Expires in: ${expiresInDays} days`);
|
|
34
|
+
sections.push("");
|
|
35
|
+
sections.push(acceptUrl);
|
|
36
|
+
sections.push("");
|
|
37
|
+
sections.push(`This invitation was sent via ${COMPANY_NAME}.`);
|
|
38
|
+
sections.push(NOTICE);
|
|
39
|
+
|
|
40
|
+
return baseTextLayout(sections.join("\n"));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function renderOrgInviteHtml(payload: OrgInvitePayload): string {
|
|
44
|
+
const { inviterName, orgName, role, acceptUrl, expiresInDays } = payload;
|
|
45
|
+
|
|
46
|
+
return htmlShell([
|
|
47
|
+
bodyParagraph("Workspace invitation."),
|
|
48
|
+
ctaBox({
|
|
49
|
+
label: "JOIN",
|
|
50
|
+
href: acceptUrl,
|
|
51
|
+
padding: "20px 24px",
|
|
52
|
+
fontSize: "16px",
|
|
53
|
+
}),
|
|
54
|
+
metaRow("From", escapeHtml(inviterName), "tight"),
|
|
55
|
+
metaRow("Workspace", escapeHtml(orgName), "tight"),
|
|
56
|
+
metaRow("Role", role, "tight"),
|
|
57
|
+
metaRow("Expires in", `${expiresInDays} days`, "normal"),
|
|
58
|
+
footer(`This invitation was sent via ${COMPANY_NAME}.`, NOTICE),
|
|
59
|
+
signatureLine(),
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership-transfer-completed notice (plain text only).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { EmailPayloads } from "../types";
|
|
6
|
+
|
|
7
|
+
import { baseTextLayout } from "./base-text";
|
|
8
|
+
|
|
9
|
+
export type OwnershipTransferCompletedPayload =
|
|
10
|
+
EmailPayloads["org.ownership_transfer_completed"];
|
|
11
|
+
|
|
12
|
+
export function renderOwnershipTransferCompletedBody(
|
|
13
|
+
payload: OwnershipTransferCompletedPayload,
|
|
14
|
+
): string {
|
|
15
|
+
const { orgName, newOwnerEmail } = payload;
|
|
16
|
+
|
|
17
|
+
const sections: string[] = [];
|
|
18
|
+
sections.push(
|
|
19
|
+
`The ownership of "${orgName}" has been transferred to ${newOwnerEmail}.`,
|
|
20
|
+
);
|
|
21
|
+
sections.push("");
|
|
22
|
+
sections.push(
|
|
23
|
+
"You are no longer the owner of this workspace, but you remain an admin.",
|
|
24
|
+
);
|
|
25
|
+
sections.push("");
|
|
26
|
+
sections.push(
|
|
27
|
+
"If you did not authorize this transfer, please contact support immediately.",
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return baseTextLayout(sections.join("\n"));
|
|
31
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership-transfer invitation email (plain text only).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { EmailPayloads } from "../types";
|
|
6
|
+
|
|
7
|
+
import { baseTextLayout } from "./base-text";
|
|
8
|
+
import { textQuote } from "./blocks";
|
|
9
|
+
|
|
10
|
+
export type OwnershipTransferPayload = EmailPayloads["org.ownership_transfer"];
|
|
11
|
+
|
|
12
|
+
export function renderOwnershipTransferBody(
|
|
13
|
+
payload: OwnershipTransferPayload,
|
|
14
|
+
): string {
|
|
15
|
+
const { orgName, acceptUrl, expiresInDays, note } = payload;
|
|
16
|
+
|
|
17
|
+
const sections: string[] = [];
|
|
18
|
+
sections.push(`You have been invited to become the owner of "${orgName}".`);
|
|
19
|
+
|
|
20
|
+
if (note) {
|
|
21
|
+
sections.push("");
|
|
22
|
+
sections.push(textQuote("Message from the current owner", note));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
sections.push("");
|
|
26
|
+
sections.push("To accept this invitation, please click the link below:");
|
|
27
|
+
sections.push(acceptUrl);
|
|
28
|
+
sections.push("");
|
|
29
|
+
sections.push(`This invitation will expire in ${expiresInDays} days.`);
|
|
30
|
+
sections.push("");
|
|
31
|
+
sections.push(
|
|
32
|
+
"If you did not expect this invitation, you can safely ignore this email.",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return baseTextLayout(sections.join("\n"));
|
|
36
|
+
}
|